diff --git a/.changeset/fix-sds-api-contracts.md b/.changeset/fix-sds-api-contracts.md new file mode 100644 index 0000000..ee48154 --- /dev/null +++ b/.changeset/fix-sds-api-contracts.md @@ -0,0 +1,11 @@ +--- +"@hypercerts-org/sdk-core": patch +--- + +Fix SDS organization and collaborator operations to match API contracts + +- Add required creatorDid parameter to organization.create endpoint +- Fix organization.list to parse organizations field instead of repositories +- Update accessType values to match SDS API: owner|shared|none (was owner|collaborator) +- Add permission string array parser for collaborator.list endpoint +- Update type definitions to match actual SDS API response formats diff --git a/.changeset/pagination-and-react-hooks-fix.md b/.changeset/pagination-and-react-hooks-fix.md new file mode 100644 index 0000000..873f4b9 --- /dev/null +++ b/.changeset/pagination-and-react-hooks-fix.md @@ -0,0 +1,30 @@ +--- +"@hypercerts-org/sdk-core": minor +"@hypercerts-org/sdk-react": patch +--- + +Add pagination support and fix React hooks for SDS operations + +**Breaking Changes (sdk-core):** +- `CollaboratorOperations.list()` now returns `{ collaborators: RepositoryAccessGrant[], cursor?: string }` instead of `RepositoryAccessGrant[]` +- `OrganizationOperations.list()` now returns `{ organizations: OrganizationInfo[], cursor?: string }` instead of `OrganizationInfo[]` + +**Features:** +- Add cursor-based pagination support to collaborator and organization list operations +- Support optional `limit` and `cursor` parameters for paginated queries +- Update internal methods (`hasAccess`, `getRole`, `get`) to handle new pagination structure + +**Bug Fixes (sdk-react):** +- Fix `useCollaborators` hook to correctly destructure paginated response +- Fix `useOrganizations` hook to correctly destructure paginated response +- All React hooks now properly handle the new pagination structure + +**Documentation:** +- Comprehensive README updates with clear examples for all SDK operations +- Added pagination examples throughout documentation +- Improved code samples with realistic use cases + +**Tests:** +- All 317 tests passing (181 sdk-core + 136 sdk-react) +- Updated test mocks to match new pagination response structure +- Build completes with zero warnings diff --git a/packages/lexicon/package.json b/packages/lexicon/package.json index 543bf00..af6f3c5 100644 --- a/packages/lexicon/package.json +++ b/packages/lexicon/package.json @@ -1,6 +1,6 @@ { "name": "@hypercerts-org/lexicon", - "version": "0.3.0", + "version": "0.4.0", "description": "ATProto lexicon definitions and TypeScript types for the Hypercerts protocol", "type": "module", "main": "dist/index.cjs", diff --git a/packages/sdk-core/README.md b/packages/sdk-core/README.md index cf61c19..ecc298c 100644 --- a/packages/sdk-core/README.md +++ b/packages/sdk-core/README.md @@ -1,54 +1,17 @@ # @hypercerts-org/sdk-core -Framework-agnostic ATProto SDK for Hypercerts. +Framework-agnostic ATProto SDK for Hypercerts. Create, manage, and collaborate on hypercerts using the AT Protocol. ```bash pnpm add @hypercerts-org/sdk-core ``` -## Entrypoints - -``` -@hypercerts-org/sdk-core -├── / → Full SDK (createATProtoSDK, Repository, types, errors) -├── /types → TypeScript types (re-exported from @hypercerts-org/lexicon) -├── /errors → Error classes -├── /lexicons → LexiconRegistry, HYPERCERT_LEXICONS, HYPERCERT_COLLECTIONS -├── /storage → InMemorySessionStore, InMemoryStateStore -└── /testing → createMockSession, MockSessionStore -``` - -## Type System - -Types are generated from ATProto lexicon definitions in `@hypercerts-org/lexicon` and re-exported with friendly aliases: - -| Lexicon Type | SDK Alias | -|--------------|-----------| -| `OrgHypercertsClaim.Main` | `HypercertClaim` | -| `OrgHypercertsClaimRights.Main` | `HypercertRights` | -| `OrgHypercertsClaimContribution.Main` | `HypercertContribution` | -| `OrgHypercertsClaimMeasurement.Main` | `HypercertMeasurement` | -| `OrgHypercertsClaimEvaluation.Main` | `HypercertEvaluation` | -| `OrgHypercertsCollection.Main` | `HypercertCollection` | -| `AppCertifiedLocation.Main` | `HypercertLocation` | - -```typescript -import type { HypercertClaim, HypercertRights } from "@hypercerts-org/sdk-core"; - -// For validation functions, import the namespaced types -import { OrgHypercertsClaim } from "@hypercerts-org/sdk-core"; - -if (OrgHypercertsClaim.isRecord(data)) { - // data is typed as HypercertClaim -} -``` - -## Usage +## Quick Start ```typescript import { createATProtoSDK } from "@hypercerts-org/sdk-core"; -// 1. Create SDK instance +// 1. Create SDK with OAuth configuration const sdk = createATProtoSDK({ oauth: { clientId: "https://your-app.com/client-metadata.json", @@ -59,85 +22,502 @@ const sdk = createATProtoSDK({ }, }); -// 2. Start OAuth flow → redirect user to authUrl +// 2. Authenticate user const authUrl = await sdk.authorize("user.bsky.social"); +// Redirect user to authUrl... + +// 3. Handle OAuth callback +const session = await sdk.callback(callbackParams); + +// 4. Get repository and start creating hypercerts +const repo = sdk.getRepository(session); +const claim = await repo.hypercerts.create({ + title: "Tree Planting Initiative 2025", + description: "Planted 1000 trees in the rainforest", + impact: { + scope: ["Environmental Conservation"], + work: { from: "2025-01-01", to: "2025-12-31" }, + contributors: ["did:plc:contributor1"], + }, +}); +``` + +## Core Concepts + +### 1. Authentication + +The SDK uses OAuth 2.0 for authentication with support for both PDS (Personal Data Server) and SDS (Shared Data Server). + +```typescript +// First-time user authentication +const authUrl = await sdk.authorize("user.bsky.social"); +// Redirect user to authUrl to complete OAuth flow + +// Handle the OAuth callback +const session = await sdk.callback({ + code: "...", + state: "...", + iss: "...", +}); -// 3. Handle callback at redirectUri → exchange code for session -const session = await sdk.callback(params); // params from callback URL +// Restore existing session for returning users +const session = await sdk.restoreSession("did:plc:user123"); -// 4. Use session to interact with repositories +// Get repository for authenticated user const repo = sdk.getRepository(session); -await repo.hypercerts.create({ title: "My Hypercert", ... }); +``` + +### 2. Working with Hypercerts + +#### Creating a Hypercert + +```typescript +const hypercert = await repo.hypercerts.create({ + title: "Climate Research Project", + description: "Research on carbon capture technologies", + image: imageBlob, // optional: File or Blob + externalUrl: "https://example.com/project", + + impact: { + scope: ["Climate Change", "Carbon Capture"], + work: { + from: "2024-01-01", + to: "2025-12-31", + }, + contributors: ["did:plc:researcher1", "did:plc:researcher2"], + }, + + rights: { + license: "CC-BY-4.0", + allowsDerivatives: true, + transferrable: false, + }, +}); + +console.log("Created hypercert:", hypercert.uri); +``` + +#### Retrieving Hypercerts + +```typescript +// Get a specific hypercert by URI +const hypercert = await repo.hypercerts.get( + "at://did:plc:user123/org.hypercerts.claim/abc123" +); + +// List all hypercerts in the repository +const { records } = await repo.hypercerts.list(); +for (const claim of records) { + console.log(claim.value.title); +} -// For returning users, restore session by DID -const existingSession = await sdk.restoreSession("did:plc:..."); +// List with pagination +const { records, cursor } = await repo.hypercerts.list({ limit: 10 }); +if (cursor) { + const nextPage = await repo.hypercerts.list({ limit: 10, cursor }); +} ``` -## Repository API +#### Updating a Hypercert +```typescript +// Update an existing hypercert +await repo.hypercerts.update( + "at://did:plc:user123/org.hypercerts.claim/abc123", + { + title: "Updated Climate Research Project", + description: "Expanded scope to include renewable energy", + impact: { + scope: ["Climate Change", "Carbon Capture", "Renewable Energy"], + work: { from: "2024-01-01", to: "2026-12-31" }, + contributors: ["did:plc:researcher1", "did:plc:researcher2"], + }, + } +); ``` -repo -├── .records → create, get, update, delete, list -├── .blobs → upload, get -├── .profile → get, update (PDS only) -├── .hypercerts → create, get, update, delete, list, addContribution, addMeasurement -├── .collaborators → grant, revoke, list, hasAccess, getRole, getPermissions, transferOwnership (SDS only) -└── .organizations → create, get, list (SDS only) + +#### Deleting a Hypercert + +```typescript +await repo.hypercerts.delete( + "at://did:plc:user123/org.hypercerts.claim/abc123" +); ``` -### Collaborator Operations (SDS only) +### 3. Contributions and Measurements + +#### Adding Contributions + +```typescript +// Add a contribution to a hypercert +const contribution = await repo.hypercerts.addContribution({ + claim: "at://did:plc:user123/org.hypercerts.claim/abc123", + contributor: "did:plc:contributor456", + description: "Led the research team and conducted field studies", + contributionType: "Work", + percentage: 40.0, +}); +``` + +#### Adding Measurements + +```typescript +// Add a measurement/evaluation +const measurement = await repo.hypercerts.addMeasurement({ + claim: "at://did:plc:user123/org.hypercerts.claim/abc123", + type: "Impact", + value: 1000, + unit: "trees planted", + verifiedBy: "did:plc:auditor789", + verificationMethod: "On-site inspection with GPS verification", + measuredAt: new Date().toISOString(), +}); +``` + +### 4. Blob Operations (Images & Files) + +```typescript +// Upload an image or file +const blobResult = await repo.blobs.upload(imageFile); +console.log("Blob uploaded:", blobResult.ref.$link); + +// Download a blob +const blobData = await repo.blobs.get( + "did:plc:user123", + "bafyreiabc123..." +); +``` + +### 5. Organizations (SDS only) + +Organizations allow multiple users to collaborate on shared repositories. + +```typescript +// Create an organization +const org = await repo.organizations.create({ + name: "Climate Research Institute", + description: "Leading research on climate solutions", + handle: "climate-research", // optional: unique handle +}); + +console.log("Organization DID:", org.did); + +// List all organizations you belong to +const { organizations } = await repo.organizations.list(); +for (const org of organizations) { + console.log(`${org.name} (${org.role})`); +} + +// List with pagination +const { organizations, cursor } = await repo.organizations.list({ limit: 10 }); + +// Get a specific organization +const org = await repo.organizations.get("did:plc:org123"); +console.log(`${org.name} - ${org.description}`); +``` + +### 6. Collaborator Management (SDS only) + +Manage who has access to your repository and what they can do. + +#### Granting Access ```typescript -// Grant access to a user +// Grant different levels of access await repo.collaborators.grant({ userDid: "did:plc:user123", role: "editor", // viewer | editor | admin | owner }); -// Revoke access -await repo.collaborators.revoke({ userDid: "did:plc:user123" }); +// Roles explained: +// - viewer: Read-only access +// - editor: Can create and edit records +// - admin: Can manage collaborators and settings +// - owner: Full control (same as repository owner) +``` + +#### Managing Collaborators + +```typescript +// List all collaborators with pagination +const { collaborators, cursor } = await repo.collaborators.list(); +for (const collab of collaborators) { + console.log(`${collab.userDid} - ${collab.role}`); +} -// List all collaborators -const collaborators = await repo.collaborators.list(); +// List next page +if (cursor) { + const nextPage = await repo.collaborators.list({ cursor, limit: 20 }); +} -// Check if user has access +// Check if a user has access const hasAccess = await repo.collaborators.hasAccess("did:plc:user123"); -// Get user's role +// Get a specific user's role const role = await repo.collaborators.getRole("did:plc:user123"); +console.log(`User role: ${role}`); // "editor", "admin", etc. // Get current user's permissions const permissions = await repo.collaborators.getPermissions(); if (permissions.admin) { - // Can manage collaborators + console.log("You can manage collaborators"); } +if (permissions.create) { + console.log("You can create records"); +} +``` + +#### Revoking Access + +```typescript +// Remove a collaborator +await repo.collaborators.revoke({ + userDid: "did:plc:user123", +}); +``` + +#### Transferring Ownership -// Transfer ownership (irreversible!) +```typescript +// Transfer repository ownership (irreversible!) await repo.collaborators.transferOwnership({ - newOwnerDid: "did:plc:new-owner", + newOwnerDid: "did:plc:newowner456", }); ``` -### SDS vs PDS Operations +### 7. Generic Record Operations + +For working with any ATProto record type: + +```typescript +// Create a generic record +const record = await repo.records.create({ + collection: "org.hypercerts.claim", + record: { + $type: "org.hypercerts.claim", + title: "My Claim", + // ... record data + }, +}); -| Operation | PDS | SDS | Namespace | -|-----------|-----|-----|-----------| -| Records (CRUD) | ✅ | ✅ | `com.atproto.repo.*` | -| Blobs | ✅ | ✅ | `com.atproto.repo.uploadBlob`, `com.atproto.sync.getBlob` | -| Profile | ✅ | ❌ | `app.bsky.actor.profile` | -| Organizations | ❌ | ✅ | `com.sds.organization.*` | -| Collaborators | ❌ | ✅ | `com.sds.repo.*` | +// Get a record +const record = await repo.records.get({ + collection: "org.hypercerts.claim", + rkey: "abc123", +}); -## Errors +// Update a record +await repo.records.update({ + collection: "org.hypercerts.claim", + rkey: "abc123", + record: { + $type: "org.hypercerts.claim", + title: "Updated Title", + // ... updated data + }, +}); + +// Delete a record +await repo.records.delete({ + collection: "org.hypercerts.claim", + rkey: "abc123", +}); + +// List records with pagination +const { records, cursor } = await repo.records.list({ + collection: "org.hypercerts.claim", + limit: 50, +}); +``` + +### 8. Profile Management (PDS only) ```typescript -import { ValidationError, NetworkError, AuthenticationError } from "@hypercerts-org/sdk-core/errors"; +// Get user profile +const profile = await repo.profile.get(); +console.log(`${profile.displayName} (@${profile.handle})`); + +// Update profile +await repo.profile.update({ + displayName: "Jane Researcher", + description: "Climate scientist and hypercert enthusiast", + avatar: avatarBlob, // optional + banner: bannerBlob, // optional +}); +``` + +## API Reference + +### Repository Operations + +| Operation | Method | PDS | SDS | Returns | +|-----------|--------|-----|-----|---------| +| **Records** | | | | | +| Create record | `repo.records.create()` | ✅ | ✅ | `{ uri, cid }` | +| Get record | `repo.records.get()` | ✅ | ✅ | Record data | +| Update record | `repo.records.update()` | ✅ | ✅ | `{ uri, cid }` | +| Delete record | `repo.records.delete()` | ✅ | ✅ | void | +| List records | `repo.records.list()` | ✅ | ✅ | `{ records, cursor? }` | +| **Hypercerts** | | | | | +| Create hypercert | `repo.hypercerts.create()` | ✅ | ✅ | `{ uri, cid, value }` | +| Get hypercert | `repo.hypercerts.get()` | ✅ | ✅ | Full hypercert | +| Update hypercert | `repo.hypercerts.update()` | ✅ | ✅ | `{ uri, cid }` | +| Delete hypercert | `repo.hypercerts.delete()` | ✅ | ✅ | void | +| List hypercerts | `repo.hypercerts.list()` | ✅ | ✅ | `{ records, cursor? }` | +| Add contribution | `repo.hypercerts.addContribution()` | ✅ | ✅ | Contribution | +| Add measurement | `repo.hypercerts.addMeasurement()` | ✅ | ✅ | Measurement | +| **Blobs** | | | | | +| Upload blob | `repo.blobs.upload()` | ✅ | ✅ | `{ ref, mimeType, size }` | +| Get blob | `repo.blobs.get()` | ✅ | ✅ | Blob data | +| **Profile** | | | | | +| Get profile | `repo.profile.get()` | ✅ | ❌ | Profile data | +| Update profile | `repo.profile.update()` | ✅ | ❌ | void | +| **Organizations** | | | | | +| Create org | `repo.organizations.create()` | ❌ | ✅ | `{ did, name, ... }` | +| Get org | `repo.organizations.get()` | ❌ | ✅ | Organization | +| List orgs | `repo.organizations.list()` | ❌ | ✅ | `{ organizations, cursor? }` | +| **Collaborators** | | | | | +| Grant access | `repo.collaborators.grant()` | ❌ | ✅ | void | +| Revoke access | `repo.collaborators.revoke()` | ❌ | ✅ | void | +| List collaborators | `repo.collaborators.list()` | ❌ | ✅ | `{ collaborators, cursor? }` | +| Check access | `repo.collaborators.hasAccess()` | ❌ | ✅ | boolean | +| Get role | `repo.collaborators.getRole()` | ❌ | ✅ | Role string | +| Get permissions | `repo.collaborators.getPermissions()` | ❌ | ✅ | Permissions | +| Transfer ownership | `repo.collaborators.transferOwnership()` | ❌ | ✅ | void | + +## Type System + +Types are generated from ATProto lexicon definitions and exported with friendly aliases: + +```typescript +import type { + HypercertClaim, + HypercertRights, + HypercertContribution, + HypercertMeasurement, + HypercertEvaluation, + HypercertCollection, + HypercertLocation, +} from "@hypercerts-org/sdk-core"; + +// For validation, use namespaced imports +import { OrgHypercertsClaim } from "@hypercerts-org/sdk-core"; + +if (OrgHypercertsClaim.isRecord(data)) { + // data is typed as HypercertClaim +} +``` + +| Lexicon Type | SDK Alias | +|--------------|-----------| +| `OrgHypercertsClaim.Main` | `HypercertClaim` | +| `OrgHypercertsClaimRights.Main` | `HypercertRights` | +| `OrgHypercertsClaimContribution.Main` | `HypercertContribution` | +| `OrgHypercertsClaimMeasurement.Main` | `HypercertMeasurement` | +| `OrgHypercertsClaimEvaluation.Main` | `HypercertEvaluation` | +| `OrgHypercertsCollection.Main` | `HypercertCollection` | +| `AppCertifiedLocation.Main` | `HypercertLocation` | + +## Error Handling + +```typescript +import { + ValidationError, + NetworkError, + AuthenticationError, + SDSRequiredError, +} from "@hypercerts-org/sdk-core/errors"; + +try { + await repo.hypercerts.create({ ... }); +} catch (error) { + if (error instanceof ValidationError) { + console.error("Invalid hypercert data:", error.message); + } else if (error instanceof NetworkError) { + console.error("Network issue:", error.message); + } else if (error instanceof AuthenticationError) { + console.error("Authentication failed:", error.message); + } else if (error instanceof SDSRequiredError) { + console.error("This operation requires SDS:", error.message); + } +} +``` + +## Package Entrypoints + +``` +@hypercerts-org/sdk-core +├── / → Full SDK (createATProtoSDK, Repository, types, errors) +├── /types → TypeScript types (re-exported from @hypercerts-org/lexicon) +├── /errors → Error classes +├── /lexicons → LexiconRegistry, HYPERCERT_LEXICONS, HYPERCERT_COLLECTIONS +├── /storage → InMemorySessionStore, InMemoryStateStore +└── /testing → createMockSession, MockSessionStore +``` + +## Advanced Usage + +### Custom Session Storage + +```typescript +import { createATProtoSDK } from "@hypercerts-org/sdk-core"; +import { InMemorySessionStore } from "@hypercerts-org/sdk-core/storage"; + +const sdk = createATProtoSDK({ + oauth: { ... }, + sessionStore: new InMemorySessionStore(), +}); +``` + +### Testing with Mocks + +```typescript +import { createMockSession, MockSessionStore } from "@hypercerts-org/sdk-core/testing"; + +const mockSession = createMockSession({ + did: "did:plc:test123", + handle: "test.user", +}); + +const mockStore = new MockSessionStore(); +await mockStore.set(mockSession); +``` + +### Working with Lexicons + +```typescript +import { + LexiconRegistry, + HYPERCERT_LEXICONS, + HYPERCERT_COLLECTIONS, +} from "@hypercerts-org/sdk-core/lexicons"; + +const registry = new LexiconRegistry(); +registry.registerLexicons(HYPERCERT_LEXICONS); + +// Validate a record +const isValid = registry.validate( + "org.hypercerts.claim", + claimData +); ``` ## Development ```bash -pnpm build # Build -pnpm test # Test -pnpm test:coverage # Coverage +pnpm install # Install dependencies +pnpm build # Build the package +pnpm test # Run tests +pnpm test:coverage # Run tests with coverage +pnpm test:watch # Run tests in watch mode ``` + +## License + +MIT + +## Resources + +- [ATProto Documentation](https://atproto.com/docs) +- [Hypercerts Documentation](https://hypercerts.org) +- [GitHub Repository](https://github.com/hypercerts-org/hypercerts-sdk) diff --git a/packages/sdk-core/package.json b/packages/sdk-core/package.json index 3b1339c..90ba695 100644 --- a/packages/sdk-core/package.json +++ b/packages/sdk-core/package.json @@ -1,6 +1,6 @@ { "name": "@hypercerts-org/sdk-core", - "version": "0.3.0", + "version": "0.4.0", "description": "Framework-agnostic ATProto SDK core for authentication, repository operations, and lexicon management", "main": "dist/index.cjs", "repository": { diff --git a/packages/sdk-core/src/core/types.ts b/packages/sdk-core/src/core/types.ts index a183978..1a5d688 100644 --- a/packages/sdk-core/src/core/types.ts +++ b/packages/sdk-core/src/core/types.ts @@ -185,9 +185,10 @@ export const OrganizationSchema = z.object({ /** * How the current user relates to this organization. * - `"owner"`: User created or owns the organization - * - `"collaborator"`: User was invited to collaborate + * - `"shared"`: User was invited to collaborate (has permissions) + * - `"none"`: User has no access to this organization */ - accessType: z.enum(["owner", "collaborator"]), + accessType: z.enum(["owner", "shared", "none"]), }); /** diff --git a/packages/sdk-core/src/repository/BlobOperationsImpl.ts b/packages/sdk-core/src/repository/BlobOperationsImpl.ts index 6fa6eaa..fea8620 100644 --- a/packages/sdk-core/src/repository/BlobOperationsImpl.ts +++ b/packages/sdk-core/src/repository/BlobOperationsImpl.ts @@ -122,7 +122,7 @@ export class BlobOperationsImpl implements BlobOperations { } return { - ref: result.data.blob.ref, + ref: { $link: result.data.blob.ref.toString() }, mimeType: result.data.blob.mimeType, size: result.data.blob.size, }; diff --git a/packages/sdk-core/src/repository/CollaboratorOperationsImpl.ts b/packages/sdk-core/src/repository/CollaboratorOperationsImpl.ts index 05e2c5c..29d44dd 100644 --- a/packages/sdk-core/src/repository/CollaboratorOperationsImpl.ts +++ b/packages/sdk-core/src/repository/CollaboratorOperationsImpl.ts @@ -107,6 +107,27 @@ export class CollaboratorOperationsImpl implements CollaboratorOperations { return "viewer"; } + /** + * Converts a permission string array to a permissions object. + * + * The SDS API returns permissions as an array of strings (e.g., ["read", "create"]). + * This method converts them to the boolean flag format used by the SDK. + * + * @param permissionArray - Array of permission strings from SDS API + * @returns Permission flags object + * @internal + */ + private parsePermissions(permissionArray: string[]): CollaboratorPermissions { + return { + read: permissionArray.includes("read"), + create: permissionArray.includes("create"), + update: permissionArray.includes("update"), + delete: permissionArray.includes("delete"), + admin: permissionArray.includes("admin"), + owner: permissionArray.includes("owner"), + }; + } + /** * Grants repository access to a user. * @@ -188,7 +209,10 @@ export class CollaboratorOperationsImpl implements CollaboratorOperations { /** * Lists all collaborators on the repository. * - * @returns Promise resolving to array of access grants + * @param params - Optional pagination parameters + * @param params.limit - Maximum number of results (1-100, default 50) + * @param params.cursor - Pagination cursor from previous response + * @returns Promise resolving to collaborators and optional cursor * @throws {@link NetworkError} if the list operation fails * * @remarks @@ -197,23 +221,37 @@ export class CollaboratorOperationsImpl implements CollaboratorOperations { * * @example * ```typescript - * const collaborators = await repo.collaborators.list(); + * // Get first page + * const page1 = await repo.collaborators.list({ limit: 10 }); + * console.log(`Found ${page1.collaborators.length} collaborators`); + * + * // Get next page if available + * if (page1.cursor) { + * const page2 = await repo.collaborators.list({ limit: 10, cursor: page1.cursor }); + * } * * // Filter active collaborators - * const active = collaborators.filter(c => !c.revokedAt); - * - * // Group by role - * const byRole = { - * owners: active.filter(c => c.role === "owner"), - * admins: active.filter(c => c.role === "admin"), - * editors: active.filter(c => c.role === "editor"), - * viewers: active.filter(c => c.role === "viewer"), - * }; + * const active = page1.collaborators.filter(c => !c.revokedAt); * ``` */ - async list(): Promise { + async list(params?: { limit?: number; cursor?: string }): Promise<{ + collaborators: RepositoryAccessGrant[]; + cursor?: string; + }> { + const queryParams = new URLSearchParams({ + repo: this.repoDid, + }); + + if (params?.limit !== undefined) { + queryParams.set("limit", params.limit.toString()); + } + + if (params?.cursor) { + queryParams.set("cursor", params.cursor); + } + const response = await this.session.fetchHandler( - `${this.serverUrl}/xrpc/com.sds.repo.listCollaborators?repo=${encodeURIComponent(this.repoDid)}`, + `${this.serverUrl}/xrpc/com.sds.repo.listCollaborators?${queryParams.toString()}`, { method: "GET" }, ); @@ -222,22 +260,30 @@ export class CollaboratorOperationsImpl implements CollaboratorOperations { } const data = await response.json(); - return (data.collaborators || []).map( + const collaborators = (data.collaborators || []).map( (c: { userDid: string; - permissions: CollaboratorPermissions; + permissions: string[]; // SDS API returns string array grantedBy: string; grantedAt: string; revokedAt?: string; - }) => ({ - userDid: c.userDid, - role: this.permissionsToRole(c.permissions), - permissions: c.permissions, - grantedBy: c.grantedBy, - grantedAt: c.grantedAt, - revokedAt: c.revokedAt, - }), + }) => { + const permissions = this.parsePermissions(c.permissions); + return { + userDid: c.userDid, + role: this.permissionsToRole(permissions), + permissions: permissions, + grantedBy: c.grantedBy, + grantedAt: c.grantedAt, + revokedAt: c.revokedAt, + }; + }, ); + + return { + collaborators, + cursor: data.cursor, + }; } /** @@ -261,7 +307,7 @@ export class CollaboratorOperationsImpl implements CollaboratorOperations { */ async hasAccess(userDid: string): Promise { try { - const collaborators = await this.list(); + const { collaborators } = await this.list(); return collaborators.some((c) => c.userDid === userDid && !c.revokedAt); } catch { return false; @@ -283,7 +329,7 @@ export class CollaboratorOperationsImpl implements CollaboratorOperations { * ``` */ async getRole(userDid: string): Promise { - const collaborators = await this.list(); + const { collaborators } = await this.list(); const collab = collaborators.find((c) => c.userDid === userDid && !c.revokedAt); return collab?.role ?? null; } diff --git a/packages/sdk-core/src/repository/HypercertOperationsImpl.ts b/packages/sdk-core/src/repository/HypercertOperationsImpl.ts index c7f5fca..d95df9c 100644 --- a/packages/sdk-core/src/repository/HypercertOperationsImpl.ts +++ b/packages/sdk-core/src/repository/HypercertOperationsImpl.ts @@ -208,7 +208,7 @@ export class HypercertOperationsImpl extends EventEmitter imple if (uploadResult.success) { imageBlobRef = { $type: "blob", - ref: uploadResult.data.blob.ref, + ref: { $link: uploadResult.data.blob.ref.toString() }, mimeType: uploadResult.data.blob.mimeType, size: uploadResult.data.blob.size, }; @@ -676,7 +676,7 @@ export class HypercertOperationsImpl extends EventEmitter imple if (uploadResult.success) { locationValue = { $type: "blob", - ref: uploadResult.data.blob.ref, + ref: { $link: uploadResult.data.blob.ref.toString() }, mimeType: uploadResult.data.blob.mimeType, size: uploadResult.data.blob.size, }; @@ -999,7 +999,7 @@ export class HypercertOperationsImpl extends EventEmitter imple if (uploadResult.success) { coverPhotoRef = { $type: "blob", - ref: uploadResult.data.blob.ref, + ref: { $link: uploadResult.data.blob.ref.toString() }, mimeType: uploadResult.data.blob.mimeType, size: uploadResult.data.blob.size, }; diff --git a/packages/sdk-core/src/repository/OrganizationOperationsImpl.ts b/packages/sdk-core/src/repository/OrganizationOperationsImpl.ts index 3d98dab..f03e3f9 100644 --- a/packages/sdk-core/src/repository/OrganizationOperationsImpl.ts +++ b/packages/sdk-core/src/repository/OrganizationOperationsImpl.ts @@ -109,10 +109,18 @@ export class OrganizationOperationsImpl implements OrganizationOperations { * ``` */ async create(params: { name: string; description?: string; handle?: string }): Promise { + const userDid = this.session.did || this.session.sub; + if (!userDid) { + throw new NetworkError("No authenticated user found"); + } + const response = await this.session.fetchHandler(`${this.serverUrl}/xrpc/com.sds.organization.create`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify(params), + body: JSON.stringify({ + ...params, + creatorDid: userDid, + }), }); if (!response.ok) { @@ -126,8 +134,15 @@ export class OrganizationOperationsImpl implements OrganizationOperations { name: data.name, description: data.description, createdAt: data.createdAt || new Date().toISOString(), - accessType: "owner", - permissions: { read: true, create: true, update: true, delete: true, admin: true, owner: true }, + accessType: data.accessType || "owner", + permissions: data.permissions || { + read: true, + create: true, + update: true, + delete: true, + admin: true, + owner: true, + }, }; } @@ -155,8 +170,8 @@ export class OrganizationOperationsImpl implements OrganizationOperations { */ async get(did: string): Promise { try { - const orgs = await this.list(); - return orgs.find((o) => o.did === did) ?? null; + const { organizations } = await this.list(); + return organizations.find((o) => o.did === did) ?? null; } catch { return null; } @@ -165,7 +180,10 @@ export class OrganizationOperationsImpl implements OrganizationOperations { /** * Lists organizations the current user has access to. * - * @returns Promise resolving to array of organization info + * @param params - Optional pagination parameters + * @param params.limit - Maximum number of results (1-100, default 50) + * @param params.cursor - Pagination cursor from previous response + * @returns Promise resolving to organizations and optional cursor * @throws {@link NetworkError} if the list operation fails * * @remarks @@ -177,21 +195,25 @@ export class OrganizationOperationsImpl implements OrganizationOperations { * * @example * ```typescript - * const orgs = await repo.organizations.list(); + * // Get first page + * const page1 = await repo.organizations.list({ limit: 20 }); + * console.log(`Found ${page1.organizations.length} organizations`); * - * // Filter by access type - * const owned = orgs.filter(o => o.accessType === "owner"); - * const collaborated = orgs.filter(o => o.accessType === "collaborator"); + * // Get next page if available + * if (page1.cursor) { + * const page2 = await repo.organizations.list({ limit: 20, cursor: page1.cursor }); + * } * - * console.log(`You own ${owned.length} organizations`); - * console.log(`You collaborate on ${collaborated.length} organizations`); + * // Filter by access type + * const owned = page1.organizations.filter(o => o.accessType === "owner"); + * const shared = page1.organizations.filter(o => o.accessType === "shared"); * ``` * * @example Display organization details * ```typescript - * const orgs = await repo.organizations.list(); + * const { organizations } = await repo.organizations.list(); * - * for (const org of orgs) { + * for (const org of organizations) { * console.log(`${org.name} (@${org.handle})`); * console.log(` DID: ${org.did}`); * console.log(` Access: ${org.accessType}`); @@ -201,9 +223,29 @@ export class OrganizationOperationsImpl implements OrganizationOperations { * } * ``` */ - async list(): Promise { + async list(params?: { limit?: number; cursor?: string }): Promise<{ + organizations: OrganizationInfo[]; + cursor?: string; + }> { + const userDid = this.session.did || this.session.sub; + if (!userDid) { + throw new NetworkError("No authenticated user found"); + } + + const queryParams = new URLSearchParams({ + userDid, + }); + + if (params?.limit !== undefined) { + queryParams.set("limit", params.limit.toString()); + } + + if (params?.cursor) { + queryParams.set("cursor", params.cursor); + } + const response = await this.session.fetchHandler( - `${this.serverUrl}/xrpc/com.sds.organization.list?userDid=${encodeURIComponent(this.session.did || this.session.sub)}`, + `${this.serverUrl}/xrpc/com.sds.organization.list?${queryParams.toString()}`, { method: "GET" }, ); @@ -212,23 +254,29 @@ export class OrganizationOperationsImpl implements OrganizationOperations { } const data = await response.json(); - return (data.repositories || []).map( + const organizations = (data.organizations || []).map( (r: { did: string; handle: string; name: string; description?: string; - accessType: "owner" | "collaborator"; + createdAt?: string; + accessType: "owner" | "shared" | "none"; permissions: CollaboratorPermissions; }) => ({ did: r.did, handle: r.handle, name: r.name, description: r.description, - createdAt: new Date().toISOString(), // SDS may not return this + createdAt: r.createdAt || new Date().toISOString(), accessType: r.accessType, permissions: r.permissions, }), ); + + return { + organizations, + cursor: data.cursor, + }; } } diff --git a/packages/sdk-core/src/repository/interfaces.ts b/packages/sdk-core/src/repository/interfaces.ts index d9ee181..35fe070 100644 --- a/packages/sdk-core/src/repository/interfaces.ts +++ b/packages/sdk-core/src/repository/interfaces.ts @@ -809,9 +809,15 @@ export interface CollaboratorOperations { /** * Lists all collaborators on the repository. * - * @returns Promise resolving to array of access grants + * @param params - Optional pagination parameters + * @param params.limit - Maximum number of results (1-100, default 50) + * @param params.cursor - Pagination cursor from previous response + * @returns Promise resolving to collaborators and optional cursor */ - list(): Promise; + list(params?: { limit?: number; cursor?: string }): Promise<{ + collaborators: RepositoryAccessGrant[]; + cursor?: string; + }>; /** * Checks if a user has any access to the repository. @@ -891,7 +897,13 @@ export interface OrganizationOperations { /** * Lists organizations the current user has access to. * - * @returns Promise resolving to array of organization info + * @param params - Optional pagination parameters + * @param params.limit - Maximum number of results (1-100, default 50) + * @param params.cursor - Pagination cursor from previous response + * @returns Promise resolving to organizations and optional cursor */ - list(): Promise; + list(params?: { limit?: number; cursor?: string }): Promise<{ + organizations: OrganizationInfo[]; + cursor?: string; + }>; } diff --git a/packages/sdk-core/src/repository/types.ts b/packages/sdk-core/src/repository/types.ts index 8873dfe..5c705a1 100644 --- a/packages/sdk-core/src/repository/types.ts +++ b/packages/sdk-core/src/repository/types.ts @@ -85,7 +85,7 @@ export interface OrganizationInfo { name: string; description?: string; createdAt: string; - accessType: "owner" | "collaborator"; + accessType: "owner" | "shared" | "none"; permissions: CollaboratorPermissions; collaboratorCount?: number; profile?: { diff --git a/packages/sdk-core/tests/repository/BlobOperationsImpl.test.ts b/packages/sdk-core/tests/repository/BlobOperationsImpl.test.ts index c1c2cb4..2e954fc 100644 --- a/packages/sdk-core/tests/repository/BlobOperationsImpl.test.ts +++ b/packages/sdk-core/tests/repository/BlobOperationsImpl.test.ts @@ -28,11 +28,14 @@ describe("BlobOperationsImpl", () => { describe("upload", () => { it("should upload a blob successfully", async () => { const mockBlob = new Blob(["test content"], { type: "text/plain" }); + const mockCID = { + toString: () => "bafyrei123", + }; mockAgent.com.atproto.repo.uploadBlob.mockResolvedValue({ success: true, data: { blob: { - ref: { $link: "bafyrei123" }, + ref: mockCID, mimeType: "text/plain", size: 12, }, @@ -61,10 +64,9 @@ describe("BlobOperationsImpl", () => { await blobOps.upload(mockBlob); - expect(mockAgent.com.atproto.repo.uploadBlob).toHaveBeenCalledWith( - expect.any(Uint8Array), - { encoding: "image/png" }, - ); + expect(mockAgent.com.atproto.repo.uploadBlob).toHaveBeenCalledWith(expect.any(Uint8Array), { + encoding: "image/png", + }); }); it("should default to application/octet-stream for blobs without type", async () => { @@ -82,10 +84,9 @@ describe("BlobOperationsImpl", () => { await blobOps.upload(mockBlob); - expect(mockAgent.com.atproto.repo.uploadBlob).toHaveBeenCalledWith( - expect.any(Uint8Array), - { encoding: "application/octet-stream" }, - ); + expect(mockAgent.com.atproto.repo.uploadBlob).toHaveBeenCalledWith(expect.any(Uint8Array), { + encoding: "application/octet-stream", + }); }); it("should throw NetworkError when API returns success: false", async () => { diff --git a/packages/sdk-core/tests/repository/CollaboratorOperationsImpl.test.ts b/packages/sdk-core/tests/repository/CollaboratorOperationsImpl.test.ts index ddb0fc7..5b88d1f 100644 --- a/packages/sdk-core/tests/repository/CollaboratorOperationsImpl.test.ts +++ b/packages/sdk-core/tests/repository/CollaboratorOperationsImpl.test.ts @@ -131,13 +131,13 @@ describe("CollaboratorOperationsImpl", () => { collaborators: [ { userDid: "did:plc:user1", - permissions: { read: true, create: true, update: true, delete: false, admin: false, owner: false }, + permissions: ["read", "create", "update"], grantedBy: "did:plc:owner", grantedAt: "2024-01-01T00:00:00Z", }, { userDid: "did:plc:user2", - permissions: { read: true, create: false, update: false, delete: false, admin: false, owner: false }, + permissions: ["read"], grantedBy: "did:plc:owner", grantedAt: "2024-01-02T00:00:00Z", }, @@ -147,10 +147,10 @@ describe("CollaboratorOperationsImpl", () => { const result = await collaboratorOps.list(); - expect(result).toHaveLength(2); - expect(result[0].userDid).toBe("did:plc:user1"); - expect(result[0].role).toBe("editor"); - expect(result[1].role).toBe("viewer"); + expect(result.collaborators).toHaveLength(2); + expect(result.collaborators[0].userDid).toBe("did:plc:user1"); + expect(result.collaborators[0].role).toBe("editor"); + expect(result.collaborators[1].role).toBe("viewer"); }); it("should handle empty collaborators list", async () => { @@ -161,7 +161,7 @@ describe("CollaboratorOperationsImpl", () => { const result = await collaboratorOps.list(); - expect(result).toHaveLength(0); + expect(result.collaborators).toHaveLength(0); }); it("should correctly map permissions to roles", async () => { @@ -171,13 +171,13 @@ describe("CollaboratorOperationsImpl", () => { collaborators: [ { userDid: "did:plc:owner", - permissions: { read: true, create: true, update: true, delete: true, admin: true, owner: true }, + permissions: ["read", "create", "update", "delete", "admin", "owner"], grantedBy: "did:plc:system", grantedAt: "2024-01-01T00:00:00Z", }, { userDid: "did:plc:admin", - permissions: { read: true, create: true, update: true, delete: true, admin: true, owner: false }, + permissions: ["read", "create", "update", "delete", "admin"], grantedBy: "did:plc:owner", grantedAt: "2024-01-01T00:00:00Z", }, @@ -187,8 +187,8 @@ describe("CollaboratorOperationsImpl", () => { const result = await collaboratorOps.list(); - expect(result[0].role).toBe("owner"); - expect(result[1].role).toBe("admin"); + expect(result.collaborators[0].role).toBe("owner"); + expect(result.collaborators[1].role).toBe("admin"); }); it("should throw NetworkError on failure", async () => { @@ -209,7 +209,7 @@ describe("CollaboratorOperationsImpl", () => { collaborators: [ { userDid: "did:plc:activeuser", - permissions: { read: true, create: false, update: false, delete: false, admin: false, owner: false }, + permissions: ["read"], grantedBy: "did:plc:owner", grantedAt: "2024-01-01T00:00:00Z", }, @@ -240,7 +240,7 @@ describe("CollaboratorOperationsImpl", () => { collaborators: [ { userDid: "did:plc:revokeduser", - permissions: { read: true, create: false, update: false, delete: false, admin: false, owner: false }, + permissions: ["read"], grantedBy: "did:plc:owner", grantedAt: "2024-01-01T00:00:00Z", revokedAt: "2024-02-01T00:00:00Z", @@ -271,7 +271,7 @@ describe("CollaboratorOperationsImpl", () => { collaborators: [ { userDid: "did:plc:editor", - permissions: { read: true, create: true, update: true, delete: false, admin: false, owner: false }, + permissions: ["read", "create", "update"], grantedBy: "did:plc:owner", grantedAt: "2024-01-01T00:00:00Z", }, @@ -302,7 +302,7 @@ describe("CollaboratorOperationsImpl", () => { collaborators: [ { userDid: "did:plc:revoked", - permissions: { read: true, create: true, update: true, delete: false, admin: false, owner: false }, + permissions: ["read", "create", "update"], grantedBy: "did:plc:owner", grantedAt: "2024-01-01T00:00:00Z", revokedAt: "2024-02-01T00:00:00Z", diff --git a/packages/sdk-core/tests/repository/OrganizationOperationsImpl.test.ts b/packages/sdk-core/tests/repository/OrganizationOperationsImpl.test.ts index 7476e4e..874011a 100644 --- a/packages/sdk-core/tests/repository/OrganizationOperationsImpl.test.ts +++ b/packages/sdk-core/tests/repository/OrganizationOperationsImpl.test.ts @@ -74,9 +74,7 @@ describe("OrganizationOperationsImpl", () => { statusText: "Conflict", }); - await expect( - orgOps.create({ name: "Test Org" }), - ).rejects.toThrow(NetworkError); + await expect(orgOps.create({ name: "Test Org" })).rejects.toThrow(NetworkError); }); }); @@ -85,7 +83,7 @@ describe("OrganizationOperationsImpl", () => { mockSession.fetchHandler.mockResolvedValue({ ok: true, json: async () => ({ - repositories: [ + organizations: [ { did: "did:plc:org1", handle: "org1.example.com", @@ -98,7 +96,7 @@ describe("OrganizationOperationsImpl", () => { did: "did:plc:org2", handle: "org2.example.com", name: "Organization 2", - accessType: "collaborator", + accessType: "shared", permissions: { read: true, create: true, update: true, delete: false, admin: false, owner: false }, }, ], @@ -116,7 +114,7 @@ describe("OrganizationOperationsImpl", () => { mockSession.fetchHandler.mockResolvedValue({ ok: true, json: async () => ({ - repositories: [], + organizations: [], }), }); @@ -139,7 +137,7 @@ describe("OrganizationOperationsImpl", () => { mockSession.fetchHandler.mockResolvedValue({ ok: true, json: async () => ({ - repositories: [ + organizations: [ { did: "did:plc:org1", handle: "org1.example.com", @@ -152,7 +150,7 @@ describe("OrganizationOperationsImpl", () => { handle: "org2.example.com", name: "Organization 2", description: "Second org", - accessType: "collaborator", + accessType: "shared", permissions: { read: true, create: true, update: false, delete: false, admin: false, owner: false }, }, ], @@ -161,28 +159,28 @@ describe("OrganizationOperationsImpl", () => { const result = await orgOps.list(); - expect(result).toHaveLength(2); - expect(result[0].did).toBe("did:plc:org1"); - expect(result[0].accessType).toBe("owner"); - expect(result[1].did).toBe("did:plc:org2"); - expect(result[1].accessType).toBe("collaborator"); + expect(result.organizations).toHaveLength(2); + expect(result.organizations[0].did).toBe("did:plc:org1"); + expect(result.organizations[0].accessType).toBe("owner"); + expect(result.organizations[1].did).toBe("did:plc:org2"); + expect(result.organizations[1].accessType).toBe("shared"); }); it("should handle empty repositories list", async () => { mockSession.fetchHandler.mockResolvedValue({ ok: true, - json: async () => ({ repositories: [] }), + json: async () => ({ organizations: [] }), }); const result = await orgOps.list(); - expect(result).toHaveLength(0); + expect(result.organizations).toHaveLength(0); }); it("should use session DID in query", async () => { mockSession.fetchHandler.mockResolvedValue({ ok: true, - json: async () => ({ repositories: [] }), + json: async () => ({ organizations: [] }), }); await orgOps.list(); @@ -197,7 +195,7 @@ describe("OrganizationOperationsImpl", () => { mockSession.did = undefined; mockSession.fetchHandler.mockResolvedValue({ ok: true, - json: async () => ({ repositories: [] }), + json: async () => ({ organizations: [] }), }); await orgOps.list(); @@ -221,7 +219,7 @@ describe("OrganizationOperationsImpl", () => { mockSession.fetchHandler.mockResolvedValue({ ok: true, json: async () => ({ - repositories: [ + organizations: [ { did: "did:plc:org", handle: "org.example.com", @@ -236,7 +234,7 @@ describe("OrganizationOperationsImpl", () => { const result = await orgOps.list(); - expect(result[0].createdAt).toBeDefined(); + expect(result.organizations[0].createdAt).toBeDefined(); }); }); }); diff --git a/packages/sdk-react/package.json b/packages/sdk-react/package.json index f262624..ffc6876 100644 --- a/packages/sdk-react/package.json +++ b/packages/sdk-react/package.json @@ -1,6 +1,6 @@ { "name": "@hypercerts-org/sdk-react", - "version": "0.3.0", + "version": "0.4.0", "description": "React hooks and components for the Hypercerts ATProto SDK", "type": "module", "main": "dist/index.cjs", diff --git a/packages/sdk-react/src/hooks/useCollaborators.ts b/packages/sdk-react/src/hooks/useCollaborators.ts index b9b8002..a269233 100644 --- a/packages/sdk-react/src/hooks/useCollaborators.ts +++ b/packages/sdk-react/src/hooks/useCollaborators.ts @@ -101,7 +101,7 @@ export function useCollaborators(repoDid: string): UseCollaboratorsResult { throw new SDSRequiredError("Collaborator management requires a Shared Data Server (SDS)"); } - const grants = await repository.collaborators.list(); + const { collaborators: grants } = await repository.collaborators.list(); // Transform RepositoryAccessGrant[] to Collaborator[] // Use permissions directly from the grant (authoritative source) diff --git a/packages/sdk-react/src/hooks/useOrganizations.ts b/packages/sdk-react/src/hooks/useOrganizations.ts index cfca4d4..a3f846a 100644 --- a/packages/sdk-react/src/hooks/useOrganizations.ts +++ b/packages/sdk-react/src/hooks/useOrganizations.ts @@ -10,11 +10,7 @@ import type { OrganizationInfo } from "@hypercerts-org/sdk-core"; import { atprotoKeys } from "../queries/keys.js"; import { useATProtoAuth } from "./useATProtoAuth.js"; import { useRepository } from "./useRepository.js"; -import type { - CreateOrganizationParams, - UseOrganizationsResult, - UseOrganizationResult, -} from "../types.js"; +import type { CreateOrganizationParams, UseOrganizationsResult, UseOrganizationResult } from "../types.js"; /** * Hook for listing and creating SDS organizations. @@ -77,7 +73,7 @@ export function useOrganizations(): UseOrganizationsResult { queryFn: async (): Promise => { if (!repository) return []; - const orgs = await repository.organizations.list(); + const { organizations: orgs } = await repository.organizations.list(); return orgs; }, enabled: authStatus === "authenticated" && repoStatus === "ready" && !!repository, @@ -112,7 +108,7 @@ export function useOrganizations(): UseOrganizationsResult { async (params: CreateOrganizationParams) => { return createMutation.mutateAsync(params); }, - [createMutation] + [createMutation], ); const refetch = useCallback(async () => { diff --git a/packages/sdk-react/tests/hooks/useCollaborators.test.tsx b/packages/sdk-react/tests/hooks/useCollaborators.test.tsx index 59ee670..28026c0 100644 --- a/packages/sdk-react/tests/hooks/useCollaborators.test.tsx +++ b/packages/sdk-react/tests/hooks/useCollaborators.test.tsx @@ -13,12 +13,14 @@ import { atprotoKeys } from "../../src/queries/keys.js"; /** * Create a mock collaborator grant for testing. */ -function createMockGrant(overrides: { - userDid?: string; - role?: "viewer" | "editor" | "admin" | "owner"; - grantedBy?: string; - grantedAt?: string; -} = {}) { +function createMockGrant( + overrides: { + userDid?: string; + role?: "viewer" | "editor" | "admin" | "owner"; + grantedBy?: string; + grantedAt?: string; + } = {}, +) { const role = overrides.role ?? "viewer"; return { userDid: overrides.userDid ?? "did:plc:collaborator123", @@ -41,7 +43,7 @@ describe("useCollaborators", () => { }); queryClient.setQueryData(atprotoKeys.session(), session); - const mockList = vi.fn().mockResolvedValue([]); + const mockList = vi.fn().mockResolvedValue({ collaborators: [] }); const mockRepo = { collaborators: { list: mockList, grant: vi.fn(), revoke: vi.fn() } }; const mockRepository = vi.fn().mockReturnValue(mockRepo); @@ -76,7 +78,7 @@ describe("useCollaborators", () => { createMockGrant({ userDid: "did:plc:user2", role: "editor" }), createMockGrant({ userDid: "did:plc:user3", role: "viewer" }), ]; - const mockList = vi.fn().mockResolvedValue(mockGrants); + const mockList = vi.fn().mockResolvedValue({ collaborators: mockGrants }); const mockRepo = { collaborators: { list: mockList, grant: vi.fn(), revoke: vi.fn() } }; const mockRepository = vi.fn().mockReturnValue(mockRepo); @@ -118,7 +120,7 @@ describe("useCollaborators", () => { }); queryClient.setQueryData(atprotoKeys.session(), session); - const mockList = vi.fn().mockResolvedValue([]); + const mockList = vi.fn().mockResolvedValue({ collaborators: [] }); const mockGrant = vi.fn().mockResolvedValue(undefined); const mockRepo = { collaborators: { list: mockList, grant: mockGrant, revoke: vi.fn() } }; const mockRepository = vi.fn().mockReturnValue(mockRepo); @@ -146,7 +148,7 @@ describe("useCollaborators", () => { }); queryClient.setQueryData(atprotoKeys.session(), session); - const mockList = vi.fn().mockResolvedValue([]); + const mockList = vi.fn().mockResolvedValue({ collaborators: [] }); const mockGrant = vi.fn().mockResolvedValue(undefined); const mockRepo = { collaborators: { list: mockList, grant: mockGrant, revoke: vi.fn() } }; const mockRepository = vi.fn().mockReturnValue(mockRepo); @@ -189,7 +191,7 @@ describe("useCollaborators", () => { queryClient.setQueryData(atprotoKeys.session(), session); let resolveGrant: () => void; - const mockList = vi.fn().mockResolvedValue([]); + const mockList = vi.fn().mockResolvedValue({ collaborators: [] }); const mockGrant = vi.fn().mockImplementation( () => new Promise((resolve) => { @@ -244,7 +246,7 @@ describe("useCollaborators", () => { }); queryClient.setQueryData(atprotoKeys.session(), session); - const mockList = vi.fn().mockResolvedValue([]); + const mockList = vi.fn().mockResolvedValue({ collaborators: [] }); const mockRevoke = vi.fn().mockResolvedValue(undefined); const mockRepo = { collaborators: { list: mockList, grant: vi.fn(), revoke: mockRevoke } }; const mockRepository = vi.fn().mockReturnValue(mockRepo); @@ -272,7 +274,7 @@ describe("useCollaborators", () => { }); queryClient.setQueryData(atprotoKeys.session(), session); - const mockList = vi.fn().mockResolvedValue([]); + const mockList = vi.fn().mockResolvedValue({ collaborators: [] }); const mockRevoke = vi.fn().mockResolvedValue(undefined); const mockRepo = { collaborators: { list: mockList, grant: vi.fn(), revoke: mockRevoke } }; const mockRepository = vi.fn().mockReturnValue(mockRepo); @@ -311,7 +313,7 @@ describe("useCollaborators", () => { queryClient.setQueryData(atprotoKeys.session(), session); let resolveRevoke: () => void; - const mockList = vi.fn().mockResolvedValue([]); + const mockList = vi.fn().mockResolvedValue({ collaborators: [] }); const mockRevoke = vi.fn().mockImplementation( () => new Promise((resolve) => { @@ -366,7 +368,7 @@ describe("useCollaborators", () => { }); queryClient.setQueryData(atprotoKeys.session(), session); - const mockList = vi.fn().mockResolvedValue([]); + const mockList = vi.fn().mockResolvedValue({ collaborators: [] }); const mockRepo = { collaborators: { list: mockList, grant: vi.fn(), revoke: vi.fn() } }; const mockRepository = vi.fn().mockReturnValue(mockRepo); diff --git a/packages/sdk-react/tests/hooks/useOrganizations.test.tsx b/packages/sdk-react/tests/hooks/useOrganizations.test.tsx index 67fb20e..db6cd2a 100644 --- a/packages/sdk-react/tests/hooks/useOrganizations.test.tsx +++ b/packages/sdk-react/tests/hooks/useOrganizations.test.tsx @@ -23,7 +23,7 @@ describe("useOrganizations", () => { }); queryClient.setQueryData(atprotoKeys.session(), session); - const mockList = vi.fn().mockResolvedValue([]); + const mockList = vi.fn().mockResolvedValue({ organizations: [] }); const mockRepo = { organizations: { list: mockList, create: vi.fn(), get: vi.fn() } }; const mockRepository = vi.fn().mockReturnValue(mockRepo); @@ -57,7 +57,7 @@ describe("useOrganizations", () => { createMockOrganization({ did: "did:plc:org1", name: "Org 1" }), createMockOrganization({ did: "did:plc:org2", name: "Org 2" }), ]; - const mockList = vi.fn().mockResolvedValue(mockOrgs); + const mockList = vi.fn().mockResolvedValue({ organizations: mockOrgs }); const mockRepo = { organizations: { list: mockList, create: vi.fn(), get: vi.fn() } }; const mockRepository = vi.fn().mockReturnValue(mockRepo); @@ -108,7 +108,7 @@ describe("useOrganizations", () => { }); queryClient.setQueryData(atprotoKeys.session(), session); - const mockList = vi.fn().mockResolvedValue([]); + const mockList = vi.fn().mockResolvedValue({ organizations: [] }); const mockCreate = vi.fn().mockResolvedValue(createMockOrganization()); const mockRepo = { organizations: { list: mockList, create: mockCreate, get: vi.fn() } }; const mockRepository = vi.fn().mockReturnValue(mockRepo); @@ -137,7 +137,7 @@ describe("useOrganizations", () => { queryClient.setQueryData(atprotoKeys.session(), session); const newOrg = createMockOrganization({ name: "New Org" }); - const mockList = vi.fn().mockResolvedValue([]); + const mockList = vi.fn().mockResolvedValue({ organizations: [] }); const mockCreate = vi.fn().mockResolvedValue(newOrg); const mockRepo = { organizations: { list: mockList, create: mockCreate, get: vi.fn() } }; const mockRepository = vi.fn().mockReturnValue(mockRepo); @@ -185,7 +185,7 @@ describe("useOrganizations", () => { queryClient.setQueryData(atprotoKeys.session(), session); let resolveCreate: (org: ReturnType) => void; - const mockList = vi.fn().mockResolvedValue([]); + const mockList = vi.fn().mockResolvedValue({ organizations: [] }); const mockCreate = vi.fn().mockImplementation( () => new Promise((resolve) => { @@ -240,7 +240,7 @@ describe("useOrganizations", () => { }); queryClient.setQueryData(atprotoKeys.session(), session); - const mockList = vi.fn().mockResolvedValue([]); + const mockList = vi.fn().mockResolvedValue({ organizations: [] }); const mockRepo = { organizations: { list: mockList, create: vi.fn(), get: vi.fn() } }; const mockRepository = vi.fn().mockReturnValue(mockRepo);