Skip to content

Complex mutations

Load an existing record, apply multiple transformations, and save it back.

Trigger: xrpc.procedure:<nsid>. Needs: happyview-db, happyview-record.

local time = require("internal.time")
local db = require("happyview.db")
local record = require("happyview.record")
function handle(input, ctx)
if not input.uri then
return { error = "UriRequired", message = "uri is required" }
end
local row = db.get(input.uri)
if not row then
return { error = "RecordNotFound", message = "no record at that uri" }
end
local r = row.record
-- Increment a counter
r.likeCount = (r.likeCount or 0) + 1
-- Merge tags, deduplicating and capping at 10
if input.tags and #input.tags > 0 then
r.tags = r.tags or {}
for _, tag in ipairs(input.tags) do
local found = false
for _, t in ipairs(r.tags) do
if t == tag then
found = true
break
end
end
if not found then
r.tags[#r.tags + 1] = tag
end
end
-- Keep only the last 10
while #r.tags > 10 do
table.remove(r.tags, 1)
end
end
-- Normalize a string field
if input.title then
r.title = string.gsub(input.title, "^%s+", "")
r.title = string.gsub(r.title, "%s+$", "")
end
-- Set a computed field
r.updatedAt = time.to_iso8601(time.now())
return record.put(input.uri, r)
end
  1. Load the existing record with db.get. The envelope’s record field is the stored body, which you can change in place. A missing uri or record returns a structured error. UriRequired and RecordNotFound must be declared under defs.main.errors in the lexicon, as in "errors": [{ "name": "RecordNotFound" }], for the response to be a 400; an undeclared one is sent as a 200.
  2. Apply transformations directly on the body’s fields:
    • Increment a counter: use or 0 (Lua) or ?? 0 (JavaScript and TypeScript) for the first time, when the field is absent.
    • Merge tags: add each of input.tags that isn’t already in r.tags, then trim the list to its last 10. In Lua, r.tags is created only when the request sends at least one tag, because an empty Lua table is written as a JSON object, not an array.
    • Normalize a string: trim whitespace with string.gsub (Lua) or trim (JavaScript and TypeScript).
    • Set a timestamp: time.to_iso8601(time.now()) for UTC ISO 8601.
  3. Write the body back with record.put, which calls putRecord to update the record on the user’s PDS. A URI in another account’s repo fails with WRITABLE_REPO.

The DPoP proof is signed per request; an app built on the HappyView SDK gets this from its session.

const response = await fetch("http://127.0.0.1:3000/xrpc/xyz.statusphere.updatePost", {
method: "POST",
headers: {
"X-Client-Key": CLIENT_KEY,
Authorization: `DPoP ${ACCESS_TOKEN}`,
DPoP: DPOP_PROOF,
"Content-Type": "application/json",
},
body: JSON.stringify({
uri: "at://did:plc:abc/xyz.statusphere.post/abc123",
tags: ["tutorial", "atproto"],
title: " My Post Title ",
}),
});
const data = await response.json();

Use this pattern when an update does more than replace fields: counters, bounded lists, string normalization or computed fields. All changes happen in memory before the single record.put call, so either all of them are written or none are.

The body is validated against the collection’s lexicon before the write; pass { validate = false } (Lua) or { validate: false } (JavaScript and TypeScript) as a third argument to skip that check.