Commits
- Added Status table at top: Phase 0/1/2 done, Phase 3 next, 92 tests passing
- Added decision log entries 10-12 capturing surprises from Phase 2:
10. XRPC validation rejects record type \u2014 catch error, use responseBody
11. Dotted-key un-flattening required before sending records to PDS
12. resolveRefProperties returns { properties, required } for accurate
required propagation through ref-flattened sub-fields
- Marked Phase 0/1/2 sections as done with commit refs
- Added a 'Deviations from the plan' note under Phase 2 explaining the
five things that ended up different from the original design
- Expanded Phase 3 with what Phase 2 already set up for it and the
open question about where async blob upload post-processing fits
Critical bugs fixed:
1. Dotted keys produced by ref/object field flattening (e.g. reply.root,
reply.parent) were sent to the PDS as literal flat keys, producing
records that would always fail lexicon validation. Added
unflattenDottedKeys() to convert them back to nested objects before
the XRPC call. Also drops empty optional values so they don't get sent
as empty strings.
2. resolveLocalDef extracted the required-fields array but discarded it,
so the required status of ref-resolved sub-fields was lost. Changed
resolveRefProperties to return { properties, required } (new ResolvedRef
type) and updated flattenRefProperties to AND the schema's required
list with the parent's required status — so optional ref sub-fields
stay optional even when the schema would require them if the ref were
present.
Resource mapper UX improvements:
- Added loadOptionsDependsOn: ['collection'] so getRecordFields is only
called when the NSID changes, not on every editor keystroke.
- Added supportAutoMap: true so users can auto-map input data to fields.
- Added noFieldsError with a helpful message when lexicon resolution fails.
Cleanup:
- Removed unused INodePropertyOptions and ResourceMapperField imports.
- Removed unused 'collection' parameter from buildRecordFromNodeParams.
- Exported buildRecordFromNodeParams + unflattenDottedKeys for testing.
New tests (21 added, 92 total):
- tests/buildRecord.test.ts — 20 tests covering raw JSON, resourceMapper
values, dotted key un-flattening, empty value handling, deep nesting,
and plain object input (from expressions).
- tests/fieldMapping.test.ts — 1 test for the optional-ref-sub-field
required propagation fix.
New modules:
- src/nodes/Atproto/lexicon.ts — Lexicon resolution from PDS endpoint
(with XRPC validation bypass via error.responseBody) or DNS-based
@atproto/lexicon-resolver fallback. In-memory cache, full lexicon
document parsing with type inference, and ref resolution helpers.
- src/nodes/Atproto/fieldMapping.ts — Lexicon schema → ResourceMapperField[]
conversion with type mapping, createdAt auto-default, recursive ref
flattening (depth cap 3), inline object unwrapping with dotted prefix.
- tests/mockLexicons.ts — 5 realistic mock lexicon documents covering
records, primitives, inline objects, deeply nested refs, and queries.
Updated modules:
- src/nodes/Atproto/Atproto.node.ts — Added resourceMapper property for
Record Data (Create/Put), methods.resourceMapping.getRecordFields,
and buildRecordFromNodeParams helper for both resourceMapper and raw JSON.
- tests/setup.ts — Added com.atproto.lexicon.resolveLexicon mock handler
and catch-all bypass for unhandled requests (DID resolution, etc.).
- tsconfig.json — Added skippedLibCheck (already set) for ESM package compat.
- TODO.md — Marked Phase 0, 1, and 2 items as complete.
Tests: 71 total (35 Phase 1 + 36 Phase 2), all passing.
Phase 0:
- Initialize project structure with n8n community node conventions
- Configure package.json with scripts, dependencies, and n8n metadata
- Set up tsconfig.json (ES2022, strict, commonjs)
- Set up oxlint.json with n8n eslint plugin via jsPlugins
- Set up vitest.config.ts and .oxfmtrc.json
- Add AT Protocol butterfly icon
- Add .gitignore (node_modules, dist)
Phase 1:
- Credential definition (AtprotoApi): identifier, appPassword, serviceUrl
with test block that validates via createSession
- TID generation (tid.ts): 13-char base32-sortable, top bit 0,
53-bit microsecond timestamp, 10-bit clock ID
- Node description (Atproto.node.ts): all 5 operations with complete
property definitions (Collection, Repo, Record Key, Record Data,
Swap Commit, Limit, Cursor)
- Operations (operations.ts): createRecord, getRecord, putRecord,
deleteRecord, listRecords — with /createdAt auto-injection
- Execute method: session management via CredentialSession,
per-item processing, continueOnFail pattern
- Error handling: friendlyError maps XRPC errors to n8n messages
- Tests: tid.test.ts (format, uniqueness, sortability),
operations.test.ts (all 5 CRUD ops + error paths),
type-injection.test.ts (/createdAt behavior),
setup.ts (msw mock XRPC server)
- README.md: project overview, use cases, quick start
- DESIGN.md: architecture, all 9 resolved ambiguities, dependency analysis
- TODO.md: 46 user stories across 3 phases + setup/testing/distribution
- PLAN.md: step-by-step implementation guide with dev loop, tooling, and scaffolding
- Added Status table at top: Phase 0/1/2 done, Phase 3 next, 92 tests passing
- Added decision log entries 10-12 capturing surprises from Phase 2:
10. XRPC validation rejects record type \u2014 catch error, use responseBody
11. Dotted-key un-flattening required before sending records to PDS
12. resolveRefProperties returns { properties, required } for accurate
required propagation through ref-flattened sub-fields
- Marked Phase 0/1/2 sections as done with commit refs
- Added a 'Deviations from the plan' note under Phase 2 explaining the
five things that ended up different from the original design
- Expanded Phase 3 with what Phase 2 already set up for it and the
open question about where async blob upload post-processing fits
Critical bugs fixed:
1. Dotted keys produced by ref/object field flattening (e.g. reply.root,
reply.parent) were sent to the PDS as literal flat keys, producing
records that would always fail lexicon validation. Added
unflattenDottedKeys() to convert them back to nested objects before
the XRPC call. Also drops empty optional values so they don't get sent
as empty strings.
2. resolveLocalDef extracted the required-fields array but discarded it,
so the required status of ref-resolved sub-fields was lost. Changed
resolveRefProperties to return { properties, required } (new ResolvedRef
type) and updated flattenRefProperties to AND the schema's required
list with the parent's required status — so optional ref sub-fields
stay optional even when the schema would require them if the ref were
present.
Resource mapper UX improvements:
- Added loadOptionsDependsOn: ['collection'] so getRecordFields is only
called when the NSID changes, not on every editor keystroke.
- Added supportAutoMap: true so users can auto-map input data to fields.
- Added noFieldsError with a helpful message when lexicon resolution fails.
Cleanup:
- Removed unused INodePropertyOptions and ResourceMapperField imports.
- Removed unused 'collection' parameter from buildRecordFromNodeParams.
- Exported buildRecordFromNodeParams + unflattenDottedKeys for testing.
New tests (21 added, 92 total):
- tests/buildRecord.test.ts — 20 tests covering raw JSON, resourceMapper
values, dotted key un-flattening, empty value handling, deep nesting,
and plain object input (from expressions).
- tests/fieldMapping.test.ts — 1 test for the optional-ref-sub-field
required propagation fix.
New modules:
- src/nodes/Atproto/lexicon.ts — Lexicon resolution from PDS endpoint
(with XRPC validation bypass via error.responseBody) or DNS-based
@atproto/lexicon-resolver fallback. In-memory cache, full lexicon
document parsing with type inference, and ref resolution helpers.
- src/nodes/Atproto/fieldMapping.ts — Lexicon schema → ResourceMapperField[]
conversion with type mapping, createdAt auto-default, recursive ref
flattening (depth cap 3), inline object unwrapping with dotted prefix.
- tests/mockLexicons.ts — 5 realistic mock lexicon documents covering
records, primitives, inline objects, deeply nested refs, and queries.
Updated modules:
- src/nodes/Atproto/Atproto.node.ts — Added resourceMapper property for
Record Data (Create/Put), methods.resourceMapping.getRecordFields,
and buildRecordFromNodeParams helper for both resourceMapper and raw JSON.
- tests/setup.ts — Added com.atproto.lexicon.resolveLexicon mock handler
and catch-all bypass for unhandled requests (DID resolution, etc.).
- tsconfig.json — Added skippedLibCheck (already set) for ESM package compat.
- TODO.md — Marked Phase 0, 1, and 2 items as complete.
Tests: 71 total (35 Phase 1 + 36 Phase 2), all passing.
Phase 0:
- Initialize project structure with n8n community node conventions
- Configure package.json with scripts, dependencies, and n8n metadata
- Set up tsconfig.json (ES2022, strict, commonjs)
- Set up oxlint.json with n8n eslint plugin via jsPlugins
- Set up vitest.config.ts and .oxfmtrc.json
- Add AT Protocol butterfly icon
- Add .gitignore (node_modules, dist)
Phase 1:
- Credential definition (AtprotoApi): identifier, appPassword, serviceUrl
with test block that validates via createSession
- TID generation (tid.ts): 13-char base32-sortable, top bit 0,
53-bit microsecond timestamp, 10-bit clock ID
- Node description (Atproto.node.ts): all 5 operations with complete
property definitions (Collection, Repo, Record Key, Record Data,
Swap Commit, Limit, Cursor)
- Operations (operations.ts): createRecord, getRecord, putRecord,
deleteRecord, listRecords — with /createdAt auto-injection
- Execute method: session management via CredentialSession,
per-item processing, continueOnFail pattern
- Error handling: friendlyError maps XRPC errors to n8n messages
- Tests: tid.test.ts (format, uniqueness, sortability),
operations.test.ts (all 5 CRUD ops + error paths),
type-injection.test.ts (/createdAt behavior),
setup.ts (msw mock XRPC server)