Files
shiftcraft/.claude/skills/pocketbase-best-practices/rules/sdk-field-modifiers.md
2026-04-17 23:26:01 +00:00

3.7 KiB

title, impact, impactDescription, tags
title impact impactDescription tags
Use Field Modifiers for Incremental Updates HIGH Atomic updates, prevents race conditions, cleaner code sdk, modifiers, relations, files, numbers, atomic

Use Field Modifiers for Incremental Updates

PocketBase supports + and - modifiers for incrementing numbers, appending/removing relation IDs, and managing file arrays without replacing the entire value.

Incorrect (read-modify-write pattern):

// Race condition: two users adding tags simultaneously
async function addTag(postId, newTagId) {
  const post = await pb.collection('posts').getOne(postId);
  const currentTags = post.tags || [];

  // Another user might have added a tag in between!
  await pb.collection('posts').update(postId, {
    tags: [...currentTags, newTagId]  // Might overwrite the other user's tag
  });
}

// Inefficient for incrementing counters
async function incrementViews(postId) {
  const post = await pb.collection('posts').getOne(postId);
  await pb.collection('posts').update(postId, {
    views: post.views + 1  // Extra read, race condition
  });
}

Correct (using field modifiers):

// Atomic relation append with + modifier
async function addTag(postId, newTagId) {
  await pb.collection('posts').update(postId, {
    'tags+': newTagId  // Appends to existing tags atomically
  });
}

// Append multiple relations
async function addTags(postId, tagIds) {
  await pb.collection('posts').update(postId, {
    'tags+': tagIds  // Appends array of IDs
  });
}

// Prepend relations (+ prefix)
async function prependTag(postId, tagId) {
  await pb.collection('posts').update(postId, {
    '+tags': tagId  // Prepends to start of array
  });
}

// Remove relations with - modifier
async function removeTag(postId, tagId) {
  await pb.collection('posts').update(postId, {
    'tags-': tagId  // Removes specific tag
  });
}

// Remove multiple relations
async function removeTags(postId, tagIds) {
  await pb.collection('posts').update(postId, {
    'tags-': tagIds  // Removes all specified tags
  });
}

// Atomic number increment
async function incrementViews(postId) {
  await pb.collection('posts').update(postId, {
    'views+': 1  // Atomic increment, no race condition
  });
}

// Atomic number decrement
async function decrementStock(productId, quantity) {
  await pb.collection('products').update(productId, {
    'stock-': quantity  // Atomic decrement
  });
}

// File append (for multi-file fields)
async function addImage(albumId, newImage) {
  await pb.collection('albums').update(albumId, {
    'images+': newImage  // Appends new file to existing
  });
}

// File removal
async function removeImage(albumId, filename) {
  await pb.collection('albums').update(albumId, {
    'images-': filename  // Removes specific file by name
  });
}

// Combined modifiers in single update
async function updatePost(postId, data) {
  await pb.collection('posts').update(postId, {
    title: data.title,        // Replace field
    'views+': 1,              // Increment number
    'tags+': data.newTagId,   // Append relation
    'tags-': data.oldTagId,   // Remove relation
    'images+': data.newImage  // Append file
  });
}

Modifier reference:

Modifier Field Types Description
field+ or +field relation, file Append/prepend to array
field- relation, file Remove from array
field+ number Increment by value
field- number Decrement by value

Benefits:

  • Atomic: No read-modify-write race conditions
  • Efficient: Single request, no extra read needed
  • Clean: Expresses intent clearly

Note: Modifiers only work with update(), not create().

Reference: PocketBase Relations