Skip to main content
del is a method that deletes multiple records at once by ID array. It executes safely within a transaction and requires admin permissions by default.
del is not defined in BaseModelClass. It’s a standard pattern automatically generated by the Syncer in each Model class when you create an Entity.

Type Signature

async del(ids: number[]): Promise<number>

Auto-Generated Code

Sonamu automatically generates the following code based on your Entity:
// src/application/user/user.model.ts (auto-generated)
class UserModelClass extends BaseModelClass {
  @api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"], guards: ["admin"] })
  async del(ids: number[]): Promise<number> {
    const wdb = this.getPuri("w");

    // Delete within transaction
    await wdb.transaction(async (trx) => {
      return trx.table("users").whereIn("users.id", ids).delete();
    });

    return ids.length;
  }
}
How it works:
  1. getPuri(β€œw”): Gets write Puri from BaseModelClass method
  2. transaction(): Starts transaction
  3. table().whereIn().delete(): Executes delete using Knex methods
  4. Returns ids.length: Returns count of IDs requested for deletion

Parameters

ids

Array of IDs for records to delete. Type: number[]
// Delete single record
await UserModel.del([123]);

// Delete multiple records
await UserModel.del([1, 2, 3, 4, 5]);

// Empty array (deletes nothing)
await UserModel.del([]);

Return Value

Type: Promise<number> Returns the count of deleted records.
// Delete 3 records
const count = await UserModel.del([1, 2, 3]);
console.log(`${count} users deleted`);  // "3 users deleted"

// Including non-existent IDs
const count = await UserModel.del([1, 999, 1000]);
console.log(`${count} users deleted`);  // "1 users deleted" (only ID 1 exists)
Non-existent IDs are ignored and don’t throw errors.

Basic Usage

Delete Single Record

import { UserModel } from "./user/user.model";

class UserService {
  async deleteUser(userId: number) {
    const count = await UserModel.del([userId]);

    if (count === 0) {
      throw new Error("User not found");
    }

    return { success: true };
  }
}

Delete Multiple Records

async deleteUsers(userIds: number[]) {
  const count = await UserModel.del(userIds);

  return {
    requested: userIds.length,
    deleted: count
  };
}

Conditional Delete

async deleteInactiveUsers() {
  // Query inactive users
  const { rows } = await UserModel.findMany("A", {
    status: "inactive",
    num: 0
  });

  // Extract IDs and delete
  const ids = rows.map(user => user.id);
  const count = await UserModel.del(ids);

  return { deleted: count };
}

Transactions

del automatically executes within a transaction.
async del(ids: number[]): Promise<number> {
  const wdb = this.getPuri("w");

  // Transaction
  await wdb.transaction(async (trx) => {
    return trx.table("users")
      .whereIn("users.id", ids)
      .delete();
  });

  return ids.length;
}

With @transactional

import { api, transactional } from "sonamu";

class UserFrame {
  @api({ httpMethod: "POST" })
  @transactional()
  async deleteUserAndRelated(userId: number) {
    // Delete related data
    await PostModel.del(
      (await PostModel.findMany("A", {
        user_id: userId,
        num: 0
      })).rows.map(p => p.id)
    );

    // Delete user
    await UserModel.del([userId]);

    // All succeed or all fail
    return { success: true };
  }
}

Permission Check

By default, del requires admin permissions.
@api({
  httpMethod: "POST",
  clients: ["axios", "tanstack-mutation"],
  guards: ["admin"]  // Default setting
})
async del(ids: number[]): Promise<number> {
  // ...
}

Delete Own Data

import { Sonamu, api, UnauthorizedException } from "sonamu";

class PostFrame {
  @api({ httpMethod: "POST" })
  async deleteMyPost(postId: number) {
    const { user } = Sonamu.getContext();

    // Check post ownership
    const post = await PostModel.findById("A", postId);

    if (post.user_id !== user.id) {
      throw new UnauthorizedException("You can only delete your own posts");
    }

    await PostModel.del([postId]);

    return { success: true };
  }
}

Practical Examples

import { UserModel, PostModel, CommentModel } from "./models";
import { Sonamu, api, transactional } from "sonamu";

class UserFrame {
  @api({ httpMethod: "POST" })
  @transactional()
  async deleteAccount() {
    const { user } = Sonamu.getContext();

    // Delete all user comments
    const { rows: comments } = await CommentModel.findMany("A", {
      user_id: user.id,
      num: 0
    });
    if (comments.length > 0) {
      await CommentModel.del(comments.map(c => c.id));
    }

    // Delete all user posts
    const { rows: posts } = await PostModel.findMany("A", {
      user_id: user.id,
      num: 0
    });
    if (posts.length > 0) {
      await PostModel.del(posts.map(p => p.id));
    }

    // Delete user
    await UserModel.del([user.id]);

    // End session
    Sonamu.getContext().passport.logout();

    return { success: true };
  }
}

API Usage

Auto-Generated del API

// Model class
class UserModelClass extends BaseModelClass {
  @api({
    httpMethod: "POST",
    clients: ["axios", "tanstack-mutation"],
    guards: ["admin"]
  })
  async del(ids: number[]): Promise<number> {
    // Auto-generated code
  }
}

Client Code

import { UserService } from "@/services/UserService";

// Delete users
const count = await UserService.del([1, 2, 3]);
console.log(`${count} users deleted`);

React (TanStack Query)

import { useMutation, useQueryClient } from "@tanstack/react-query";
import { UserService } from "@/services/UserService";

function DeleteUserButton({ userId }: { userId: number }) {
  const queryClient = useQueryClient();

  const deleteUser = useMutation({
    mutationFn: (id: number) => UserService.del([id]),
    onSuccess: () => {
      // Invalidate cache
      queryClient.invalidateQueries({ queryKey: ["users"] });
    }
  });

  const handleDelete = () => {
    if (confirm("Are you sure you want to delete?")) {
      deleteUser.mutate(userId);
    }
  };

  return (
    <button
      onClick={handleDelete}
      disabled={deleteUser.isPending}
    >
      {deleteUser.isPending ? "Deleting..." : "Delete"}
    </button>
  );
}

Foreign Key Constraints

CASCADE Setting

-- Automatically delete child records when parent is deleted
CREATE TABLE comments (
  id SERIAL PRIMARY KEY,
  post_id INTEGER NOT NULL,
  content TEXT NOT NULL,
  FOREIGN KEY (post_id)
    REFERENCES posts(id)
    ON DELETE CASCADE
);
// Comments automatically deleted when post is deleted
await PostModel.del([1]);

RESTRICT Setting

-- Cannot delete parent record if child records exist
CREATE TABLE posts (
  id SERIAL PRIMARY KEY,
  user_id INTEGER NOT NULL,
  FOREIGN KEY (user_id)
    REFERENCES users(id)
    ON DELETE RESTRICT
);
// Error when deleting user with posts
try {
  await UserModel.del([1]);
} catch (error) {
  // Foreign key violation
  console.error("Cannot delete user with posts");
}

Manual Handling

@transactional()
async deleteUserWithPosts(userId: number) {
  // Delete posts first
  const { rows: posts } = await PostModel.findMany("A", {
    user_id: userId,
    num: 0
  });

  if (posts.length > 0) {
    await PostModel.del(posts.map(p => p.id));
  }

  // Then delete user
  await UserModel.del([userId]);

  return { success: true };
}

Deletion Verification

Check Existence

async deleteUserSafely(userId: number) {
  // Check user exists
  try {
    await UserModel.findById("A", userId);
  } catch (error) {
    if (error instanceof NotFoundException) {
      throw new BadRequestException("User not found");
    }
    throw error;
  }

  // Delete
  const count = await UserModel.del([userId]);

  return { success: count > 0 };
}

Verify After Delete

async deleteAndVerify(userId: number) {
  // Delete
  const count = await UserModel.del([userId]);

  // Verify actually deleted
  const deleted = await UserModel.findOne("A", { id: userId });

  return {
    deleteCount: count,
    verified: deleted === null
  };
}

Performance Optimization

Batch Delete

Split large deletions into batches.
async deleteManyUsers(ids: number[]) {
  const batchSize = 500;
  let totalDeleted = 0;

  for (let i = 0; i < ids.length; i += batchSize) {
    const batch = ids.slice(i, i + batchSize);
    const count = await UserModel.del(batch);
    totalDeleted += count;
  }

  return { total: totalDeleted };
}

Index Utilization

Add indexes on foreign keys that are frequently deleted.
-- If deleting by user_id frequently
CREATE INDEX idx_posts_user_id ON posts(user_id);

Cautions

1. Pass as Array

del must receive an array.
// ❌ Wrong
await UserModel.del(123);

// βœ… Correct
await UserModel.del([123]);

2. Return Value is Count

const count = await UserModel.del([1, 2, 3]);
console.log(count);  // 3 (or actual deleted count)

3. CASCADE Caution

CASCADE settings can delete unintended data.
// ❌ Dangerous: all comments on posts also deleted
await PostModel.del([1, 2, 3]);
Use transactions when deleting from multiple related tables.
// βœ… Safe: all succeed or all fail
@transactional()
async deleteUserAndRelated(userId: number) {
  await CommentModel.del([...]);
  await PostModel.del([...]);
  await UserModel.del([userId]);
}

5. Permission Check

Be careful when changing default guards: ["admin"] setting.
// ⚠️ Warning: anyone can delete
@api({ guards: [] })
async del(ids: number[]) {
  // Dangerous!
}

Soft Delete vs Hard Delete

Hard Delete (del)

// Permanently remove from DB
await UserModel.del([1]);
Pros:
  • Saves disk space
  • Simple structure
Cons:
  • Cannot recover
  • History lost

Soft Delete (save)

// Use status or deleted_at column
await UserModel.save([
  {
    id: 1,
    status: "deleted",
    deleted_at: new Date()
  }
]);
Pros:
  • Recoverable
  • Preserves history
  • Audit trail
Cons:
  • Increases disk usage
  • Query complexity increases

Next Steps