Skip to content

Comments

fix: addContribution updates hypercert contributors#130

Merged
aspiers merged 1 commit intohypercerts-org:developfrom
Kzoeps:add-contribution-update-cert
Feb 17, 2026
Merged

fix: addContribution updates hypercert contributors#130
aspiers merged 1 commit intohypercerts-org:developfrom
Kzoeps:add-contribution-update-cert

Conversation

@Kzoeps
Copy link
Contributor

@Kzoeps Kzoeps commented Feb 9, 2026

Summary

  • Fixes addContribution to properly update the hypercert's contributors array
  • Refactors method signature to align with create() method's contribution handling
  • Adds support for batch contributor addition and proper contributor reference tracking

Breaking Changes

  • hypercertUri now required
  • contributors now accepts ContributorIdentityParams[] (DIDs, StrongRefs, or inline params)
  • contributionDetails replaces separate role/description parameters
  • Returns UpdateResult instead of CreateResult
  • update() method now accepts actual record fields (UpdateHypercertParams)

See changeset for migration examples.

Summary by CodeRabbit

  • Breaking Changes

    • addContribution now requires hypercertUri, accepts structured contributor identities and contributionDetails, supports optional weight/onProgress, and returns UpdateResult
    • update now accepts a dedicated UpdateHypercertParams type
    • Public types now use RefUri; two previously exported resolved types removed
  • Improvements

    • Batch contributor additions preserve existing contributors and unify contributor/detail handling
    • Centralized URI validation and stronger-reference (RefUri) usage
  • Tests

    • Added tests covering contributor entry building and attach flows; test helpers exposed for coverage

@changeset-bot
Copy link

changeset-bot bot commented Feb 9, 2026

🦋 Changeset detected

Latest commit: a9701cd

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@hypercerts-org/sdk-core Minor
@hypercerts-org/sdk-react Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented Feb 9, 2026

📝 Walkthrough

Walkthrough

Refactors hypercert contribution and update flows: adds RefUri types, introduces UpdateHypercertParams, changes signatures for update and addContribution, centralizes AT‑URI validation, adds helpers to build/attach contributor entries, removes legacy types/helpers, and expands tests and changelog.

Changes

Cohort / File(s) Summary
Public types & services
packages/sdk-core/src/services/hypercerts/types.ts, packages/sdk-core/src/repository/interfaces.ts, packages/sdk-core/src/index.ts
Introduce `RefUri = string
Repository implementation & API signatures
packages/sdk-core/src/repository/HypercertOperationsImpl.ts
Change update() to accept UpdateHypercertParams; overhaul addContribution() to require hypercertUri, structured contributors: ContributorIdentityParams[], contributionDetails: ContributionDetailsParams, optional weight/onProgress, and return UpdateResult; embed contributor/data as RefUri.
Helpers & control flow
packages/sdk-core/src/repository/HypercertOperationsImpl.ts
Add parseAndValidateUri(), buildContributorEntries(), attachContributorsToHypercert(), createContributionDetailsRecord(), resolveContributorIdentity()/resolveContributionDetails(); remove createContributionsWithProgress(); centralize URI validation and contributor resolution.
Tests & test helpers
packages/sdk-core/tests/repository/HypercertOperationsImpl.test.ts
Add TestableHypercertOperations test subclass exposing protected helpers for testing; expand tests for building/attaching contributors and integration flows; update test imports/fixtures.
Changelog & docs
.changeset/refactor-add-contribution-update-hypercert.md
Changelog entry describing API refactor, migration guidance, and rationale.
Minor test adjustments
packages/sdk-core/tests/auth/OAuthClient.test.ts, packages/sdk-core/tests/core/SDK.test.ts
Increase timeouts on a couple of tests to 10000ms.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant Ops as HypercertOperationsImpl
  participant Store as Repository/Records
  participant Resolver as Identity/Details Resolver

  Client->>Ops: addContribution(hypercertUri, contributors[], contributionDetails, weight?, onProgress?)
  Ops->>Ops: parseAndValidateUri(hypercertUri)
  Ops->>Resolver: resolveContributorIdentity(each contributor)
  Resolver-->>Ops: RefUri (contributorIdentity)
  Ops->>Resolver: resolveContributionDetails(contributionDetails) / createContributionDetailsRecord()
  Resolver-->>Ops: RefUri (contributionDetails)
  Ops->>Ops: buildContributorEntries(refs..., weight)
  Ops->>Store: attachContributorsToHypercert(hypercertUri, newContributorEntries)
  Store-->>Ops: UpdateResult
  Ops-->>Client: UpdateResult
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • aspiers
  • s-adamantine
  • bitbeckers

Poem

🐰 I stitched RefUris with careful hops,
Contributors gathered, no more props.
buildContributorEntries, attach with glee,
Hypercerts leaner — come celebrate with me! 🥕✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately summarizes the main change: fixing addContribution to update hypercert contributors, which is the core functionality change across this refactor.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/sdk-core/src/repository/HypercertOperationsImpl.ts (1)

1377-1380: ⚠️ Potential issue | 🟡 Minor

Typo in comment: "contribtorInformation".

📝 Fix typo
-      // we still store as contribtorInformation since it cant directly be a string
+      // we still store as contributorInformation since it can't directly be a string
🤖 Fix all issues with AI agents
In `@packages/sdk-core/tests/repository/HypercertOperationsImpl.test.ts`:
- Around line 957-980: The test "should update contributors array" uses
hypercertOps.update with an activity URI but the mocked getRecord response
returns a record with collection "org.hypercerts.claim.record"; update the mock
so the returned record's collection is "org.hypercerts.claim.activity" (either
by changing the beforeEach mock response or overriding the mock in this test) so
hypercertOps.update sees a matching activity record when asserting contributors;
change the mocked record's collection field where
mockAgent.com.atproto.repo.getRecord (or the beforeEach mock that supplies the
record) constructs the returned record.
🧹 Nitpick comments (2)
packages/sdk-core/src/repository/HypercertOperationsImpl.ts (1)

1293-1328: Use nullish coalescing for $type and createdAt to follow the established pattern.

The createContributionDetailsRecord sets $type and createdAt explicitly, then spreads ...extraProps last — which accidentally allows callers to override them via the index signature. Other helpers in this file (e.g., createLocationRecord at line 2683-2684, buildAttachmentRecord at lines 1104-1105) use the nullish coalescing pattern $type ?? DEFAULT per project conventions.

♻️ Suggested fix
   private async createContributionDetailsRecord(params: {
     role: string;
     contributionDescription?: string;
     startDate?: string;
     endDate?: string;
     [key: string]: unknown;
   }): Promise<CreateResult> {
     const createdAt = new Date().toISOString();
-    const { role, contributionDescription, startDate, endDate, ...extraProps } = params;
+    const { role, contributionDescription, startDate, endDate, $type, createdAt: callerCreatedAt, ...extraProps } = params;
     const contributionRecord: HypercertContributionDetails = {
-      $type: HYPERCERT_COLLECTIONS.CONTRIBUTION_DETAILS,
+      ...extraProps,
+      $type: ($type as string) ?? HYPERCERT_COLLECTIONS.CONTRIBUTION_DETAILS,
+      createdAt: (callerCreatedAt as string) ?? createdAt,
       role,
-      createdAt,
       contributionDescription,
       startDate,
       endDate,
-      ...extraProps,
     };

Based on learnings, record creation helper methods should use nullish coalescing to preserve caller-supplied $type and createdAt fields when provided, defaulting only when missing.

packages/sdk-core/tests/repository/HypercertOperationsImpl.test.ts (1)

4172-4286: Inconsistent approach for testing private vs. protected methods, and duplicate assertions.

Two observations:

  1. Inconsistency: buildContributorEntries and attachContributorsToHypercert are tested cleanly via the TestableHypercertOperations subclass, but resolveToStrongRef falls back to @ts-expect-error casts. Consider exposing it via the same TestableHypercertOperations subclass for consistency.

  2. Duplicate rejects.toThrow calls: Several tests (e.g., lines 4220–4225, 4236–4239, 4255–4258, 4264–4272, 4277–4285) invoke the method twice — once to check the error type, once to check the message. Each call creates a new rejected promise and re-exercises the whole mock chain. Use a single assertion with a regex or string match instead:

await expect(hypercertOps.resolveToStrongRef(invalidUri))
  .rejects.toThrow(expect.objectContaining({
    constructor: ValidationError,
    message: expect.stringContaining("Invalid AT-URI format"),
  }));

Or chain .rejects to a variable:

const rejection = expect(hypercertOps.resolveToStrongRef(invalidUri)).rejects;
await rejection.toThrow(ValidationError);
await rejection.toThrow("Invalid AT-URI format");

This halves the mock invocations and avoids subtle ordering issues if mock state changes between calls.

@Kzoeps Kzoeps force-pushed the add-contribution-update-cert branch from fc7d0f9 to e957b7d Compare February 9, 2026 11:20
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/sdk-core/src/repository/HypercertOperationsImpl.ts`:
- Around line 1377-1379: The inline comment in HypercertOperationsImpl.ts near
the identity handling branch has a misspelling ("contribtorInformation"); update
the comment to read "contributorInformation" (or better: "contributor
information") so it matches the actual symbol and intent around the
addContributorInformation call and stored data; ensure the comment is corrected
adjacent to the identity check and the await this.addContributorInformation({
identifier: identity }) line.
🧹 Nitpick comments (2)
packages/sdk-core/src/repository/HypercertOperationsImpl.ts (1)

1293-1310: ...extraProps spread can silently override $type and createdAt.

The destructuring at line 1301 only extracts named fields, so if a caller passes $type or createdAt via the [key: string]: unknown index signature, they'll end up in extraProps and override the explicit defaults at lines 1303-1305 (since the spread is last). Use the nullish coalescing pattern for consistency with other record creation helpers.

Suggested fix
-  private async createContributionDetailsRecord(params: {
-    role: string;
-    contributionDescription?: string;
-    startDate?: string;
-    endDate?: string;
-    [key: string]: unknown;
-  }): Promise<CreateResult> {
-    const createdAt = new Date().toISOString();
-    const { role, contributionDescription, startDate, endDate, ...extraProps } = params;
-    const contributionRecord: HypercertContributionDetails = {
-      $type: HYPERCERT_COLLECTIONS.CONTRIBUTION_DETAILS,
-      role,
-      createdAt,
-      contributionDescription,
-      startDate,
-      endDate,
-      ...extraProps,
-    };
+  private async createContributionDetailsRecord(params: {
+    role: string;
+    contributionDescription?: string;
+    startDate?: string;
+    endDate?: string;
+    [key: string]: unknown;
+  }): Promise<CreateResult> {
+    const { role, contributionDescription, startDate, endDate, $type, createdAt, ...extraProps } = params;
+    const contributionRecord: HypercertContributionDetails = {
+      ...extraProps,
+      $type: ($type as string) ?? HYPERCERT_COLLECTIONS.CONTRIBUTION_DETAILS,
+      createdAt: (createdAt as string) ?? new Date().toISOString(),
+      role,
+      contributionDescription,
+      startDate,
+      endDate,
+    };

Based on learnings: "record creation helper methods should use nullish coalescing to preserve caller-supplied $type and createdAt fields when provided, defaulting only when missing. Pattern: $type: $type ?? HYPERCERT_COLLECTIONS.X, createdAt: createdAt ?? new Date().toISOString()."

packages/sdk-core/tests/repository/HypercertOperationsImpl.test.ts (1)

4172-4286: Expose resolveToStrongRef via TestableHypercertOperations instead of @ts-expect-error.

The rest of this file uses the TestableHypercertOperations subclass to expose protected methods cleanly. These tests use repeated @ts-expect-error comments (lines 4179, 4201, 4219, 4222, etc.) to access a private method. Exposing it through the subclass would be more consistent and avoid the 12+ suppression comments.

Additionally, several tests (e.g., lines 4220–4225, 4235–4240) call the method twice — once per assertion — doubling the mock invocations unnecessarily. Use a variable to capture the rejection instead:

♻️ Example consolidation
-    // `@ts-expect-error` - accessing private method for testing
-    await expect(hypercertOps.resolveToStrongRef(invalidUri)).rejects.toThrow(ValidationError);
-    await expect(
-      // `@ts-expect-error` - accessing private method for testing
-      hypercertOps.resolveToStrongRef(invalidUri),
-    ).rejects.toThrow("Invalid AT-URI format");
+    const promise = hypercertOps.testResolveToStrongRef(invalidUri);
+    await expect(promise).rejects.toThrow(ValidationError);
+    await expect(promise).rejects.toThrow("Invalid AT-URI format");

@Kzoeps Kzoeps force-pushed the add-contribution-update-cert branch from e957b7d to a9701cd Compare February 9, 2026 11:48
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@packages/sdk-core/src/repository/HypercertOperationsImpl.ts`:
- Around line 1293-1310: The createContributionDetailsRecord function currently
always overwrites $type and createdAt; update it to accept optional $type and
createdAt in the CreateContributionDetailsParams type and use nullish coalescing
like buildAttachmentRecord: destructure params to include $type and createdAt,
compute finalCreatedAt = createdAt ?? new Date().toISOString(), and set
contributionRecord.$type = $type ?? HYPERCERT_COLLECTIONS.CONTRIBUTION_DETAILS
and contributionRecord.createdAt = finalCreatedAt while preserving
...extraProps.

In `@packages/sdk-core/src/repository/interfaces.ts`:
- Around line 385-425: UpdateHypercertParams is defined from HypercertClaim
which violates the repo convention; change UpdateHypercertParams to be
Partial<CreateHypercertParams> to follow the UpdateXxxParams =
Partial<CreateXxxParams> rule, and if you still need a type that mirrors the
record schema create a new distinct type (e.g., HypercertClaimUpdate or
UpdateHypercertRecord) defined as Partial<Omit<HypercertClaim, "$type" |
"createdAt" | "rights">> and use that where record-schema updates are required;
also apply the same pattern to the other similar types referenced in the review.
🧹 Nitpick comments (2)
packages/sdk-core/src/repository/HypercertOperationsImpl.ts (2)

1480-1498: Double-fetch of the hypercert record.

attachContributorsToHypercert calls this.get(hypercertUri) on line 1488 to read the existing contributors, then this.update(...) on line 1492 which internally fetches the same record again (line 649). Under concurrent modifications this also creates a wider TOCTOU window.

Consider refactoring update() to optionally accept pre-fetched record data, or extracting the merge+put logic to avoid the redundant round-trip.


1373-1380: Bare DID strings always create new contributorInformation records—potential duplicates.

When identity is a string (e.g., a DID), resolveContributorIdentity unconditionally calls addContributorInformation (line 1379), creating a new record each time. If addContribution is called multiple times for the same DID, this produces duplicate contributor records. Consider using a deterministic rkey (e.g., hashing the identifier) in addContributorInformation to achieve idempotent upserts, consistent with how createHypercertRecord uses content-hashed rKeys.

@aspiers aspiers merged commit 16e5c5f into hypercerts-org:develop Feb 17, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants