Skip to main content
save is a method that saves or updates records. It automatically handles INSERT/UPDATE using UpsertBuilder and executes safely within a transaction.
save 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 save(
  saveParams: SaveParams[]
): 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"] })
  async save(spa: UserSaveParams[]): Promise<number[]> {
    const wdb = this.getPuri("w");

    // 1. Register records with UpsertBuilder
    spa.forEach((sp) => {
      wdb.ubRegister("users", sp);
    });

    // 2. Execute Upsert within transaction
    return wdb.transaction(async (trx) => {
      const ids = await trx.ubUpsert("users");
      return ids;
    });
  }
}
How it works:
  1. getPuri(“w”): Gets write Puri from BaseModelClass method
  2. ubRegister(): Registers records with UpsertBuilder
  3. transaction(): Starts transaction
  4. ubUpsert(): Automatically handles INSERT/UPDATE and returns IDs

Parameters

saveParams

Array of record data to save. Type: SaveParams[]
type UserSaveParams = {
  id?: number;         // If present: UPDATE, if absent: INSERT
  email: string;
  name: string;
  status?: string;
  // ... other fields
}

// Save single record
await UserModel.save([
  { email: "john@example.com", name: "John" }
]);

// Save multiple records
await UserModel.save([
  { email: "john@example.com", name: "John" },
  { email: "jane@example.com", name: "Jane" }
]);

Role of id

  • If id exists: UPDATE the record with that ID
  • If id is absent: INSERT a new record
// INSERT (no id)
await UserModel.save([
  { email: "new@example.com", name: "New User" }
]);

// UPDATE (with id)
await UserModel.save([
  { id: 1, email: "updated@example.com", name: "Updated Name" }
]);

Return Value

Type: Promise<number[]> Returns an array of IDs for the saved records.
// INSERT
const [id] = await UserModel.save([
  { email: "john@example.com", name: "John" }
]);
console.log("Created ID:", id);  // 123

// UPDATE
const [id] = await UserModel.save([
  { id: 123, name: "John Smith" }
]);
console.log("Updated ID:", id);  // 123

// Multiple records
const ids = await UserModel.save([
  { email: "a@example.com", name: "A" },
  { email: "b@example.com", name: "B" }
]);
console.log("Created IDs:", ids);  // [124, 125]

Basic Usage

Create New Record (INSERT)

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

class UserService {
  async createUser(email: string, name: string) {
    const [id] = await UserModel.save([
      {
        email,
        name,
        status: "active",
        created_at: new Date()
      }
    ]);

    return { id };
  }
}

Update Record (UPDATE)

async updateUser(userId: number, name: string) {
  const [id] = await UserModel.save([
    {
      id: userId,
      name,
      updated_at: new Date()
    }
  ]);

  return { id };
}

Upsert (INSERT or UPDATE)

async upsertUser(userData: { id?: number; email: string; name: string }) {
  const [id] = await UserModel.save([userData]);

  return { id };
}

UpsertBuilder How It Works

1. Register Records (ubRegister)

async save(spa: UserSaveParams[]): Promise<number[]> {
  const wdb = this.getPuri("w");

  // Register each record with UpsertBuilder
  spa.forEach((sp) => {
    wdb.ubRegister("users", sp);
  });

  // ...
}

2. Execute Transaction

// Execute upsert within transaction
return wdb.transaction(async (trx) => {
  const ids = await trx.ubUpsert("users");
  return ids;
});

3. Automatic INSERT/UPDATE Handling

UpsertBuilder automatically performs INSERT or UPDATE based on the presence of the id field:
-- No id: INSERT
INSERT INTO users (email, name, status)
VALUES ('john@example.com', 'John', 'active')
RETURNING id;

-- With id: UPDATE
INSERT INTO users (id, email, name)
VALUES (123, 'john@example.com', 'John Smith')
ON CONFLICT (id)
DO UPDATE SET
  email = EXCLUDED.email,
  name = EXCLUDED.name
RETURNING id;

Batch Save

You can save multiple records at once.
async bulkCreateUsers(users: { email: string; name: string }[]) {
  const ids = await UserModel.save(
    users.map(user => ({
      email: user.email,
      name: user.name,
      status: "active",
      created_at: new Date()
    }))
  );

  return { count: ids.length, ids };
}

Saving Relationship Data

1:N Relationship

// Save post + comments
const postId = await PostModel.save([
  {
    title: "My Post",
    content: "Content here"
  }
]);

await CommentModel.save([
  {
    post_id: postId[0],
    content: "First comment"
  },
  {
    post_id: postId[0],
    content: "Second comment"
  }
]);

N:M Relationship

// Save user + assign roles
const userId = await UserModel.save([
  {
    email: "admin@example.com",
    name: "Admin User"
  }
]);

await UserRoleModel.save([
  {
    user_id: userId[0],
    role_id: 1  // Admin
  },
  {
    user_id: userId[0],
    role_id: 2  // Editor
  }
]);

Practical Examples

import { UserModel } from "./user/user.model";
import { api, BadRequestException } from "sonamu";
import bcrypt from "bcrypt";

class AuthFrame {
  @api({ httpMethod: "POST" })
  async signup(params: {
    email: string;
    password: string;
    name: string;
  }) {
    // Check email duplicate
    const existing = await UserModel.findOne("A", {
      email: params.email
    });

    if (existing) {
      throw new BadRequestException("Email already in use");
    }

    // Hash password
    const hashedPassword = await bcrypt.hash(params.password, 10);

    // Create user
    const [id] = await UserModel.save([
      {
        email: params.email,
        password: hashedPassword,
        name: params.name,
        status: "active",
        email_verified: false,
        created_at: new Date()
      }
    ]);

    return {
      id,
      email: params.email,
      name: params.name
    };
  }
}

Partial Update

You can update only specified fields.
// Update only name
await UserModel.save([
  {
    id: 123,
    name: "New Name"
    // Other fields remain unchanged
  }
]);

// Update multiple fields
await UserModel.save([
  {
    id: 123,
    name: "New Name",
    email: "new@example.com",
    updated_at: new Date()
  }
]);
Fields not specified are not changed. To set a field to NULL, explicitly pass null.

Transactions

save automatically executes within a transaction.
// Automatic transaction
return wdb.transaction(async (trx) => {
  const ids = await trx.ubUpsert("users");
  return ids;
});

With @transactional

import { api, transactional } from "sonamu";

class UserFrame {
  @api({ httpMethod: "POST" })
  @transactional()
  async createUserWithProfile(params: {
    email: string;
    name: string;
    bio: string;
  }) {
    // Create user
    const [userId] = await UserModel.save([
      {
        email: params.email,
        name: params.name
      }
    ]);

    // Create profile
    await ProfileModel.save([
      {
        user_id: userId,
        bio: params.bio
      }
    ]);

    // Both succeed or both fail (atomicity)
    return { userId };
  }
}

Validation

1. Automatic Zod Validation

SaveParams are automatically generated as Zod schemas from Entity definitions.
// Automatically validated
await UserModel.save([
  {
    email: "invalid-email",  // ❌ Email format validation fails
    name: "John"
  }
]);

2. Custom Validation

async createUser(params: UserSaveParams) {
  // Check email duplicate
  const existing = await UserModel.findOne("A", {
    email: params.email
  });

  if (existing) {
    throw new BadRequestException("Email already in use");
  }

  // Check password strength
  if (params.password.length < 8) {
    throw new BadRequestException("Password must be at least 8 characters");
  }

  return await UserModel.save([params]);
}

API Usage

Auto-Generated save API

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

Client Code

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

// Create user
const ids = await UserService.save([
  {
    email: "john@example.com",
    name: "John"
  }
]);

React (TanStack Query)

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

function CreateUserForm() {
  const createUser = useMutation({
    mutationFn: (params: { email: string; name: string }) =>
      UserService.save([params])
  });

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);

    createUser.mutate({
      email: formData.get("email") as string,
      name: formData.get("name") as string
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" required />
      <input name="name" required />
      <button type="submit" disabled={createUser.isPending}>
        {createUser.isPending ? "Creating..." : "Create User"}
      </button>

      {createUser.isSuccess && (
        <p>User created with ID: {createUser.data[0]}</p>
      )}

      {createUser.isError && (
        <p>Error: {createUser.error.message}</p>
      )}
    </form>
  );
}

Advanced Features

Unique Constraint Handling

UpsertBuilder automatically handles Unique constraints.
// If email is unique
await UserModel.save([
  { email: "john@example.com", name: "John" }  // INSERT
]);

await UserModel.save([
  { email: "john@example.com", name: "John Smith" }  // UPDATE (same email)
]);

JSON Columns

await UserModel.save([
  {
    id: 1,
    metadata: {
      preferences: {
        theme: "dark",
        language: "ko"
      },
      tags: ["vip", "premium"]
    }
  }
]);

Array Columns (PostgreSQL)

await PostModel.save([
  {
    id: 1,
    tags: ["typescript", "node", "database"]
  }
]);

Performance Optimization

Batch Size Limit

// Split large data saves into chunks
const users = [/* 10000 items */];

// Save in chunks of 500
for (let i = 0; i < users.length; i += 500) {
  const chunk = users.slice(i, i + 500);
  await UserModel.save(chunk);
}

Transaction Reuse

@api({ httpMethod: "POST" })
@transactional()
async bulkOperation(data: LargeDataSet) {
  // Execute multiple saves in same transaction
  await UserModel.save(data.users);
  await PostModel.save(data.posts);
  await CommentModel.save(data.comments);

  // All succeed or all fail
}

Cautions

1. Pass as Array

save must receive an array.
// ❌ Wrong
await UserModel.save({ email: "john@example.com" });

// ✅ Correct
await UserModel.save([{ email: "john@example.com" }]);

2. Return Value is ID Array

// Single record
const [id] = await UserModel.save([{ ... }]);

// Multiple records
const [id1, id2, id3] = await UserModel.save([{ ... }, { ... }, { ... }]);

3. Caution with Partial Updates

// ❌ Email NOT changed to null
await UserModel.save([
  {
    id: 1,
    name: "New Name"
    // No email field → keeps existing value, does NOT change to null
  }
]);

// ✅ Explicit null
await UserModel.save([
  {
    id: 1,
    email: null  // Explicit null
  }
]);

4. Relationship Data Order

Save parent records before child records.
// ✅ Correct order
const [postId] = await PostModel.save([{ ... }]);
await CommentModel.save([{ post_id: postId, ... }]);

// ❌ Wrong order (no post_id)
await CommentModel.save([{ post_id: undefined, ... }]);
const [postId] = await PostModel.save([{ ... }]);

Next Steps