@hypercerts-org/sdk-core 0.2.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +328 -0
- package/.turbo/turbo-test.log +118 -0
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/errors.cjs +260 -0
- package/dist/errors.cjs.map +1 -0
- package/dist/errors.d.ts +233 -0
- package/dist/errors.mjs +253 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.cjs +4531 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +3430 -0
- package/dist/index.mjs +4448 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lexicons.cjs +420 -0
- package/dist/lexicons.cjs.map +1 -0
- package/dist/lexicons.d.ts +227 -0
- package/dist/lexicons.mjs +410 -0
- package/dist/lexicons.mjs.map +1 -0
- package/dist/storage.cjs +270 -0
- package/dist/storage.cjs.map +1 -0
- package/dist/storage.d.ts +474 -0
- package/dist/storage.mjs +267 -0
- package/dist/storage.mjs.map +1 -0
- package/dist/testing.cjs +415 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.ts +928 -0
- package/dist/testing.mjs +410 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/types.cjs +220 -0
- package/dist/types.cjs.map +1 -0
- package/dist/types.d.ts +2118 -0
- package/dist/types.mjs +212 -0
- package/dist/types.mjs.map +1 -0
- package/eslint.config.mjs +22 -0
- package/package.json +90 -0
- package/rollup.config.js +75 -0
- package/src/auth/OAuthClient.ts +497 -0
- package/src/core/SDK.ts +410 -0
- package/src/core/config.ts +243 -0
- package/src/core/errors.ts +257 -0
- package/src/core/interfaces.ts +324 -0
- package/src/core/types.ts +281 -0
- package/src/errors.ts +57 -0
- package/src/index.ts +107 -0
- package/src/lexicons.ts +64 -0
- package/src/repository/BlobOperationsImpl.ts +199 -0
- package/src/repository/CollaboratorOperationsImpl.ts +288 -0
- package/src/repository/HypercertOperationsImpl.ts +1146 -0
- package/src/repository/LexiconRegistry.ts +332 -0
- package/src/repository/OrganizationOperationsImpl.ts +234 -0
- package/src/repository/ProfileOperationsImpl.ts +281 -0
- package/src/repository/RecordOperationsImpl.ts +340 -0
- package/src/repository/Repository.ts +482 -0
- package/src/repository/interfaces.ts +868 -0
- package/src/repository/types.ts +111 -0
- package/src/services/hypercerts/types.ts +87 -0
- package/src/storage/InMemorySessionStore.ts +127 -0
- package/src/storage/InMemoryStateStore.ts +146 -0
- package/src/storage.ts +63 -0
- package/src/testing/index.ts +67 -0
- package/src/testing/mocks.ts +142 -0
- package/src/testing/stores.ts +285 -0
- package/src/testing.ts +64 -0
- package/src/types.ts +86 -0
- package/tests/auth/OAuthClient.test.ts +164 -0
- package/tests/core/SDK.test.ts +176 -0
- package/tests/core/errors.test.ts +81 -0
- package/tests/repository/BlobOperationsImpl.test.ts +154 -0
- package/tests/repository/CollaboratorOperationsImpl.test.ts +323 -0
- package/tests/repository/HypercertOperationsImpl.test.ts +652 -0
- package/tests/repository/LexiconRegistry.test.ts +192 -0
- package/tests/repository/OrganizationOperationsImpl.test.ts +242 -0
- package/tests/repository/ProfileOperationsImpl.test.ts +254 -0
- package/tests/repository/RecordOperationsImpl.test.ts +375 -0
- package/tests/repository/Repository.test.ts +149 -0
- package/tests/utils/fixtures.ts +117 -0
- package/tests/utils/mocks.ts +109 -0
- package/tests/utils/repository-fixtures.ts +78 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +30 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProfileOperationsImpl - User profile operations.
|
|
3
|
+
*
|
|
4
|
+
* This module provides the implementation for AT Protocol profile
|
|
5
|
+
* management, including fetching and updating user profiles.
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Agent } from "@atproto/api";
|
|
11
|
+
import { NetworkError } from "../core/errors.js";
|
|
12
|
+
import type { ProfileOperations } from "./interfaces.js";
|
|
13
|
+
import type { UpdateResult } from "./types.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Implementation of profile operations for user profile management.
|
|
17
|
+
*
|
|
18
|
+
* Profiles in AT Protocol are stored as records in the `app.bsky.actor.profile`
|
|
19
|
+
* collection with the special rkey "self". This class provides a convenient
|
|
20
|
+
* API for reading and updating profile data.
|
|
21
|
+
*
|
|
22
|
+
* @remarks
|
|
23
|
+
* This class is typically not instantiated directly. Access it through
|
|
24
|
+
* {@link Repository.profile}.
|
|
25
|
+
*
|
|
26
|
+
* **Profile Fields**:
|
|
27
|
+
* - `handle`: Read-only, managed by the PDS
|
|
28
|
+
* - `displayName`: User's display name (max 64 chars typically)
|
|
29
|
+
* - `description`: Profile bio (max 256 chars typically)
|
|
30
|
+
* - `avatar`: Profile picture blob reference
|
|
31
|
+
* - `banner`: Banner image blob reference
|
|
32
|
+
* - `website`: User's website URL (may not be available on all servers)
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* // Get profile
|
|
37
|
+
* const profile = await repo.profile.get();
|
|
38
|
+
* console.log(`${profile.displayName} (@${profile.handle})`);
|
|
39
|
+
*
|
|
40
|
+
* // Update profile
|
|
41
|
+
* await repo.profile.update({
|
|
42
|
+
* displayName: "New Name",
|
|
43
|
+
* description: "Updated bio",
|
|
44
|
+
* });
|
|
45
|
+
*
|
|
46
|
+
* // Update with new avatar
|
|
47
|
+
* const avatarBlob = new Blob([imageData], { type: "image/png" });
|
|
48
|
+
* await repo.profile.update({ avatar: avatarBlob });
|
|
49
|
+
*
|
|
50
|
+
* // Remove a field
|
|
51
|
+
* await repo.profile.update({ website: null });
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* @internal
|
|
55
|
+
*/
|
|
56
|
+
export class ProfileOperationsImpl implements ProfileOperations {
|
|
57
|
+
/**
|
|
58
|
+
* Creates a new ProfileOperationsImpl.
|
|
59
|
+
*
|
|
60
|
+
* @param agent - AT Protocol Agent for making API calls
|
|
61
|
+
* @param repoDid - DID of the repository/user
|
|
62
|
+
* @param _serverUrl - Server URL (reserved for future use)
|
|
63
|
+
*
|
|
64
|
+
* @internal
|
|
65
|
+
*/
|
|
66
|
+
constructor(
|
|
67
|
+
private agent: Agent,
|
|
68
|
+
private repoDid: string,
|
|
69
|
+
private _serverUrl: string,
|
|
70
|
+
) {}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Gets the repository's profile.
|
|
74
|
+
*
|
|
75
|
+
* @returns Promise resolving to profile data
|
|
76
|
+
* @throws {@link NetworkError} if the profile cannot be fetched
|
|
77
|
+
*
|
|
78
|
+
* @remarks
|
|
79
|
+
* This method fetches the full profile using the `getProfile` API,
|
|
80
|
+
* which includes resolved information like follower counts on some
|
|
81
|
+
* servers. For hypercerts SDK usage, the basic profile fields are
|
|
82
|
+
* returned.
|
|
83
|
+
*
|
|
84
|
+
* **Note**: The `website` field may not be available on all AT Protocol
|
|
85
|
+
* servers. Standard Bluesky profiles don't include this field.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```typescript
|
|
89
|
+
* const profile = await repo.profile.get();
|
|
90
|
+
*
|
|
91
|
+
* console.log(`Handle: @${profile.handle}`);
|
|
92
|
+
* console.log(`Name: ${profile.displayName || "(not set)"}`);
|
|
93
|
+
* console.log(`Bio: ${profile.description || "(no bio)"}`);
|
|
94
|
+
*
|
|
95
|
+
* if (profile.avatar) {
|
|
96
|
+
* // Avatar is a URL or blob reference
|
|
97
|
+
* console.log(`Avatar: ${profile.avatar}`);
|
|
98
|
+
* }
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
async get(): Promise<{
|
|
102
|
+
handle: string;
|
|
103
|
+
displayName?: string;
|
|
104
|
+
description?: string;
|
|
105
|
+
avatar?: string;
|
|
106
|
+
banner?: string;
|
|
107
|
+
website?: string;
|
|
108
|
+
}> {
|
|
109
|
+
try {
|
|
110
|
+
const result = await this.agent.getProfile({ actor: this.repoDid });
|
|
111
|
+
|
|
112
|
+
if (!result.success) {
|
|
113
|
+
throw new NetworkError("Failed to get profile");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
handle: result.data.handle,
|
|
118
|
+
displayName: result.data.displayName,
|
|
119
|
+
description: result.data.description,
|
|
120
|
+
avatar: result.data.avatar,
|
|
121
|
+
banner: result.data.banner,
|
|
122
|
+
// Note: website may not be available in standard profile
|
|
123
|
+
};
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (error instanceof NetworkError) throw error;
|
|
126
|
+
throw new NetworkError(
|
|
127
|
+
`Failed to get profile: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
128
|
+
error,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Updates the repository's profile.
|
|
135
|
+
*
|
|
136
|
+
* @param params - Fields to update. Pass `null` to remove a field.
|
|
137
|
+
* Omitted fields are preserved from the existing profile.
|
|
138
|
+
* @returns Promise resolving to update result with new URI and CID
|
|
139
|
+
* @throws {@link NetworkError} if the update fails
|
|
140
|
+
*
|
|
141
|
+
* @remarks
|
|
142
|
+
* This method performs a read-modify-write operation:
|
|
143
|
+
* 1. Fetches the existing profile record
|
|
144
|
+
* 2. Merges in the provided updates
|
|
145
|
+
* 3. Writes the updated profile back
|
|
146
|
+
*
|
|
147
|
+
* **Image Handling**: When providing `avatar` or `banner` as a Blob,
|
|
148
|
+
* the image is automatically uploaded and the blob reference is stored
|
|
149
|
+
* in the profile.
|
|
150
|
+
*
|
|
151
|
+
* **Field Removal**: Pass `null` to explicitly remove a field. Omitting
|
|
152
|
+
* a field (not including it in params) preserves the existing value.
|
|
153
|
+
*
|
|
154
|
+
* @example Update display name and bio
|
|
155
|
+
* ```typescript
|
|
156
|
+
* await repo.profile.update({
|
|
157
|
+
* displayName: "Alice",
|
|
158
|
+
* description: "Building impact certificates",
|
|
159
|
+
* });
|
|
160
|
+
* ```
|
|
161
|
+
*
|
|
162
|
+
* @example Update avatar image
|
|
163
|
+
* ```typescript
|
|
164
|
+
* // From a file input
|
|
165
|
+
* const file = document.getElementById("avatar").files[0];
|
|
166
|
+
* await repo.profile.update({ avatar: file });
|
|
167
|
+
*
|
|
168
|
+
* // From raw data
|
|
169
|
+
* const response = await fetch("https://example.com/my-avatar.png");
|
|
170
|
+
* const blob = await response.blob();
|
|
171
|
+
* await repo.profile.update({ avatar: blob });
|
|
172
|
+
* ```
|
|
173
|
+
*
|
|
174
|
+
* @example Remove description
|
|
175
|
+
* ```typescript
|
|
176
|
+
* // Removes the description field entirely
|
|
177
|
+
* await repo.profile.update({ description: null });
|
|
178
|
+
* ```
|
|
179
|
+
*
|
|
180
|
+
* @example Multiple updates at once
|
|
181
|
+
* ```typescript
|
|
182
|
+
* const newAvatar = new Blob([avatarData], { type: "image/png" });
|
|
183
|
+
* const newBanner = new Blob([bannerData], { type: "image/jpeg" });
|
|
184
|
+
*
|
|
185
|
+
* await repo.profile.update({
|
|
186
|
+
* displayName: "New Name",
|
|
187
|
+
* description: "New bio",
|
|
188
|
+
* avatar: newAvatar,
|
|
189
|
+
* banner: newBanner,
|
|
190
|
+
* });
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
async update(params: {
|
|
194
|
+
displayName?: string | null;
|
|
195
|
+
description?: string | null;
|
|
196
|
+
avatar?: Blob | null;
|
|
197
|
+
banner?: Blob | null;
|
|
198
|
+
website?: string | null;
|
|
199
|
+
}): Promise<UpdateResult> {
|
|
200
|
+
try {
|
|
201
|
+
// Get existing profile record
|
|
202
|
+
const existing = await this.agent.com.atproto.repo.getRecord({
|
|
203
|
+
repo: this.repoDid,
|
|
204
|
+
collection: "app.bsky.actor.profile",
|
|
205
|
+
rkey: "self",
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const existingProfile = (existing.data.value as Record<string, unknown>) || {};
|
|
209
|
+
|
|
210
|
+
// Build updated profile
|
|
211
|
+
const updatedProfile: Record<string, unknown> = { ...existingProfile };
|
|
212
|
+
|
|
213
|
+
if (params.displayName !== undefined) {
|
|
214
|
+
if (params.displayName === null) {
|
|
215
|
+
delete updatedProfile.displayName;
|
|
216
|
+
} else {
|
|
217
|
+
updatedProfile.displayName = params.displayName;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (params.description !== undefined) {
|
|
222
|
+
if (params.description === null) {
|
|
223
|
+
delete updatedProfile.description;
|
|
224
|
+
} else {
|
|
225
|
+
updatedProfile.description = params.description;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Handle avatar upload
|
|
230
|
+
if (params.avatar !== undefined) {
|
|
231
|
+
if (params.avatar === null) {
|
|
232
|
+
delete updatedProfile.avatar;
|
|
233
|
+
} else {
|
|
234
|
+
const arrayBuffer = await params.avatar.arrayBuffer();
|
|
235
|
+
const uint8Array = new Uint8Array(arrayBuffer);
|
|
236
|
+
const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
|
|
237
|
+
encoding: params.avatar.type || "image/jpeg",
|
|
238
|
+
});
|
|
239
|
+
if (uploadResult.success) {
|
|
240
|
+
updatedProfile.avatar = uploadResult.data.blob;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Handle banner upload
|
|
246
|
+
if (params.banner !== undefined) {
|
|
247
|
+
if (params.banner === null) {
|
|
248
|
+
delete updatedProfile.banner;
|
|
249
|
+
} else {
|
|
250
|
+
const arrayBuffer = await params.banner.arrayBuffer();
|
|
251
|
+
const uint8Array = new Uint8Array(arrayBuffer);
|
|
252
|
+
const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
|
|
253
|
+
encoding: params.banner.type || "image/jpeg",
|
|
254
|
+
});
|
|
255
|
+
if (uploadResult.success) {
|
|
256
|
+
updatedProfile.banner = uploadResult.data.blob;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const result = await this.agent.com.atproto.repo.putRecord({
|
|
262
|
+
repo: this.repoDid,
|
|
263
|
+
collection: "app.bsky.actor.profile",
|
|
264
|
+
rkey: "self",
|
|
265
|
+
record: updatedProfile,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
if (!result.success) {
|
|
269
|
+
throw new NetworkError("Failed to update profile");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return { uri: result.data.uri, cid: result.data.cid };
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (error instanceof NetworkError) throw error;
|
|
275
|
+
throw new NetworkError(
|
|
276
|
+
`Failed to update profile: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
277
|
+
error,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RecordOperationsImpl - Low-level record CRUD operations.
|
|
3
|
+
*
|
|
4
|
+
* This module provides the implementation for direct AT Protocol
|
|
5
|
+
* record operations (create, read, update, delete, list).
|
|
6
|
+
*
|
|
7
|
+
* @packageDocumentation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Agent } from "@atproto/api";
|
|
11
|
+
import { NetworkError, ValidationError } from "../core/errors.js";
|
|
12
|
+
import type { LexiconRegistry } from "./LexiconRegistry.js";
|
|
13
|
+
import type { RecordOperations } from "./interfaces.js";
|
|
14
|
+
import type { CreateResult, UpdateResult, PaginatedList } from "./types.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Implementation of low-level AT Protocol record operations.
|
|
18
|
+
*
|
|
19
|
+
* This class provides direct access to the AT Protocol repository API
|
|
20
|
+
* for CRUD operations on records. It handles:
|
|
21
|
+
*
|
|
22
|
+
* - Lexicon validation before create/update operations
|
|
23
|
+
* - Error mapping to SDK error types
|
|
24
|
+
* - Response normalization
|
|
25
|
+
*
|
|
26
|
+
* @remarks
|
|
27
|
+
* This class is typically not instantiated directly. Access it through
|
|
28
|
+
* {@link Repository.records}.
|
|
29
|
+
*
|
|
30
|
+
* All operations are performed against the repository DID specified
|
|
31
|
+
* at construction time. To operate on a different repository, create
|
|
32
|
+
* a new Repository instance using {@link Repository.repo}.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* // Access through Repository
|
|
37
|
+
* const repo = sdk.repository(session);
|
|
38
|
+
*
|
|
39
|
+
* // Create a record
|
|
40
|
+
* const { uri, cid } = await repo.records.create({
|
|
41
|
+
* collection: "org.example.myRecord",
|
|
42
|
+
* record: { title: "Hello", createdAt: new Date().toISOString() },
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* // Update the record
|
|
46
|
+
* const rkey = uri.split("/").pop()!;
|
|
47
|
+
* await repo.records.update({
|
|
48
|
+
* collection: "org.example.myRecord",
|
|
49
|
+
* rkey,
|
|
50
|
+
* record: { title: "Updated", createdAt: new Date().toISOString() },
|
|
51
|
+
* });
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* @internal
|
|
55
|
+
*/
|
|
56
|
+
export class RecordOperationsImpl implements RecordOperations {
|
|
57
|
+
/**
|
|
58
|
+
* Creates a new RecordOperationsImpl.
|
|
59
|
+
*
|
|
60
|
+
* @param agent - AT Protocol Agent for making API calls
|
|
61
|
+
* @param repoDid - DID of the repository to operate on
|
|
62
|
+
* @param lexiconRegistry - Registry for record validation
|
|
63
|
+
*
|
|
64
|
+
* @internal
|
|
65
|
+
*/
|
|
66
|
+
constructor(
|
|
67
|
+
private agent: Agent,
|
|
68
|
+
private repoDid: string,
|
|
69
|
+
private lexiconRegistry: LexiconRegistry,
|
|
70
|
+
) {}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Creates a new record in the specified collection.
|
|
74
|
+
*
|
|
75
|
+
* @param params - Creation parameters
|
|
76
|
+
* @param params.collection - NSID of the collection (e.g., "org.hypercerts.hypercert")
|
|
77
|
+
* @param params.record - Record data conforming to the collection's lexicon schema
|
|
78
|
+
* @param params.rkey - Optional record key. If not provided, a TID (timestamp-based ID)
|
|
79
|
+
* is automatically generated by the server.
|
|
80
|
+
* @returns Promise resolving to the created record's URI and CID
|
|
81
|
+
* @throws {@link ValidationError} if the record doesn't conform to the lexicon schema
|
|
82
|
+
* @throws {@link NetworkError} if the API request fails
|
|
83
|
+
*
|
|
84
|
+
* @remarks
|
|
85
|
+
* The record is validated against the collection's lexicon before sending
|
|
86
|
+
* to the server. If no lexicon is registered for the collection, validation
|
|
87
|
+
* is skipped (allowing custom record types).
|
|
88
|
+
*
|
|
89
|
+
* **AT-URI Format**: `at://{did}/{collection}/{rkey}`
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```typescript
|
|
93
|
+
* const result = await repo.records.create({
|
|
94
|
+
* collection: "org.hypercerts.hypercert",
|
|
95
|
+
* record: {
|
|
96
|
+
* title: "My Hypercert",
|
|
97
|
+
* description: "...",
|
|
98
|
+
* createdAt: new Date().toISOString(),
|
|
99
|
+
* },
|
|
100
|
+
* });
|
|
101
|
+
* console.log(`Created: ${result.uri}`);
|
|
102
|
+
* // Output: Created: at://did:plc:abc123/org.hypercerts.hypercert/xyz789
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
async create(params: { collection: string; record: unknown; rkey?: string }): Promise<CreateResult> {
|
|
106
|
+
const validation = this.lexiconRegistry.validate(params.collection, params.record);
|
|
107
|
+
if (!validation.valid) {
|
|
108
|
+
throw new ValidationError(`Invalid record for collection ${params.collection}: ${validation.error}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const result = await this.agent.com.atproto.repo.createRecord({
|
|
113
|
+
repo: this.repoDid,
|
|
114
|
+
collection: params.collection,
|
|
115
|
+
record: params.record as Record<string, unknown>,
|
|
116
|
+
rkey: params.rkey,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (!result.success) {
|
|
120
|
+
throw new NetworkError("Failed to create record");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { uri: result.data.uri, cid: result.data.cid };
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (error instanceof ValidationError || error instanceof NetworkError) throw error;
|
|
126
|
+
throw new NetworkError(
|
|
127
|
+
`Failed to create record: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
128
|
+
error,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Updates an existing record (full replacement).
|
|
135
|
+
*
|
|
136
|
+
* @param params - Update parameters
|
|
137
|
+
* @param params.collection - NSID of the collection
|
|
138
|
+
* @param params.rkey - Record key (the last segment of the AT-URI)
|
|
139
|
+
* @param params.record - New record data (completely replaces existing record)
|
|
140
|
+
* @returns Promise resolving to the updated record's URI and new CID
|
|
141
|
+
* @throws {@link ValidationError} if the record doesn't conform to the lexicon schema
|
|
142
|
+
* @throws {@link NetworkError} if the API request fails
|
|
143
|
+
*
|
|
144
|
+
* @remarks
|
|
145
|
+
* This is a full replacement operation, not a partial update. The entire
|
|
146
|
+
* record is replaced with the new data. To preserve existing fields,
|
|
147
|
+
* first fetch the record with {@link get}, modify it, then update.
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* // Get existing record
|
|
152
|
+
* const existing = await repo.records.get({
|
|
153
|
+
* collection: "org.hypercerts.hypercert",
|
|
154
|
+
* rkey: "xyz789",
|
|
155
|
+
* });
|
|
156
|
+
*
|
|
157
|
+
* // Update with modified data
|
|
158
|
+
* const updated = await repo.records.update({
|
|
159
|
+
* collection: "org.hypercerts.hypercert",
|
|
160
|
+
* rkey: "xyz789",
|
|
161
|
+
* record: {
|
|
162
|
+
* ...existing.value,
|
|
163
|
+
* title: "Updated Title",
|
|
164
|
+
* },
|
|
165
|
+
* });
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
async update(params: { collection: string; rkey: string; record: unknown }): Promise<UpdateResult> {
|
|
169
|
+
const validation = this.lexiconRegistry.validate(params.collection, params.record);
|
|
170
|
+
if (!validation.valid) {
|
|
171
|
+
throw new ValidationError(`Invalid record for collection ${params.collection}: ${validation.error}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const result = await this.agent.com.atproto.repo.putRecord({
|
|
176
|
+
repo: this.repoDid,
|
|
177
|
+
collection: params.collection,
|
|
178
|
+
rkey: params.rkey,
|
|
179
|
+
record: params.record as Record<string, unknown>,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (!result.success) {
|
|
183
|
+
throw new NetworkError("Failed to update record");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { uri: result.data.uri, cid: result.data.cid };
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (error instanceof ValidationError || error instanceof NetworkError) throw error;
|
|
189
|
+
throw new NetworkError(
|
|
190
|
+
`Failed to update record: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
191
|
+
error,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Gets a single record by collection and key.
|
|
198
|
+
*
|
|
199
|
+
* @param params - Get parameters
|
|
200
|
+
* @param params.collection - NSID of the collection
|
|
201
|
+
* @param params.rkey - Record key
|
|
202
|
+
* @returns Promise resolving to the record's URI, CID, and value
|
|
203
|
+
* @throws {@link NetworkError} if the record is not found or request fails
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* ```typescript
|
|
207
|
+
* const record = await repo.records.get({
|
|
208
|
+
* collection: "org.hypercerts.hypercert",
|
|
209
|
+
* rkey: "xyz789",
|
|
210
|
+
* });
|
|
211
|
+
*
|
|
212
|
+
* console.log(record.uri); // at://did:plc:abc123/org.hypercerts.hypercert/xyz789
|
|
213
|
+
* console.log(record.cid); // bafyrei...
|
|
214
|
+
* console.log(record.value); // { title: "...", description: "...", ... }
|
|
215
|
+
* ```
|
|
216
|
+
*/
|
|
217
|
+
async get(params: { collection: string; rkey: string }): Promise<{ uri: string; cid: string; value: unknown }> {
|
|
218
|
+
try {
|
|
219
|
+
const result = await this.agent.com.atproto.repo.getRecord({
|
|
220
|
+
repo: this.repoDid,
|
|
221
|
+
collection: params.collection,
|
|
222
|
+
rkey: params.rkey,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (!result.success) {
|
|
226
|
+
throw new NetworkError("Failed to get record");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { uri: result.data.uri, cid: result.data.cid ?? "", value: result.data.value };
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (error instanceof NetworkError) throw error;
|
|
232
|
+
throw new NetworkError(
|
|
233
|
+
`Failed to get record: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
234
|
+
error,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Lists records in a collection with pagination.
|
|
241
|
+
*
|
|
242
|
+
* @param params - List parameters
|
|
243
|
+
* @param params.collection - NSID of the collection
|
|
244
|
+
* @param params.limit - Maximum number of records to return (server may impose its own limit)
|
|
245
|
+
* @param params.cursor - Pagination cursor from a previous response
|
|
246
|
+
* @returns Promise resolving to paginated list of records
|
|
247
|
+
* @throws {@link NetworkError} if the request fails
|
|
248
|
+
*
|
|
249
|
+
* @remarks
|
|
250
|
+
* Records are returned in reverse chronological order (newest first).
|
|
251
|
+
* Use the `cursor` from the response to fetch subsequent pages.
|
|
252
|
+
*
|
|
253
|
+
* @example Paginating through all records
|
|
254
|
+
* ```typescript
|
|
255
|
+
* let cursor: string | undefined;
|
|
256
|
+
* const allRecords = [];
|
|
257
|
+
*
|
|
258
|
+
* do {
|
|
259
|
+
* const page = await repo.records.list({
|
|
260
|
+
* collection: "org.hypercerts.hypercert",
|
|
261
|
+
* limit: 100,
|
|
262
|
+
* cursor,
|
|
263
|
+
* });
|
|
264
|
+
* allRecords.push(...page.records);
|
|
265
|
+
* cursor = page.cursor;
|
|
266
|
+
* } while (cursor);
|
|
267
|
+
*
|
|
268
|
+
* console.log(`Total records: ${allRecords.length}`);
|
|
269
|
+
* ```
|
|
270
|
+
*/
|
|
271
|
+
async list(params: {
|
|
272
|
+
collection: string;
|
|
273
|
+
limit?: number;
|
|
274
|
+
cursor?: string;
|
|
275
|
+
}): Promise<PaginatedList<{ uri: string; cid: string; value: unknown }>> {
|
|
276
|
+
try {
|
|
277
|
+
const result = await this.agent.com.atproto.repo.listRecords({
|
|
278
|
+
repo: this.repoDid,
|
|
279
|
+
collection: params.collection,
|
|
280
|
+
limit: params.limit,
|
|
281
|
+
cursor: params.cursor,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
if (!result.success) {
|
|
285
|
+
throw new NetworkError("Failed to list records");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
records: result.data.records?.map((r) => ({ uri: r.uri, cid: r.cid, value: r.value })) || [],
|
|
290
|
+
cursor: result.data.cursor ?? undefined,
|
|
291
|
+
};
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (error instanceof NetworkError) throw error;
|
|
294
|
+
throw new NetworkError(
|
|
295
|
+
`Failed to list records: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
296
|
+
error,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Deletes a record from a collection.
|
|
303
|
+
*
|
|
304
|
+
* @param params - Delete parameters
|
|
305
|
+
* @param params.collection - NSID of the collection
|
|
306
|
+
* @param params.rkey - Record key to delete
|
|
307
|
+
* @throws {@link NetworkError} if the deletion fails
|
|
308
|
+
*
|
|
309
|
+
* @remarks
|
|
310
|
+
* Deletion is permanent. The record's AT-URI cannot be reused (the same
|
|
311
|
+
* rkey can be used for a new record, but it will have a different CID).
|
|
312
|
+
*
|
|
313
|
+
* @example
|
|
314
|
+
* ```typescript
|
|
315
|
+
* await repo.records.delete({
|
|
316
|
+
* collection: "org.hypercerts.hypercert",
|
|
317
|
+
* rkey: "xyz789",
|
|
318
|
+
* });
|
|
319
|
+
* ```
|
|
320
|
+
*/
|
|
321
|
+
async delete(params: { collection: string; rkey: string }): Promise<void> {
|
|
322
|
+
try {
|
|
323
|
+
const result = await this.agent.com.atproto.repo.deleteRecord({
|
|
324
|
+
repo: this.repoDid,
|
|
325
|
+
collection: params.collection,
|
|
326
|
+
rkey: params.rkey,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
if (!result.success) {
|
|
330
|
+
throw new NetworkError("Failed to delete record");
|
|
331
|
+
}
|
|
332
|
+
} catch (error) {
|
|
333
|
+
if (error instanceof NetworkError) throw error;
|
|
334
|
+
throw new NetworkError(
|
|
335
|
+
`Failed to delete record: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
336
|
+
error,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|