- Guides
- Recipes
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)endimport time from "internal.time";import db from "happyview.db";import record from "happyview.record";
export default async function handle(input, ctx) { if (!input.uri) { return { error: "UriRequired", message: "uri is required" }; }
const row = await db.get(input.uri); if (!row) { return { error: "RecordNotFound", message: "no record at that uri" }; } const r = row.record;
// Increment a counter r.likeCount = (r.likeCount ?? 0) + 1;
// Merge tags, deduplicating and capping at 10 if (input.tags) { r.tags = [...new Set([...(r.tags ?? []), ...input.tags])].slice(-10); }
// Normalize a string field if (input.title) { r.title = input.title.trim(); }
// Set a computed field r.updatedAt = time.to_iso8601(time.now());
return record.put(input.uri, r);}import time from "internal.time";import db from "happyview.db";import record from "happyview.record";
interface Input { uri?: string; tags?: string[]; title?: string;}
export default async function handle(input: Input) { if (!input.uri) { return { error: "UriRequired", message: "uri is required" }; }
const row = await db.get(input.uri); if (!row) { return { error: "RecordNotFound", message: "no record at that uri" }; } const r = row.record;
// Increment a counter r.likeCount = (r.likeCount ?? 0) + 1;
// Merge tags, deduplicating and capping at 10 if (input.tags) { r.tags = [...new Set([...(r.tags ?? []), ...input.tags])].slice(-10); }
// Normalize a string field if (input.title) { r.title = input.title.trim(); }
// Set a computed field r.updatedAt = time.to_iso8601(time.now());
return record.put(input.uri, r);}How it works
Section titled “How it works”- Load the existing record with
db.get. The envelope’srecordfield is the stored body, which you can change in place. A missingurior record returns a structured error.UriRequiredandRecordNotFoundmust be declared underdefs.main.errorsin the lexicon, as in"errors": [{ "name": "RecordNotFound" }], for the response to be a 400; an undeclared one is sent as a 200. - 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.tagsthat isn’t already inr.tags, then trim the list to its last 10. In Lua,r.tagsis 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) ortrim(JavaScript and TypeScript). - Set a timestamp:
time.to_iso8601(time.now())for UTC ISO 8601.
- Increment a counter: use
- Write the body back with
record.put, which callsputRecordto update the record on the user’s PDS. A URI in another account’s repo fails withWRITABLE_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();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();let response = client .post("http://127.0.0.1:3000/xrpc/xyz.statusphere.updatePost") .header("X-Client-Key", client_key) .header("Authorization", format!("DPoP {access_token}")) .header("DPoP", dpop_proof) .json(&serde_json::json!({ "uri": "at://did:plc:abc/xyz.statusphere.post/abc123", "tags": ["tutorial", "atproto"], "title": " My Post Title " })) .send() .await?;body := `{ "uri": "at://did:plc:abc/xyz.statusphere.post/abc123", "tags": ["tutorial", "atproto"], "title": " My Post Title "}`req, _ := http.NewRequest("POST", "http://127.0.0.1:3000/xrpc/xyz.statusphere.updatePost", bytes.NewBufferString(body))req.Header.Set("X-Client-Key", clientKey)req.Header.Set("Authorization", "DPoP "+accessToken)req.Header.Set("DPoP", dpopProof)req.Header.Set("Content-Type", "application/json")resp, err := http.DefaultClient.Do(req)curl -X POST http://127.0.0.1:3000/xrpc/xyz.statusphere.updatePost \ -H "X-Client-Key: $CLIENT_KEY" \ -H "Authorization: DPoP $ACCESS_TOKEN" \ -H "DPoP: $DPOP_PROOF" \ -H "Content-Type: application/json" \ -d '{ "uri": "at://did:plc:abc/xyz.statusphere.post/abc123", "tags": ["tutorial", "atproto"], "title": " My Post Title " }'Use case
Section titled “Use case”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.
© HappyProto

