Webhooks for the AT Protocol airglow.run
atproto atprotocol automation webhook
12
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: improve data sources properties handling

Hugo 6e8d1fc0 be4de6e7

+105 -6
+29 -2
app/islands/AutomationForm.tsx
··· 891 891 892 892 const actionResultPlaceholders = actions.flatMap((a, i) => 893 893 isRecordProducingAction(a.type) 894 - ? [`action${i + 1}.uri`, `action${i + 1}.cid`, `action${i + 1}.rkey`] 894 + ? [ 895 + `action${i + 1}.uri`, 896 + `action${i + 1}.cid`, 897 + `action${i + 1}.did`, 898 + `action${i + 1}.collection`, 899 + `action${i + 1}.rkey`, 900 + ] 895 901 : [], 896 902 ); 897 903 ··· 900 906 ...fields.map((f) => `event.commit.record.${f.path}`), 901 907 ...fetches 902 908 .filter((f) => f.name) 903 - .flatMap((f) => [`${f.name}.uri`, `${f.name}.cid`, `${f.name}.record.*`]), 909 + .flatMap((f) => [ 910 + `${f.name}.uri`, 911 + `${f.name}.cid`, 912 + `${f.name}.did`, 913 + `${f.name}.collection`, 914 + `${f.name}.rkey`, 915 + `${f.name}.record.*`, 916 + ]), 904 917 ...actionResultPlaceholders, 905 918 ]; 906 919 ··· 1055 1068 .flatMap((f) => [ 1056 1069 <CopyPlaceholder key={`${f.name}.uri`} value={`${f.name}.uri`} />, 1057 1070 <CopyPlaceholder key={`${f.name}.cid`} value={`${f.name}.cid`} />, 1071 + <CopyPlaceholder key={`${f.name}.did`} value={`${f.name}.did`}> 1072 + <span class={s.placeholderDesc}>owner DID</span> 1073 + </CopyPlaceholder>, 1074 + <CopyPlaceholder 1075 + key={`${f.name}.collection`} 1076 + value={`${f.name}.collection`} 1077 + />, 1078 + <CopyPlaceholder key={`${f.name}.rkey`} value={`${f.name}.rkey`} />, 1058 1079 <CopyPlaceholder key={`${f.name}.record`} value={`${f.name}.record.*`}> 1059 1080 <span class={s.placeholderDesc}>access nested fields</span> 1060 1081 </CopyPlaceholder>, ··· 1076 1097 </CopyPlaceholder> 1077 1098 <CopyPlaceholder value={`action${i + 1}.cid`}> 1078 1099 <span class={s.placeholderDesc}>Content hash from Action {i + 1}</span> 1100 + </CopyPlaceholder> 1101 + <CopyPlaceholder value={`action${i + 1}.did`}> 1102 + <span class={s.placeholderDesc}>Owner DID from Action {i + 1}</span> 1103 + </CopyPlaceholder> 1104 + <CopyPlaceholder value={`action${i + 1}.collection`}> 1105 + <span class={s.placeholderDesc}>Collection from Action {i + 1}</span> 1079 1106 </CopyPlaceholder> 1080 1107 <CopyPlaceholder value={`action${i + 1}.rkey`}> 1081 1108 <span class={s.placeholderDesc}>Record key from Action {i + 1}</span>
+18
lib/actions/fetcher.test.ts
··· 21 21 mockFetchRecord.mockResolvedValueOnce({ 22 22 uri: "at://did:plc:eventuser/app.bsky.actor.profile/self", 23 23 cid: "bafycid", 24 + did: "did:plc:eventuser", 25 + collection: "app.bsky.actor.profile", 26 + rkey: "self", 24 27 record: { displayName: "Alice" }, 25 28 }); 26 29 ··· 33 36 expect(result.context.profile).toEqual({ 34 37 uri: "at://did:plc:eventuser/app.bsky.actor.profile/self", 35 38 cid: "bafycid", 39 + did: "did:plc:eventuser", 40 + collection: "app.bsky.actor.profile", 41 + rkey: "self", 36 42 record: { displayName: "Alice" }, 37 43 }); 38 44 expect(result.errors).toEqual([]); ··· 46 52 .mockResolvedValueOnce({ 47 53 uri: "at://did1/col/rk1", 48 54 cid: "c1", 55 + did: "did1", 56 + collection: "col", 57 + rkey: "rk1", 49 58 record: { a: 1 }, 50 59 }) 51 60 .mockResolvedValueOnce({ 52 61 uri: "at://did2/col/rk2", 53 62 cid: "c2", 63 + did: "did2", 64 + collection: "col", 65 + rkey: "rk2", 54 66 record: { b: 2 }, 55 67 }); 56 68 ··· 73 85 mockFetchRecord.mockResolvedValueOnce({ 74 86 uri: "at://did:plc:owner/col/rk", 75 87 cid: "c", 88 + did: "did:plc:owner", 89 + collection: "col", 90 + rkey: "rk", 76 91 record: {}, 77 92 }); 78 93 ··· 115 130 mockFetchRecord.mockRejectedValueOnce(new Error("PDS unreachable")).mockResolvedValueOnce({ 116 131 uri: "at://ok/col/rk", 117 132 cid: "c", 133 + did: "ok", 134 + collection: "col", 135 + rkey: "rk", 118 136 record: { ok: true }, 119 137 }); 120 138
+23
lib/actions/template.test.ts
··· 233 233 expect(result).toEqual({ name: "Alice" }); 234 234 }); 235 235 236 + it("exposes did/collection/rkey from fetch context", async () => { 237 + const fetchContext = { 238 + repo: { 239 + uri: "at://did:plc:owner/sh.tangled.repo/abc123", 240 + cid: "bafycid", 241 + did: "did:plc:owner", 242 + collection: "sh.tangled.repo", 243 + rkey: "abc123", 244 + record: { name: "airglow" }, 245 + }, 246 + }; 247 + const result = await renderTemplate( 248 + '{"owner":"{{repo.did}}","col":"{{repo.collection}}","key":"{{repo.rkey}}"}', 249 + event, 250 + fetchContext, 251 + ); 252 + expect(result).toEqual({ 253 + owner: "did:plc:owner", 254 + col: "sh.tangled.repo", 255 + key: "abc123", 256 + }); 257 + }); 258 + 236 259 it("resolves undefined placeholders to empty string", async () => { 237 260 const result = await renderTemplate('{"val":"{{event.commit.record.missing}}"}', event); 238 261 expect(result).toEqual({ val: "" });
+8 -1
lib/actions/template.ts
··· 7 7 8 8 export type FetchContext = Record< 9 9 string, 10 - { uri: string; cid: string; rkey?: string; record: Record<string, unknown> } 10 + { 11 + uri: string; 12 + cid: string; 13 + did?: string; 14 + collection?: string; 15 + rkey?: string; 16 + record: Record<string, unknown>; 17 + } 11 18 >; 12 19 13 20 /**
+2
lib/jetstream/handler.test.ts
··· 210 210 action1: { 211 211 uri: okWithUri.uri, 212 212 cid: okWithUri.cid, 213 + did: "did:plc:test", 214 + collection: "app.bsky.feed.post", 213 215 rkey: "abc123", 214 216 record: {}, 215 217 },
+13 -3
lib/jetstream/handler.ts
··· 6 6 import { executePatchRecord } from "../actions/patch-record.js"; 7 7 import { resolveFetches } from "../actions/fetcher.js"; 8 8 import { renderTemplate, renderTextTemplate, type FetchContext } from "../actions/template.js"; 9 + import { parseAtUri } from "../pds/resolver.js"; 9 10 import type { MatchedEvent } from "./consumer.js"; 10 11 11 12 /** Handle a matched Jetstream event: resolve fetches, then dispatch all actions. */ ··· 34 35 // Inject synthetic action result so subsequent dry-run actions can reference {{actionN.*}} 35 36 if (isRecordProducingAction(action.$type)) { 36 37 fetchContext[`action${i + 1}`] = { 37 - uri: `at://dry-run/${action.$type}/placeholder`, 38 + uri: `at://did:dry:run/${action.$type}/placeholder`, 38 39 cid: "dry-run-cid", 40 + did: "did:dry:run", 41 + collection: action.$type, 39 42 rkey: "placeholder", 40 43 record: {}, 41 44 }; ··· 60 63 61 64 // Accumulate result into fetchContext for downstream actions 62 65 if (result.uri && result.cid) { 63 - const rkey = result.uri.split("/").pop() ?? ""; 64 - fetchContext[`action${i + 1}`] = { uri: result.uri, cid: result.cid, rkey, record: {} }; 66 + const { did, collection, rkey } = parseAtUri(result.uri); 67 + fetchContext[`action${i + 1}`] = { 68 + uri: result.uri, 69 + cid: result.cid, 70 + did, 71 + collection, 72 + rkey, 73 + record: {}, 74 + }; 65 75 } 66 76 67 77 // Fail-fast: stop chain on error
+6
lib/pds/resolver.test.ts
··· 144 144 expect(result).toEqual({ 145 145 uri: "at://did:plc:abc/app.bsky.feed.post/rk1", 146 146 cid: "bafycid123", 147 + did: "did:plc:abc", 148 + collection: "app.bsky.feed.post", 149 + rkey: "rk1", 147 150 record: { text: "hello" }, 148 151 }); 149 152 ··· 204 207 const result = await fetchRecord("at://did:plc:abc/col/rk"); 205 208 expect(result.uri).toBe("at://did:plc:abc/col/rk"); 206 209 expect(result.cid).toBe(""); 210 + expect(result.did).toBe("did:plc:abc"); 211 + expect(result.collection).toBe("col"); 212 + expect(result.rkey).toBe("rk"); 207 213 expect(result.record).toEqual({}); 208 214 }); 209 215 });
+6
lib/pds/resolver.ts
··· 19 19 export type FetchedRecord = { 20 20 uri: string; 21 21 cid: string; 22 + did: string; 23 + collection: string; 24 + rkey: string; 22 25 record: Record<string, unknown>; 23 26 }; 24 27 ··· 103 106 return { 104 107 uri: data.uri ?? atUri, 105 108 cid: data.cid ?? "", 109 + did, 110 + collection, 111 + rkey, 106 112 record: data.value ?? {}, 107 113 }; 108 114 }