[READ-ONLY] Mirror of https://github.com/andrioid/n8n-nodes-atproto. atproto node for n8n
0

Configure Feed

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

UX: collection picker, subtitle, options collection, blob hints

- Collection field → resourceLocator with searchable list (queries
the user's PDS via describeRepo) + free-text "By NSID" mode with
NSID format validation.

- Add subtitle showing the current operation on the canvas.

- Move Swap Commit into an Options collection so it doesn't clutter
the main view for the common case.

- Blob fields now show a hint in the displayName explaining that
the value should be a binary property name from the input.

+123 -24
+116 -22
src/nodes/Atproto/Atproto.node.ts
··· 3 3 IExecuteFunctions, 4 4 ILoadOptionsFunctions, 5 5 INodeExecutionData, 6 + INodeListSearchResult, 6 7 INodeType, 7 8 INodeTypeDescription, 8 9 ResourceMapperFields, ··· 84 85 } 85 86 86 87 // --------------------------------------------------------------------------- 88 + // Helpers 89 + // --------------------------------------------------------------------------- 90 + 91 + /** 92 + * Extract the NSID string from a collection parameter. 93 + * 94 + * Handles both the plain string (legacy / expressions) and the 95 + * resourceLocator object `{ mode, value }` returned by the RLC widget. 96 + */ 97 + function extractCollectionNsid(param: unknown): string { 98 + if (typeof param === 'string') return param; 99 + if (param && typeof param === 'object' && 'value' in param) { 100 + return String((param as { value: unknown }).value ?? ''); 101 + } 102 + return ''; 103 + } 104 + 105 + // --------------------------------------------------------------------------- 87 106 // Node 88 107 // --------------------------------------------------------------------------- 89 108 ··· 94 113 icon: 'file:atproto.svg', 95 114 group: ['transform'], 96 115 version: 1, 116 + subtitle: '={{ $parameter["operation"] }}', 97 117 description: 'CRUD records in any AT Protocol collection', 98 118 defaults: { 99 119 name: 'AT Protocol', ··· 151 171 }, 152 172 153 173 // ------------------------------------------------------------------ 154 - // Collection NSID 174 + // Collection (resourceLocator — searchable list + free text) 155 175 // ------------------------------------------------------------------ 156 176 { 157 - displayName: 'Collection (NSID)', 177 + displayName: 'Collection', 158 178 name: 'collection', 159 - type: 'string', 179 + type: 'resourceLocator', 160 180 required: true, 161 - placeholder: 'app.bsky.feed.post', 162 - description: 'The NSID of the record collection (e.g. app.bsky.feed.post)', 181 + description: 'The record collection to operate on', 182 + default: { mode: 'list', value: '' }, 163 183 displayOptions: { 164 184 show: { 165 185 operation: [ ··· 171 191 ], 172 192 }, 173 193 }, 174 - default: '', 194 + modes: [ 195 + { 196 + displayName: 'From List', 197 + name: 'list', 198 + type: 'list', 199 + placeholder: 'Select a collection…', 200 + typeOptions: { 201 + searchListMethod: 'searchCollections', 202 + searchable: true, 203 + }, 204 + }, 205 + { 206 + displayName: 'By NSID', 207 + name: 'nsid', 208 + type: 'string', 209 + placeholder: 'e.g. app.bsky.feed.post', 210 + validation: [ 211 + { 212 + type: 'regex', 213 + properties: { 214 + regex: '^[a-z][a-z0-9]*(\\.[a-zA-Z][a-zA-Z0-9]*){2,}$', 215 + errorMessage: 'Must be a valid NSID (e.g. app.bsky.feed.post)', 216 + }, 217 + }, 218 + ], 219 + }, 220 + ], 175 221 }, 176 222 177 223 // ------------------------------------------------------------------ ··· 278 324 noFieldsError: 279 325 'Could not resolve lexicon for this NSID. Enter record data as JSON using an expression, or check the collection NSID.', 280 326 }, 281 - loadOptionsDependsOn: ['collection'], 327 + loadOptionsDependsOn: ['collection.value'], 282 328 }, 283 329 displayOptions: { 284 330 show: { ··· 288 334 }, 289 335 290 336 // ------------------------------------------------------------------ 291 - // Swap Commit (Put only — optional) 337 + // Options (advanced / less-common fields) 292 338 // ------------------------------------------------------------------ 293 339 { 294 - displayName: 'Swap Commit (CID)', 295 - name: 'swapCommit', 296 - type: 'string', 297 - required: false, 298 - placeholder: 'bafyreia...', 299 - description: 300 - 'Optional. Compare-and-swap with the current commit CID. The put is rejected if the current record CID does not match.', 340 + displayName: 'Options', 341 + name: 'options', 342 + type: 'collection', 343 + placeholder: 'Add Option', 344 + default: {}, 301 345 displayOptions: { 302 346 show: { 303 - operation: ['putRecord', 'deleteRecord', 'createRecord'], 347 + operation: ['createRecord', 'putRecord', 'deleteRecord'], 304 348 }, 305 349 }, 306 - default: '', 350 + options: [ 351 + { 352 + displayName: 'Swap Commit (CID)', 353 + name: 'swapCommit', 354 + type: 'string', 355 + default: '', 356 + placeholder: 'bafyreia...', 357 + description: 358 + 'Compare-and-swap with the current commit CID. The write is rejected if the repo head does not match.', 359 + }, 360 + ], 307 361 }, 308 362 309 363 // ------------------------------------------------------------------ ··· 351 405 // Resource mapper method — called in the n8n editor to resolve fields 352 406 // ----------------------------------------------------------------------- 353 407 methods = { 408 + listSearch: { 409 + searchCollections: async function ( 410 + this: ILoadOptionsFunctions, 411 + filter?: string, 412 + ): Promise<INodeListSearchResult> { 413 + try { 414 + const credentials = await this.getCredentials('atprotoApi'); 415 + const agent = await createAgent(credentials as IDataObject); 416 + 417 + const response = await agent.com.atproto.repo.describeRepo({ 418 + repo: agent.did!, 419 + }); 420 + 421 + let collections = (response.data as { collections?: string[] }) 422 + .collections ?? []; 423 + 424 + if (filter) { 425 + const q = filter.toLowerCase(); 426 + collections = collections.filter((c) => 427 + c.toLowerCase().includes(q), 428 + ); 429 + } 430 + 431 + return { 432 + results: collections 433 + .sort() 434 + .map((nsid) => ({ name: nsid, value: nsid })), 435 + }; 436 + } catch { 437 + return { results: [] }; 438 + } 439 + }, 440 + }, 354 441 resourceMapping: { 355 442 getRecordFields: async function ( 356 443 this: ILoadOptionsFunctions, 357 444 ): Promise<ResourceMapperFields> { 358 - const nsid = this.getNodeParameter('collection') as string; 445 + const nsid = extractCollectionNsid( 446 + this.getNodeParameter('collection'), 447 + ); 359 448 360 449 if (!nsid) { 361 450 return { fields: [] }; ··· 403 492 for (let i = 0; i < items.length; i++) { 404 493 try { 405 494 const operation = this.getNodeParameter('operation', i) as string; 406 - const collection = this.getNodeParameter('collection', i) as string; 495 + const collection = extractCollectionNsid( 496 + this.getNodeParameter('collection', i), 497 + ); 407 498 408 499 let result: IDataObject; 409 500 ··· 416 507 : generateTid(); 417 508 const recordData = this.getNodeParameter('recordData', i); 418 509 const record = buildRecordFromNodeParams(recordData); 419 - const swapCommit = this.getNodeParameter('swapCommit', i) as string; 510 + const opts = this.getNodeParameter('options', i, {}) as IDataObject; 511 + const swapCommit = (opts.swapCommit as string) ?? ''; 420 512 421 513 // Phase 3: upload blobs referenced by binary property names 422 514 const schema = await resolveLexiconSchema(agent, collection); ··· 462 554 const rkey = this.getNodeParameter('rkey', i) as string; 463 555 const recordData = this.getNodeParameter('recordData', i); 464 556 const record = buildRecordFromNodeParams(recordData); 465 - const swapCommit = this.getNodeParameter('swapCommit', i) as string; 557 + const putOpts = this.getNodeParameter('options', i, {}) as IDataObject; 558 + const swapCommit = (putOpts.swapCommit as string) ?? ''; 466 559 467 560 // Phase 3: upload blobs referenced by binary property names 468 561 const schema = await resolveLexiconSchema(agent, collection); ··· 493 586 494 587 case 'deleteRecord': { 495 588 const rkey = this.getNodeParameter('rkey', i) as string; 496 - const swapCommit = this.getNodeParameter('swapCommit', i) as string; 589 + const delOpts = this.getNodeParameter('options', i, {}) as IDataObject; 590 + const swapCommit = (delOpts.swapCommit as string) ?? ''; 497 591 498 592 const res = await deleteRecord(agent, { 499 593 collection,
+7 -2
src/nodes/Atproto/fieldMapping.ts
··· 286 286 type: fieldType as ResourceMapperField['type'], 287 287 }; 288 288 289 - // Attach optional description 290 - if (prop.description) { 289 + // Attach optional description (with blob hint) 290 + if (prop.type === 'blob') { 291 + const desc = prop.description?.replace(/\.\s*$/, ''); 292 + field.displayName = desc 293 + ? `${name} (${desc} — binary property name from input)` 294 + : `${name} (binary property name from input)`; 295 + } else if (prop.description) { 291 296 field.displayName = `${name} (${prop.description})`; 292 297 } 293 298