Skip to content

Update Operations ​

Update operations in Routier leverage the framework's powerful change tracking system. Entities returned from queries are proxy objects that automatically track changes, making updates simple and efficient.

Quick Navigation ​

Overview ​

Routier's update system works through:

  1. Proxy-based change tracking - Entities automatically track modifications
  2. No manual update calls - Changes are detected automatically
  3. Batch change management - Multiple changes are saved together
  4. Type-safe updates - Full TypeScript support for property modifications
  5. Efficient persistence - Changes are optimized for database operations

How Change Tracking Works ​

Proxy Entities ​

When you query entities in Routier, they are returned as proxy objects that automatically track changes:

ts
import { DataStore } from "@routier/datastore";
import { MemoryPlugin } from "@routier/memory-plugin";
import { s } from "@routier/core/schema";

// Define a user schema
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    name: s.string(),
    email: s.string().distinct(),
    age: s.number(),
    isActive: s.boolean().default(true),
    createdAt: s.date().default(() => new Date()),
}).compile();

// Create DataStore with collection
class AppContext extends DataStore {
    constructor() {
        super(new MemoryPlugin("app-db"));
    }

    users = this.collection(userSchema).proxy().create();
}

const ctx = new AppContext();

// Proxy entities - entities returned from queries are proxy objects
const user = await ctx.users.firstAsync();
console.log("User before update:", user);

// Direct property modification - changes are automatically tracked
user.name = "Updated Name";
user.email = "[email protected]";
user.age = 30;

console.log("User after update:", user);
console.log("Changes tracked automatically");

// Save changes to persist updates
await ctx.saveChangesAsync();

Automatic Change Detection ​

Routier automatically detects property changes without requiring manual update calls:

ts
import { DataStore } from "@routier/datastore";
import { MemoryPlugin } from "@routier/memory-plugin";
import { s } from "@routier/core/schema";

// Define a user schema
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    name: s.string(),
    email: s.string().distinct(),
    age: s.number(),
    isActive: s.boolean().default(true),
    createdAt: s.date().default(() => new Date()),
}).compile();

// Create DataStore with collection
class AppContext extends DataStore {
    constructor() {
        super(new MemoryPlugin("app-db"));
    }

    users = this.collection(userSchema).proxy().create();
}

const ctx = new AppContext();

// Automatic change detection - no manual update calls needed
const user = await ctx.users.firstAsync();

// Modify properties directly - Routier automatically detects changes
user.name = "John Updated";
user.email = "[email protected]";
user.age = 31;

// Changes are tracked automatically - no need to call update methods
console.log("User updated:", user);

// Save all tracked changes
await ctx.saveChangesAsync();

Basic Update Operations ​

Single Property Updates ​

Update individual properties on entities:

ts
import { DataStore } from "@routier/datastore";
import { MemoryPlugin } from "@routier/memory-plugin";
import { s } from "@routier/core/schema";

// Define a user schema
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    name: s.string(),
    email: s.string().distinct(),
    age: s.number(),
    isActive: s.boolean().default(true),
    createdAt: s.date().default(() => new Date()),
}).compile();

// Create DataStore with collection
class AppContext extends DataStore {
    constructor() {
        super(new MemoryPlugin("app-db"));
    }

    users = this.collection(userSchema).proxy().create();
}

const ctx = new AppContext();

// Single property updates
const user = await ctx.users.firstAsync();

// Update individual properties
user.name = "New Name";
console.log("Updated name:", user.name);

user.email = "[email protected]";
console.log("Updated email:", user.email);

user.age = 25;
console.log("Updated age:", user.age);

// Save changes
await ctx.saveChangesAsync();

Multiple Property Updates ​

Update multiple properties on a single entity:

ts
import { DataStore } from "@routier/datastore";
import { MemoryPlugin } from "@routier/memory-plugin";
import { s } from "@routier/core/schema";

// Define a user schema
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    name: s.string(),
    email: s.string().distinct(),
    age: s.number(),
    isActive: s.boolean().default(true),
    createdAt: s.date().default(() => new Date()),
}).compile();

// Create DataStore with collection
class AppContext extends DataStore {
    constructor() {
        super(new MemoryPlugin("app-db"));
    }

    users = this.collection(userSchema).proxy().create();
}

const ctx = new AppContext();

// Multiple property updates
const user = await ctx.users.firstAsync();

// Update multiple properties at once
user.name = "John Doe";
user.email = "[email protected]";
user.age = 30;
user.isActive = true;

console.log("Updated user:", user);

// All changes are tracked and will be saved together
await ctx.saveChangesAsync();

Nested Object Updates ​

Update nested objects and their properties:

ts
import { DataStore } from "@routier/datastore";
import { MemoryPlugin } from "@routier/memory-plugin";
import { s } from "@routier/core/schema";

// Define a user schema with nested objects
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    name: s.string(),
    email: s.string().distinct(),
    profile: s.object({
        firstName: s.string(),
        lastName: s.string(),
        bio: s.string().optional(),
        avatar: s.string().optional(),
    }),
    preferences: s.object({
        theme: s.string("light", "dark").default("light"),
        notifications: s.boolean().default(true),
    }).default({}),
}).compile();

// Create DataStore with collection
class AppContext extends DataStore {
    constructor() {
        super(new MemoryPlugin("app-db"));
    }

    users = this.collection(userSchema).proxy().create();
}

const ctx = new AppContext();

// Nested object updates
const user = await ctx.users.firstAsync();

// Update nested object properties
user.profile.firstName = "John";
user.profile.lastName = "Doe";
user.profile.bio = "Software developer";

// Update preferences
user.preferences.theme = "dark";
user.preferences.notifications = false;

console.log("Updated user with nested objects:", user);

// Save changes
await ctx.saveChangesAsync();

Array Updates ​

Modify arrays within entities:

ts
import { DataStore } from "@routier/datastore";
import { MemoryPlugin } from "@routier/memory-plugin";
import { s } from "@routier/core/schema";

// Define a user schema with arrays
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    name: s.string(),
    email: s.string().distinct(),
    tags: s.array(s.string()).default([]),
    scores: s.array(s.number()).default([]),
}).compile();

// Create DataStore with collection
class AppContext extends DataStore {
    constructor() {
        super(new MemoryPlugin("app-db"));
    }

    users = this.collection(userSchema).proxy().create();
}

const ctx = new AppContext();

// Array updates
const user = await ctx.users.firstAsync();

// Update array properties
user.tags = ["developer", "javascript", "typescript"];
user.scores = [85, 92, 78, 96];

console.log("Updated user with arrays:", user);

// Modify existing arrays
user.tags.push("react");
user.scores.push(88);

console.log("Added to arrays:", user);

// Save changes
await ctx.saveChangesAsync();

Batch Update Operations ​

Update Multiple Entities ​

Update multiple entities efficiently:

ts
import { DataStore } from "@routier/datastore";
import { MemoryPlugin } from "@routier/memory-plugin";
import { s } from "@routier/core/schema";

// Define a user schema
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    name: s.string(),
    email: s.string().distinct(),
    age: s.number(),
    isActive: s.boolean().default(true),
    createdAt: s.date().default(() => new Date()),
}).compile();

// Create DataStore with collection
class AppContext extends DataStore {
    constructor() {
        super(new MemoryPlugin("app-db"));
    }

    users = this.collection(userSchema).proxy().create();
}

const ctx = new AppContext();

// Update multiple entities
const users = await ctx.users.where(u => u.age >= 18).toArrayAsync();

// Update multiple entities in batch
users.forEach(user => {
    user.isActive = true;
    user.name = user.name.toUpperCase();
});

console.log("Updated users:", users);

// All changes are tracked and saved together
await ctx.saveChangesAsync();

Conditional Batch Updates ​

Apply updates based on conditions:

ts
import { DataStore } from "@routier/datastore";
import { MemoryPlugin } from "@routier/memory-plugin";
import { s } from "@routier/core/schema";

// Define a user schema
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    name: s.string(),
    email: s.string().distinct(),
    age: s.number(),
    isActive: s.boolean().default(true),
    createdAt: s.date().default(() => new Date()),
}).compile();

// Create DataStore with collection
class AppContext extends DataStore {
    constructor() {
        super(new MemoryPlugin("app-db"));
    }

    users = this.collection(userSchema).proxy().create();
}

const ctx = new AppContext();

// Conditional batch updates
const users = await ctx.users.toArrayAsync();

// Apply conditional updates
users.forEach(user => {
    if (user.age >= 18) {
        user.isActive = true;
    }

    if (user.email.includes("admin")) {
        user.name = `Admin: ${user.name}`;
    }

    if (user.createdAt < new Date("2023-01-01")) {
        user.name = `Legacy: ${user.name}`;
    }
});

console.log("Conditionally updated users:", users);

// Save all changes
await ctx.saveChangesAsync();

Batch Updates with Transformations ​

Apply transformations to multiple entities:

ts
import { DataStore } from "@routier/datastore";
import { MemoryPlugin } from "@routier/memory-plugin";
import { s } from "@routier/core/schema";

// Define a user schema
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    name: s.string(),
    email: s.string().distinct(),
    age: s.number(),
    score: s.number().default(0),
    createdAt: s.date().default(() => new Date()),
}).compile();

// Create DataStore with collection
class AppContext extends DataStore {
    constructor() {
        super(new MemoryPlugin("app-db"));
    }

    users = this.collection(userSchema).proxy().create();
}

const ctx = new AppContext();

// Batch updates with transformations
const users = await ctx.users.toArrayAsync();

// Apply transformations to multiple users
users.forEach(user => {
    // Transform name to uppercase
    user.name = user.name.toUpperCase();

    // Calculate score based on age
    user.score = user.age * 10;

    // Add prefix to email
    user.email = `user_${user.email}`;
});

console.log("Transformed users:", users);

// Save all transformations
await ctx.saveChangesAsync();

Advanced Update Patterns ​

Computed Updates ​

Update entities with computed or derived values:

ts
import { DataStore } from "@routier/datastore";
import { MemoryPlugin } from "@routier/memory-plugin";
import { s } from "@routier/core/schema";

// Define a user schema with computed properties
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    firstName: s.string(),
    lastName: s.string(),
    email: s.string().distinct(),
    age: s.number(),
}).modify(w => ({
    // Computed property that updates when firstName or lastName changes
    fullName: w.computed((entity) => `${entity.firstName} ${entity.lastName}`).tracked(),
})).compile();

// Create DataStore with collection
class AppContext extends DataStore {
    constructor() {
        super(new MemoryPlugin("app-db"));
    }

    users = this.collection(userSchema).proxy().create();
}

const ctx = new AppContext();

// Computed updates - computed properties update automatically
const user = await ctx.users.firstAsync();

console.log("Before update:", user.fullName); // "John Doe"

// Update the underlying properties
user.firstName = "Jane";
user.lastName = "Smith";

console.log("After update:", user.fullName); // "Jane Smith" - automatically computed

// Save changes
await ctx.saveChangesAsync();

Incremental Updates ​

Apply incremental changes to numeric fields:

ts
import { DataStore } from "@routier/datastore";
import { MemoryPlugin } from "@routier/memory-plugin";
import { s } from "@routier/core/schema";

// Define a user schema
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    name: s.string(),
    email: s.string().distinct(),
    score: s.number().default(0),
    level: s.number().default(1),
    createdAt: s.date().default(() => new Date()),
}).compile();

// Create DataStore with collection
class AppContext extends DataStore {
    constructor() {
        super(new MemoryPlugin("app-db"));
    }

    users = this.collection(userSchema).proxy().create();
}

const ctx = new AppContext();

// Incremental updates
const user = await ctx.users.firstAsync();

// Increment numeric values
user.score += 10;
user.level += 1;

console.log("Incremented values:", { score: user.score, level: user.level });

// Decrement values
user.score -= 5;

console.log("Decremented score:", user.score);

// Save changes
await ctx.saveChangesAsync();

Conditional Field Updates ​

Update fields based on specific conditions:

ts
import { DataStore } from "@routier/datastore";
import { MemoryPlugin } from "@routier/memory-plugin";
import { s } from "@routier/core/schema";

// Define a user schema
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    name: s.string(),
    email: s.string().distinct(),
    age: s.number(),
    isActive: s.boolean().default(true),
    role: s.string("admin", "user", "guest").default("user"),
    createdAt: s.date().default(() => new Date()),
}).compile();

// Create DataStore with collection
class AppContext extends DataStore {
    constructor() {
        super(new MemoryPlugin("app-db"));
    }

    users = this.collection(userSchema).proxy().create();
}

const ctx = new AppContext();

// Conditional field updates
const user = await ctx.users.firstAsync();

// Update fields based on conditions
if (user.age >= 18) {
    user.isActive = true;
}

if (user.email.includes("admin")) {
    user.role = "admin";
}

if (user.createdAt < new Date("2023-01-01")) {
    user.name = `Legacy: ${user.name}`;
}

// Only update if certain conditions are met
if (user.score && user.score > 100) {
    user.role = "premium";
}

console.log("Conditionally updated user:", user);

// Save changes
await ctx.saveChangesAsync();

Change Management ​

Checking for Changes ​

Monitor and check for pending changes:

ts
import { DataStore } from "@routier/datastore";
import { MemoryPlugin } from "@routier/memory-plugin";
import { s } from "@routier/core/schema";

// Define a user schema
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    name: s.string(),
    email: s.string().distinct(),
    age: s.number(),
    createdAt: s.date().default(() => new Date()),
}).compile();

// Create DataStore with collection
class AppContext extends DataStore {
    constructor() {
        super(new MemoryPlugin("app-db"));
    }

    users = this.collection(userSchema).proxy().create();
}

const ctx = new AppContext();

// Checking for changes
const user = await ctx.users.firstAsync();

// Check if there are any pending changes
const hasChanges = ctx.hasChanges();
console.log("Has changes:", hasChanges); // false

// Make some changes
user.name = "Updated Name";
user.email = "[email protected]";

// Check again
const hasChangesAfter = ctx.hasChanges();
console.log("Has changes after update:", hasChangesAfter); // true

// Save changes
await ctx.saveChangesAsync();

// Check after save
const hasChangesAfterSave = ctx.hasChanges();
console.log("Has changes after save:", hasChangesAfterSave); // false

Saving Changes ​

Persist tracked changes to the database:

ts
import { DataStore } from "@routier/datastore";
import { MemoryPlugin } from "@routier/memory-plugin";
import { s } from "@routier/core/schema";

// Define a user schema
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    name: s.string(),
    email: s.string().distinct(),
    age: s.number(),
    createdAt: s.date().default(() => new Date()),
}).compile();

// Create DataStore with collection
class AppContext extends DataStore {
    constructor() {
        super(new MemoryPlugin("app-db"));
    }

    users = this.collection(userSchema).proxy().create();
}

const ctx = new AppContext();

// Saving changes
const user = await ctx.users.firstAsync();

// Make changes
user.name = "Updated Name";
user.email = "[email protected]";
user.age = 30;

console.log("Changes made:", user);

// Save all tracked changes
await ctx.saveChangesAsync();
console.log("Changes saved successfully");

// Verify changes were persisted
const updatedUser = await ctx.users.firstAsync();
console.log("Persisted user:", updatedUser);

Partial Saves ​

Save changes in batches or selectively:

ts
import { DataStore } from "@routier/datastore";
import { MemoryPlugin } from "@routier/memory-plugin";
import { s } from "@routier/core/schema";

// Define a user schema
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    name: s.string(),
    email: s.string().distinct(),
    age: s.number(),
    createdAt: s.date().default(() => new Date()),
}).compile();

// Create DataStore with collection
class AppContext extends DataStore {
    constructor() {
        super(new MemoryPlugin("app-db"));
    }

    users = this.collection(userSchema).proxy().create();
}

const ctx = new AppContext();

// Partial saves - save changes incrementally
const user1 = await ctx.users.firstAsync();
const user2 = await ctx.users.skip(1).firstAsync();

// Update first user
user1.name = "Updated User 1";
user1.email = "[email protected]";

// Save first user's changes
await ctx.saveChangesAsync();
console.log("First user saved");

// Update second user
user2.name = "Updated User 2";
user2.email = "[email protected]";

// Save second user's changes
await ctx.saveChangesAsync();
console.log("Second user saved");

// Both users are now updated and persisted

Update Type Safety ​

Schema Type Checking ​

Ensure type safety when updating entities:

ts
import { DataStore } from "@routier/datastore";
import { MemoryPlugin } from "@routier/memory-plugin";
import { s, InferType } from "@routier/core/schema";

// Define a user schema
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    name: s.string(),
    email: s.string().distinct(),
    age: s.number(),
    createdAt: s.date().default(() => new Date()),
}).compile();

// Create DataStore with collection
class AppContext extends DataStore {
    constructor() {
        super(new MemoryPlugin("app-db"));
    }

    users = this.collection(userSchema).proxy().create();
}

const ctx = new AppContext();

// Type-safe updates with TypeScript
type User = InferType<typeof userSchema>;

async function updateUser(userId: string, updates: Partial<User>) {
    const user = await ctx.users.firstOrUndefinedAsync(u => u.id === userId);

    if (!user) {
        throw new Error("User not found");
    }

    // TypeScript ensures type safety for property assignments
    if (updates.name !== undefined) {
        user.name = updates.name; // TypeScript ensures this is a string
    }

    if (updates.email !== undefined) {
        user.email = updates.email; // TypeScript ensures this is a string
    }

    if (updates.age !== undefined) {
        user.age = updates.age; // TypeScript ensures this is a number
    }

    await ctx.saveChangesAsync();
    return user;
}

// Usage with type safety
await updateUser("user-123", {
    name: "John Doe",
    age: 30
    // email: 123 // TypeScript error: Type 'number' is not assignable to type 'string'
});

Business Logic Type Checking ​

Implement business logic validation during updates:

ts
import { DataStore } from "@routier/datastore";
import { MemoryPlugin } from "@routier/memory-plugin";
import { s, InferType } from "@routier/core/schema";

// Define a user schema
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    name: s.string(),
    email: s.string().distinct(),
    age: s.number(),
    role: s.string("admin", "user", "guest").default("user"),
    createdAt: s.date().default(() => new Date()),
}).compile();

// Create DataStore with collection
class AppContext extends DataStore {
    constructor() {
        super(new MemoryPlugin("app-db"));
    }

    users = this.collection(userSchema).proxy().create();
}

const ctx = new AppContext();

// Business logic type checking
type User = InferType<typeof userSchema>;

async function promoteUser(userId: string) {
    const user = await ctx.users.firstOrUndefinedAsync(u => u.id === userId);

    if (!user) {
        throw new Error("User not found");
    }

    // Business logic validation
    if (user.age < 18) {
        throw new Error("Cannot promote users under 18");
    }

    if (user.role === "admin") {
        throw new Error("User is already an admin");
    }

    // Type-safe update
    user.role = "admin"; // TypeScript ensures this is a valid role

    await ctx.saveChangesAsync();
    return user;
}

// Usage with business logic
try {
    const promotedUser = await promoteUser("user-123");
    console.log("User promoted:", promotedUser);
} catch (error) {
    console.error("Promotion failed:", error.message);
}

Performance Considerations ​

Batch Updates ​

Optimize performance with batch update operations:

ts
import { DataStore } from "@routier/datastore";
import { MemoryPlugin } from "@routier/memory-plugin";
import { s } from "@routier/core/schema";

// Define a user schema
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    name: s.string(),
    email: s.string().distinct(),
    age: s.number(),
    isActive: s.boolean().default(true),
    createdAt: s.date().default(() => new Date()),
}).compile();

// Create DataStore with collection
class AppContext extends DataStore {
    constructor() {
        super(new MemoryPlugin("app-db"));
    }

    users = this.collection(userSchema).proxy().create();
}

const ctx = new AppContext();

// Batch updates for better performance
const users = await ctx.users.where(u => u.age >= 18).toArrayAsync();

// Update multiple entities efficiently
users.forEach(user => {
    user.isActive = true;
    user.name = user.name.toUpperCase();
});

console.log(`Updated ${users.length} users`);

// Save all changes in one operation
await ctx.saveChangesAsync();
console.log("All changes saved efficiently");

Change Batching ​

Manage change batching for optimal performance:

ts
import { DataStore } from "@routier/datastore";
import { MemoryPlugin } from "@routier/memory-plugin";
import { s } from "@routier/core/schema";

// Define a user schema
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    name: s.string(),
    email: s.string().distinct(),
    age: s.number(),
    createdAt: s.date().default(() => new Date()),
}).compile();

// Create DataStore with collection
class AppContext extends DataStore {
    constructor() {
        super(new MemoryPlugin("app-db"));
    }

    users = this.collection(userSchema).proxy().create();
}

const ctx = new AppContext();

// Change batching - group related changes together
const user1 = await ctx.users.firstAsync();
const user2 = await ctx.users.skip(1).firstAsync();

// Make changes to multiple entities
user1.name = "Updated User 1";
user1.email = "[email protected]";

user2.name = "Updated User 2";
user2.email = "[email protected]";

// All changes are batched and saved together
await ctx.saveChangesAsync();
console.log("All changes batched and saved");

// This is more efficient than:
// await ctx.saveChangesAsync(); // after user1 changes
// await ctx.saveChangesAsync(); // after user2 changes

Best Practices ​

1. Leverage Change Tracking ​

Take advantage of automatic change tracking:

ts
import { DataStore } from "@routier/datastore";
import { MemoryPlugin } from "@routier/memory-plugin";
import { s } from "@routier/core/schema";

// Define a user schema
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    name: s.string(),
    email: s.string().distinct(),
    age: s.number(),
    createdAt: s.date().default(() => new Date()),
}).compile();

// Create DataStore with collection
class AppContext extends DataStore {
    constructor() {
        super(new MemoryPlugin("app-db"));
    }

    users = this.collection(userSchema).proxy().create();
}

const ctx = new AppContext();

// Leverage change tracking - no manual update methods needed
const user = await ctx.users.firstAsync();

// ✅ Good - direct property modification
user.name = "Updated Name";
user.email = "[email protected]";
user.age = 30;

// ✅ Good - changes are automatically tracked
console.log("Changes tracked automatically");

// ✅ Good - save all changes at once
await ctx.saveChangesAsync();

// ❌ Avoid - manual update methods (don't exist in Routier)
// user.update({ name: "New Name" }); // This doesn't exist
// user.save(); // This doesn't exist

Update related fields in a single operation:

ts
import { DataStore } from "@routier/datastore";
import { MemoryPlugin } from "@routier/memory-plugin";
import { s } from "@routier/core/schema";

// Define a user schema
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    firstName: s.string(),
    lastName: s.string(),
    email: s.string().distinct(),
    age: s.number(),
    createdAt: s.date().default(() => new Date()),
}).compile();

// Create DataStore with collection
class AppContext extends DataStore {
    constructor() {
        super(new MemoryPlugin("app-db"));
    }

    users = this.collection(userSchema).proxy().create();
}

const ctx = new AppContext();

// Update related fields together
const user = await ctx.users.firstAsync();

// ✅ Good - update related fields together
user.firstName = "John";
user.lastName = "Doe";
user.email = "[email protected]";

// ✅ Good - all related changes are tracked together
console.log("Related fields updated together");

// Save all related changes
await ctx.saveChangesAsync();

// ❌ Avoid - updating fields separately and saving multiple times
// user.firstName = "John";
// await ctx.saveChangesAsync();
// user.lastName = "Doe";
// await ctx.saveChangesAsync();

3. Validate Before Updating ​

Implement validation before applying updates:

ts
import { DataStore } from "@routier/datastore";
import { MemoryPlugin } from "@routier/memory-plugin";
import { s, InferType } from "@routier/core/schema";

// Define a user schema
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    name: s.string(),
    email: s.string().distinct(),
    age: s.number(),
    role: s.string("admin", "user", "guest").default("user"),
    createdAt: s.date().default(() => new Date()),
}).compile();

// Create DataStore with collection
class AppContext extends DataStore {
    constructor() {
        super(new MemoryPlugin("app-db"));
    }

    users = this.collection(userSchema).proxy().create();
}

const ctx = new AppContext();

// Validate before updating
type User = InferType<typeof userSchema>;

async function updateUserSafely(userId: string, updates: Partial<User>) {
    const user = await ctx.users.firstOrUndefinedAsync(u => u.id === userId);

    if (!user) {
        throw new Error("User not found");
    }

    // Validate business rules before updating
    if (updates.age !== undefined && updates.age < 0) {
        throw new Error("Age cannot be negative");
    }

    if (updates.email !== undefined && !updates.email.includes("@")) {
        throw new Error("Invalid email format");
    }

    if (updates.role !== undefined && !["admin", "user", "guest"].includes(updates.role)) {
        throw new Error("Invalid role");
    }

    // Apply updates
    if (updates.name !== undefined) user.name = updates.name;
    if (updates.email !== undefined) user.email = updates.email;
    if (updates.age !== undefined) user.age = updates.age;
    if (updates.role !== undefined) user.role = updates.role;

    await ctx.saveChangesAsync();
    return user;
}

// Usage with validation
try {
    const updatedUser = await updateUserSafely("user-123", {
        name: "John Doe",
        age: 30,
        role: "admin"
    });
    console.log("User updated successfully:", updatedUser);
} catch (error) {
    console.error("Update failed:", error.message);
}

4. Use Meaningful Update Patterns ​

Follow consistent patterns for updates:

ts
import { DataStore } from "@routier/datastore";
import { MemoryPlugin } from "@routier/memory-plugin";
import { s } from "@routier/core/schema";

// Define a user schema
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    name: s.string(),
    email: s.string().distinct(),
    age: s.number(),
    score: s.number().default(0),
    level: s.number().default(1),
    createdAt: s.date().default(() => new Date()),
}).compile();

// Create DataStore with collection
class AppContext extends DataStore {
    constructor() {
        super(new MemoryPlugin("app-db"));
    }

    users = this.collection(userSchema).proxy().create();
}

const ctx = new AppContext();

// Use meaningful update patterns
const user = await ctx.users.firstAsync();

// ✅ Good - meaningful update patterns
function updateUserProfile(user: any, profileData: { name: string; email: string }) {
    user.name = profileData.name;
    user.email = profileData.email;
}

function incrementUserScore(user: any, points: number) {
    user.score += points;

    // Level up logic
    if (user.score >= 100) {
        user.level += 1;
        user.score = 0; // Reset score
    }
}

function activateUser(user: any) {
    user.isActive = true;
}

// Usage
updateUserProfile(user, { name: "John Doe", email: "[email protected]" });
incrementUserScore(user, 25);
activateUser(user);

// Save all changes
await ctx.saveChangesAsync();

// ❌ Avoid - meaningless direct assignments without context
// user.name = "John";
// user.email = "[email protected]";
// user.score = 100;

Error Handling ​

Update Error Handling ​

Handle errors gracefully during update operations:

ts
import { DataStore } from "@routier/datastore";
import { MemoryPlugin } from "@routier/memory-plugin";
import { s } from "@routier/core/schema";

// Define a user schema
const userSchema = s.define("users", {
    id: s.string().key().identity(),
    name: s.string(),
    email: s.string().distinct(),
    age: s.number(),
    createdAt: s.date().default(() => new Date()),
}).compile();

// Create DataStore with collection
class AppContext extends DataStore {
    constructor() {
        super(new MemoryPlugin("app-db"));
    }

    users = this.collection(userSchema).proxy().create();
}

const ctx = new AppContext();

// Update error handling
async function updateUserSafely(userId: string, updates: { name?: string; email?: string; age?: number }) {
    try {
        const user = await ctx.users.firstOrUndefinedAsync(u => u.id === userId);

        if (!user) {
            throw new Error(`User with ID ${userId} not found`);
        }

        // Apply updates
        if (updates.name !== undefined) user.name = updates.name;
        if (updates.email !== undefined) user.email = updates.email;
        if (updates.age !== undefined) user.age = updates.age;

        // Save changes
        await ctx.saveChangesAsync();

        console.log("User updated successfully:", user);
        return user;

    } catch (error) {
        console.error("Update failed:", error.message);

        // Handle different types of errors
        if (error.message.includes("not found")) {
            console.error("User not found - check user ID");
        } else if (error.message.includes("network")) {
            console.error("Network error - retry later");
        } else if (error.message.includes("database")) {
            console.error("Database error - check connection");
        } else {
            console.error("Unexpected error:", error);
        }

        throw error; // Re-throw for caller to handle
    }
}

// Usage with error handling
try {
    const updatedUser = await updateUserSafely("user-123", {
        name: "John Doe",
        email: "[email protected]",
        age: 30
    });
} catch (error) {
    console.error("Failed to update user:", error.message);
}

Common Update Patterns ​

User Profile Updates ​

ts
const user = await ctx.users.where((u) => u.id === userId).firstAsync();
if (user) {
  user.name = newData.name;
  user.email = newData.email;
  user.updatedAt = new Date();
  await ctx.saveChangesAsync();
}

Status Updates ​

ts
const orders = await ctx.orders
  .where((o) => o.status === "pending")
  .toArrayAsync();
orders.forEach((order) => {
  order.status = "processing";
  order.processedAt = new Date();
});
await ctx.saveChangesAsync();

Batch Price Updates ​

ts
const products = await ctx.products
  .where((p) => p.category === "electronics")
  .toArrayAsync();
products.forEach((product) => {
  product.price = product.price * 1.1; // 10% increase
});
await ctx.saveChangesAsync();

Next Steps ​

Released under the MIT License.