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:
getPuri(“w”) : Gets write Puri from BaseModelClass method
ubRegister() : Registers records with UpsertBuilder
transaction() : Starts transaction
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
User Signup
Update Profile
Create Post
Create Order
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
};
}
}
import { UserModel } from "./user/user.model" ;
import { Sonamu , api , UnauthorizedException } from "sonamu" ;
class UserFrame {
@ api ({ httpMethod: "POST" })
async updateProfile ( params : {
name ?: string ;
bio ?: string ;
avatar_url ?: string ;
}) {
const { user } = Sonamu . getContext ();
if ( ! user ) {
throw new UnauthorizedException ( "Login required" );
}
// Update profile
const [ id ] = await UserModel . save ([
{
id: user . id ,
... params ,
updated_at: new Date ()
}
]);
// Fetch updated user info
const updated = await UserModel . findById ( "A" , id );
return updated ;
}
}
import { PostModel } from "./post/post.model" ;
import { Sonamu , api } from "sonamu" ;
class PostFrame {
@ api ({ httpMethod: "POST" })
async createPost ( params : {
title : string ;
content : string ;
tags ?: string [];
}) {
const { user } = Sonamu . getContext ();
// Create post
const [ postId ] = await PostModel . save ([
{
user_id: user . id ,
title: params . title ,
content: params . content ,
status: "draft" ,
created_at: new Date ()
}
]);
// Save tags
if ( params . tags && params . tags . length > 0 ) {
await TagModel . save (
params . tags . map ( tag => ({
post_id: postId ,
name: tag
}))
);
}
return {
id: postId ,
title: params . title
};
}
}
import { OrderModel , OrderItemModel } from "./models" ;
import { Sonamu , api , transactional } from "sonamu" ;
class OrderFrame {
@ api ({ httpMethod: "POST" })
@ transactional ()
async createOrder ( params : {
items : Array <{
product_id : number ;
quantity : number ;
price : number ;
}>;
shipping_address : string ;
}) {
const { user } = Sonamu . getContext ();
// Calculate total
const total = params . items . reduce (
( sum , item ) => sum + ( item . price * item . quantity ),
0
);
// Create order
const [ orderId ] = await OrderModel . save ([
{
user_id: user . id ,
total_amount: total ,
status: "pending" ,
shipping_address: params . shipping_address ,
created_at: new Date ()
}
]);
// Create order items
const itemIds = await OrderItemModel . save (
params . items . map ( item => ({
order_id: orderId ,
product_id: item . product_id ,
quantity: item . quantity ,
price: item . price
}))
);
return {
orderId ,
itemCount: itemIds . length ,
total
};
}
}
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" ]
}
]);
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