bknd-crud-update

Use when updating existing records in a Bknd entity via the SDK or REST API. Covers updateOne, updateMany, updating relations ($set, $add, $remove, $unset), partial updates, conditional updates, response handling, and common patterns.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "bknd-crud-update" with this command: npx skills add cameronapak/bknd-skills/cameronapak-bknd-skills-bknd-crud-update

CRUD Update

Update existing records in your Bknd database using the SDK or REST API.

Prerequisites

  • Bknd project running (local or deployed)
  • Entity exists with records to update
  • SDK configured or API endpoint known
  • Record ID or filter criteria known

When to Use UI Mode

  • Quick one-off edits
  • Manual data corrections
  • Visual verification during development

UI steps: Admin Panel > Data > Select Entity > Click record > Edit fields > Save

When to Use Code Mode

  • Application logic for user edits
  • Form submissions
  • Bulk updates
  • Automated data maintenance

Code Approach

Step 1: Set Up SDK Client

import { Api } from "bknd";

const api = new Api({
  host: "http://localhost:7654",
});

// If auth required:
api.updateToken("your-jwt-token");

Step 2: Update Single Record

Use updateOne(entity, id, data):

const { ok, data, error } = await api.data.updateOne("posts", 1, {
  title: "Updated Title",
  status: "published",
});

if (ok) {
  console.log("Updated post:", data.id);
} else {
  console.error("Failed:", error.message);
}

Step 3: Handle Response

The response object:

type UpdateResponse = {
  ok: boolean;       // Success/failure
  data?: {           // Updated record (if ok)
    id: number;
    // ...all fields with new values
  };
  error?: {          // Error info (if !ok)
    message: string;
    code: string;
  };
};

Step 4: Partial Updates

Only changed fields are required - other fields remain unchanged:

// Only update title, keep everything else
await api.data.updateOne("posts", 1, {
  title: "New Title Only",
});

// Update multiple fields
await api.data.updateOne("users", 5, {
  name: "New Name",
  bio: "Updated bio",
  updated_at: new Date().toISOString(),
});

Step 5: Update Relations

Change Linked Record (Many-to-One)

// Change post author to user ID 2
await api.data.updateOne("posts", 1, {
  author: { $set: 2 },
});

Unlink Record (Set to NULL)

// Remove author link
await api.data.updateOne("posts", 1, {
  author: { $unset: true },
});

Many-to-Many: Add Relations

// Add tags 4 and 5 to existing tags
await api.data.updateOne("posts", 1, {
  tags: { $add: [4, 5] },
});

Many-to-Many: Remove Relations

// Remove tag 2 from post
await api.data.updateOne("posts", 1, {
  tags: { $remove: [2] },
});

Many-to-Many: Replace All Relations

// Replace all tags with new set
await api.data.updateOne("posts", 1, {
  tags: { $set: [1, 3, 5] },
});

Combined Field and Relation Update

await api.data.updateOne("posts", 1, {
  title: "Updated Post",
  status: "published",
  author: { $set: newAuthorId },
  tags: { $add: [newTagId] },
});

Step 6: Update Multiple Records (Bulk)

Use updateMany(entity, where, data):

// Archive all draft posts
const { ok, data } = await api.data.updateMany(
  "posts",
  { status: { $eq: "draft" } },     // where clause (required)
  { status: "archived" },            // update values
);

// data contains affected records
console.log("Archived", data.length, "posts");

Important: where clause is required to prevent accidental update-all.

// Update posts by author
await api.data.updateMany(
  "posts",
  { author_id: { $eq: userId } },
  { author_id: newUserId },
);

// Update old records
await api.data.updateMany(
  "sessions",
  { last_active: { $lt: "2024-01-01" } },
  { expired: true },
);

REST API Approach

Update One

curl -X PATCH http://localhost:7654/api/data/posts/1 \
  -H "Content-Type: application/json" \
  -d '{"title": "Updated Title", "status": "published"}'

Update with Auth

curl -X PATCH http://localhost:7654/api/data/posts/1 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -d '{"title": "Protected Update"}'

Update Relations

# Change author
curl -X PATCH http://localhost:7654/api/data/posts/1 \
  -H "Content-Type: application/json" \
  -d '{"author": {"$set": 2}}'

# Add tags
curl -X PATCH http://localhost:7654/api/data/posts/1 \
  -H "Content-Type: application/json" \
  -d '{"tags": {"$add": [4, 5]}}'

Update Many

curl -X PATCH "http://localhost:7654/api/data/posts?where=%7B%22status%22%3A%22draft%22%7D" \
  -H "Content-Type: application/json" \
  -d '{"status": "archived"}'

React Integration

Edit Form

import { useApp } from "bknd/react";
import { useState, useEffect } from "react";

function EditPostForm({ postId }: { postId: number }) {
  const { api } = useApp();
  const [title, setTitle] = useState("");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // Load existing data
  useEffect(() => {
    api.data.readOne("posts", postId).then(({ data }) => {
      if (data) setTitle(data.title);
    });
  }, [postId]);

  async function handleSubmit(e: React.FormEvent) {
    e.preventDefault();
    setLoading(true);
    setError(null);

    const { ok, error: apiError } = await api.data.updateOne("posts", postId, {
      title,
    });

    setLoading(false);

    if (!ok) {
      setError(apiError.message);
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="Post title"
      />
      <button type="submit" disabled={loading}>
        {loading ? "Saving..." : "Save"}
      </button>
      {error && <p className="error">{error}</p>}
    </form>
  );
}

With SWR Revalidation

import useSWR, { mutate } from "swr";

function useUpdatePost() {
  const { api } = useApp();

  async function updatePost(id: number, updates: object) {
    const { ok, data, error } = await api.data.updateOne("posts", id, updates);

    if (ok) {
      // Revalidate the post and list
      mutate(`posts/${id}`);
      mutate("posts");
    }

    return { ok, data, error };
  }

  return { updatePost };
}

Optimistic Update

function useOptimisticUpdate() {
  const { api } = useApp();
  const [posts, setPosts] = useState<Post[]>([]);

  async function updatePost(id: number, updates: Partial<Post>) {
    // Optimistic: update immediately
    const originalPosts = [...posts];
    setPosts((prev) =>
      prev.map((p) => (p.id === id ? { ...p, ...updates } : p))
    );

    // Actual update
    const { ok } = await api.data.updateOne("posts", id, updates);

    if (!ok) {
      // Rollback on failure
      setPosts(originalPosts);
    }

    return { ok };
  }

  return { posts, updatePost };
}

Full Example

import { Api } from "bknd";

const api = new Api({ host: "http://localhost:7654" });

// Authenticate
await api.auth.login({ email: "user@example.com", password: "password" });

// Simple field update
const { ok, data } = await api.data.updateOne("posts", 1, {
  title: "Updated Title",
  updated_at: new Date().toISOString(),
});

// Update with relation changes
await api.data.updateOne("posts", 1, {
  status: "published",
  published_at: new Date().toISOString(),
  category: { $set: 3 },        // Change category
  tags: { $add: [7, 8] },       // Add new tags
});

// Bulk update: mark old drafts as archived
await api.data.updateMany(
  "posts",
  {
    status: { $eq: "draft" },
    created_at: { $lt: "2024-01-01" },
  },
  { status: "archived" }
);

// Toggle boolean field
const post = await api.data.readOne("posts", 1);
if (post.ok) {
  await api.data.updateOne("posts", 1, {
    featured: !post.data.featured,
  });
}

Common Patterns

Upsert (Update or Insert)

async function upsert(
  api: Api,
  entity: string,
  where: object,
  data: object
) {
  const { data: existing } = await api.data.readOneBy(entity, { where });

  if (existing) {
    return api.data.updateOne(entity, existing.id, data);
  }

  return api.data.createOne(entity, data);
}

// Usage
await upsert(
  api,
  "settings",
  { key: { $eq: "theme" } },
  { key: "theme", value: "dark" }
);

Conditional Update

async function updateIf(
  api: Api,
  entity: string,
  id: number,
  condition: (record: any) => boolean,
  updates: object
) {
  const { data: current } = await api.data.readOne(entity, id);

  if (!current || !condition(current)) {
    return { ok: false, error: { message: "Condition not met" } };
  }

  return api.data.updateOne(entity, id, updates);
}

// Only update if not published
await updateIf(
  api,
  "posts",
  1,
  (post) => post.status !== "published",
  { title: "New Title" }
);

Increment/Decrement Field

async function increment(
  api: Api,
  entity: string,
  id: number,
  field: string,
  amount: number = 1
) {
  const { data: current } = await api.data.readOne(entity, id);
  if (!current) return { ok: false };

  return api.data.updateOne(entity, id, {
    [field]: current[field] + amount,
  });
}

// Increment view count
await increment(api, "posts", 1, "view_count");

// Decrement stock
await increment(api, "products", 5, "stock", -1);

Soft Delete

async function softDelete(api: Api, entity: string, id: number) {
  return api.data.updateOne(entity, id, {
    deleted_at: new Date().toISOString(),
  });
}

async function restore(api: Api, entity: string, id: number) {
  return api.data.updateOne(entity, id, {
    deleted_at: null,
  });
}

Batch Update with Progress

async function batchUpdate(
  api: Api,
  entity: string,
  updates: Array<{ id: number; data: object }>,
  onProgress?: (done: number, total: number) => void
) {
  const results = [];

  for (let i = 0; i < updates.length; i++) {
    const { id, data } = updates[i];
    const result = await api.data.updateOne(entity, id, data);
    results.push(result);
    onProgress?.(i + 1, updates.length);
  }

  return results;
}

// Usage
await batchUpdate(
  api,
  "products",
  [
    { id: 1, data: { price: 19.99 } },
    { id: 2, data: { price: 29.99 } },
    { id: 3, data: { price: 39.99 } },
  ],
  (done, total) => console.log(`${done}/${total}`)
);

Common Pitfalls

Record Not Found

Problem: Update returns no data or error.

Fix: Verify record exists first:

const { data: existing } = await api.data.readOne("posts", id);
if (!existing) {
  throw new Error("Post not found");
}
await api.data.updateOne("posts", id, updates);

Invalid Relation ID

Problem: FOREIGN KEY constraint failed

Fix: Verify related record exists:

// Wrong - author ID doesn't exist
await api.data.updateOne("posts", 1, { author: { $set: 999 } });

// Correct - verify first
const { data: author } = await api.data.readOne("users", newAuthorId);
if (author) {
  await api.data.updateOne("posts", 1, { author: { $set: newAuthorId } });
}

Unique Constraint Violation

Problem: UNIQUE constraint failed when updating to existing value.

Fix: Check uniqueness before update:

// Check if email already taken by another user
const { data: existing } = await api.data.readOneBy("users", {
  where: {
    email: { $eq: newEmail },
    id: { $ne: currentUserId },  // Exclude current user
  },
});

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

await api.data.updateOne("users", currentUserId, { email: newEmail });

Not Checking Response

Problem: Assuming success without verification.

Fix: Always check ok:

// Wrong
const { data } = await api.data.updateOne("posts", 1, updates);
console.log(data.title);  // data might be undefined!

// Correct
const { ok, data, error } = await api.data.updateOne("posts", 1, updates);
if (!ok) {
  throw new Error(error.message);
}
console.log(data.title);

Updating Without Auth

Problem: Unauthorized error.

Fix: Authenticate first:

await api.auth.login({ email, password });
// or
api.updateToken(savedToken);

await api.data.updateOne("posts", 1, updates);

Using Wrong Relation Operator

Problem: Replacing when intending to add.

Fix: Use correct operator:

// $set replaces ALL relations
await api.data.updateOne("posts", 1, { tags: { $set: [5] } });
// Post now has ONLY tag 5

// $add keeps existing and adds new
await api.data.updateOne("posts", 1, { tags: { $add: [5] } });
// Post keeps existing tags AND adds tag 5

Forgetting updateMany Where Clause

Problem: Trying to update all without where.

Fix: Always provide where clause:

// updateMany requires where clause
await api.data.updateMany(
  "posts",
  { status: { $eq: "draft" } },  // Required
  { status: "archived" }
);

// To update ALL records, use explicit condition
await api.data.updateMany(
  "posts",
  { id: { $gt: 0 } },  // Match all
  { reviewed: true }
);

Verification

After updating, verify changes:

const { ok } = await api.data.updateOne("posts", 1, { title: "New Title" });

if (ok) {
  const { data } = await api.data.readOne("posts", 1);
  console.log("Updated title:", data.title);
}

Or via admin panel: Admin Panel > Data > Select Entity > Find record > Verify fields.

DOs and DON'Ts

DO:

  • Check ok before using response data
  • Verify record exists before updating
  • Use $add/$remove for incremental relation changes
  • Handle unique constraint errors
  • Authenticate before updating protected records
  • Revalidate caches after updates

DON'T:

  • Assume updateOne always succeeds
  • Use $set for relations when meaning $add
  • Update without where clause in updateMany
  • Ignore validation errors
  • Forget to refresh UI after updates
  • Use non-existent IDs in relation operators

Related Skills

  • bknd-crud-create - Create records before updating
  • bknd-crud-read - Fetch records to get current values
  • bknd-crud-delete - Remove records instead of updating
  • bknd-define-relationship - Set up relations for linking
  • bknd-bulk-operations - Large-scale update patterns

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

btca-bknd-repo-learn

No summary provided by upstream source.

Repository SourceNeeds Review
General

bknd-registration

No summary provided by upstream source.

Repository SourceNeeds Review
General

bknd-bulk-operations

No summary provided by upstream source.

Repository SourceNeeds Review
General

bknd-login-flow

No summary provided by upstream source.

Repository SourceNeeds Review
bknd-crud-update | V50.AI