@hypercerts-org/sdk-core 0.10.0-beta.5 → 0.10.0-beta.6
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/CHANGELOG.md +73 -0
- package/README.md +178 -39
- package/dist/index.cjs +393 -268
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +197 -149
- package/dist/index.mjs +393 -268
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.ts +197 -148
- package/package.json +3 -2
package/dist/index.mjs
CHANGED
|
@@ -4197,47 +4197,25 @@ class HypercertOperationsImpl extends EventEmitter {
|
|
|
4197
4197
|
*/
|
|
4198
4198
|
async attachLocation(hypercertUri, location) {
|
|
4199
4199
|
try {
|
|
4200
|
-
if (!location.srs) {
|
|
4201
|
-
throw new ValidationError("srs (Spatial Reference System) is required. Example: 'EPSG:4326' for WGS84 coordinates, or 'http://www.opengis.net/def/crs/OGC/1.3/CRS84' for CRS84.");
|
|
4202
|
-
}
|
|
4203
4200
|
// Validate that hypercert exists (unused but confirms hypercert is valid)
|
|
4204
4201
|
await this.get(hypercertUri);
|
|
4205
|
-
const
|
|
4206
|
-
const locationData = await this.resolveUriOrBlob(location.location, "application/geo+json");
|
|
4207
|
-
const locationRecord = {
|
|
4208
|
-
$type: HYPERCERT_COLLECTIONS.LOCATION,
|
|
4209
|
-
lpVersion: location.lpVersion || "1.0",
|
|
4210
|
-
srs: location.srs,
|
|
4211
|
-
locationType: location.locationType,
|
|
4212
|
-
location: locationData,
|
|
4213
|
-
createdAt,
|
|
4214
|
-
name: location.name,
|
|
4215
|
-
description: location.description,
|
|
4216
|
-
};
|
|
4217
|
-
const validation = validate(locationRecord, HYPERCERT_COLLECTIONS.LOCATION, "main", false);
|
|
4218
|
-
if (!validation.success) {
|
|
4219
|
-
throw new ValidationError(`Invalid location record: ${validation.error?.message}`);
|
|
4220
|
-
}
|
|
4221
|
-
const result = await this.agent.com.atproto.repo.createRecord({
|
|
4222
|
-
repo: this.repoDid,
|
|
4223
|
-
collection: HYPERCERT_COLLECTIONS.LOCATION,
|
|
4224
|
-
record: locationRecord,
|
|
4225
|
-
});
|
|
4226
|
-
if (!result.success) {
|
|
4227
|
-
throw new NetworkError("Failed to attach location");
|
|
4228
|
-
}
|
|
4202
|
+
const resolvedLocation = await this.resolveLocation(location);
|
|
4229
4203
|
await this.update({
|
|
4230
4204
|
uri: hypercertUri,
|
|
4231
4205
|
updates: {
|
|
4232
4206
|
location: {
|
|
4233
4207
|
$type: "com.atproto.repo.strongRef",
|
|
4234
|
-
uri:
|
|
4235
|
-
cid:
|
|
4208
|
+
uri: resolvedLocation.uri,
|
|
4209
|
+
cid: resolvedLocation.cid,
|
|
4236
4210
|
},
|
|
4237
4211
|
},
|
|
4238
4212
|
});
|
|
4239
|
-
this.emit("locationAttached", {
|
|
4240
|
-
|
|
4213
|
+
this.emit("locationAttached", {
|
|
4214
|
+
uri: resolvedLocation.uri,
|
|
4215
|
+
cid: resolvedLocation.cid,
|
|
4216
|
+
hypercertUri,
|
|
4217
|
+
});
|
|
4218
|
+
return { uri: resolvedLocation.uri, cid: resolvedLocation.cid };
|
|
4241
4219
|
}
|
|
4242
4220
|
catch (error) {
|
|
4243
4221
|
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
@@ -4250,23 +4228,86 @@ class HypercertOperationsImpl extends EventEmitter {
|
|
|
4250
4228
|
*
|
|
4251
4229
|
* @param content - Either a URI string or a Blob to upload
|
|
4252
4230
|
* @param fallbackMimeType - MIME type to use if Blob.type is empty
|
|
4253
|
-
* @returns Promise resolving to either a URI ref or blob ref
|
|
4231
|
+
* @returns Promise resolving to either a URI ref or blob ref
|
|
4254
4232
|
* @internal
|
|
4255
4233
|
*/
|
|
4256
4234
|
async resolveUriOrBlob(content, fallbackMimeType) {
|
|
4257
4235
|
if (typeof content === "string") {
|
|
4258
|
-
|
|
4236
|
+
const uriRef = {
|
|
4259
4237
|
$type: "org.hypercerts.defs#uri",
|
|
4260
4238
|
uri: content,
|
|
4261
4239
|
};
|
|
4240
|
+
return uriRef;
|
|
4262
4241
|
}
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4242
|
+
const uploadedBlob = await this.handleBlobUpload(content, fallbackMimeType);
|
|
4243
|
+
const blobRef = {
|
|
4244
|
+
$type: "org.hypercerts.defs#smallBlob",
|
|
4245
|
+
blob: uploadedBlob,
|
|
4246
|
+
};
|
|
4247
|
+
return blobRef;
|
|
4248
|
+
}
|
|
4249
|
+
async resolveCollectionImageInput(input, isBanner = false) {
|
|
4250
|
+
if (typeof input === "string") {
|
|
4251
|
+
return { $type: "org.hypercerts.defs#uri", uri: input };
|
|
4252
|
+
}
|
|
4253
|
+
const blob = await this.handleBlobUpload(input, "image/jpeg");
|
|
4254
|
+
if (isBanner) {
|
|
4255
|
+
return { $type: "org.hypercerts.defs#largeImage", image: blob };
|
|
4256
|
+
}
|
|
4257
|
+
return { $type: "org.hypercerts.defs#smallImage", image: blob };
|
|
4258
|
+
}
|
|
4259
|
+
async resolveLocationValue(location) {
|
|
4260
|
+
if (typeof location === "string" || location instanceof Blob) {
|
|
4261
|
+
return this.resolveUriOrBlob(location, "application/geo+json");
|
|
4262
|
+
}
|
|
4263
|
+
return location;
|
|
4264
|
+
}
|
|
4265
|
+
/**
|
|
4266
|
+
* Check if an AttachLocationParams is the object form (not a StrongRef or string).
|
|
4267
|
+
* @internal
|
|
4268
|
+
*/
|
|
4269
|
+
isLocationObject(location) {
|
|
4270
|
+
return (typeof location === "object" &&
|
|
4271
|
+
!("uri" in location) &&
|
|
4272
|
+
!("cid" in location) &&
|
|
4273
|
+
location !== null &&
|
|
4274
|
+
!Array.isArray(location));
|
|
4275
|
+
}
|
|
4276
|
+
/**
|
|
4277
|
+
* Helper to resolve a location reference to a StrongRef.
|
|
4278
|
+
*
|
|
4279
|
+
* @param location - Location parameter (StrongRef, string URI, or location object)
|
|
4280
|
+
* @returns Promise resolving to a StrongRef
|
|
4281
|
+
* @throws {ValidationError} When string input doesn't match AT-URI pattern
|
|
4282
|
+
* @throws {NetworkError} When getRecord fails or returns no CID
|
|
4283
|
+
* @internal
|
|
4284
|
+
*/
|
|
4285
|
+
async resolveLocation(location) {
|
|
4286
|
+
if (typeof location === "string") {
|
|
4287
|
+
return this.resolveStrongRefFromUri(location);
|
|
4288
|
+
}
|
|
4289
|
+
if (this.isLocationObject(location)) {
|
|
4290
|
+
return this.createLocationRecord(location);
|
|
4269
4291
|
}
|
|
4292
|
+
if ("uri" in location && "cid" in location) {
|
|
4293
|
+
return { $type: "com.atproto.repo.strongRef", uri: location.uri, cid: location.cid };
|
|
4294
|
+
}
|
|
4295
|
+
throw new ValidationError("resolveLocation: Unsupported location input.");
|
|
4296
|
+
}
|
|
4297
|
+
async resolveStrongRefFromUri(uri) {
|
|
4298
|
+
const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
4299
|
+
if (!uriMatch) {
|
|
4300
|
+
throw new ValidationError(`resolveLocation: Invalid location AT-URI: "${uri}"`);
|
|
4301
|
+
}
|
|
4302
|
+
const [, repo, collection, rkey] = uriMatch;
|
|
4303
|
+
const record = await this.agent.com.atproto.repo.getRecord({ repo, collection, rkey });
|
|
4304
|
+
if (!record.success) {
|
|
4305
|
+
throw new NetworkError(`resolveLocation: getRecord failed for repo=${repo}, collection=${collection}, rkey=${rkey}`);
|
|
4306
|
+
}
|
|
4307
|
+
if (!record.data.cid) {
|
|
4308
|
+
throw new NetworkError(`resolveLocation: getRecord returned no CID for repo=${repo}, collection=${collection}, rkey=${rkey}`);
|
|
4309
|
+
}
|
|
4310
|
+
return { $type: "com.atproto.repo.strongRef", uri, cid: record.data.cid };
|
|
4270
4311
|
}
|
|
4271
4312
|
/**
|
|
4272
4313
|
* Adds evidence to any subject via the subject ref.
|
|
@@ -4505,7 +4546,7 @@ class HypercertOperationsImpl extends EventEmitter {
|
|
|
4505
4546
|
*
|
|
4506
4547
|
* @param params - Collection parameters
|
|
4507
4548
|
* @param params.title - Collection title
|
|
4508
|
-
* @param params.
|
|
4549
|
+
* @param params.items - Array of hypercert references with weights
|
|
4509
4550
|
* @param params.shortDescription - Optional short description
|
|
4510
4551
|
* @param params.banner - Optional cover image blob
|
|
4511
4552
|
* @returns Promise resolving to collection record URI and CID
|
|
@@ -4517,66 +4558,64 @@ class HypercertOperationsImpl extends EventEmitter {
|
|
|
4517
4558
|
* const collection = await repo.hypercerts.createCollection({
|
|
4518
4559
|
* title: "Climate Projects 2024",
|
|
4519
4560
|
* shortDescription: "Our climate impact portfolio",
|
|
4520
|
-
*
|
|
4521
|
-
* { uri: hypercert1Uri, cid: hypercert1Cid,
|
|
4522
|
-
* { uri: hypercert2Uri, cid: hypercert2Cid,
|
|
4523
|
-
* { uri: hypercert3Uri, cid: hypercert3Cid,
|
|
4561
|
+
* items: [
|
|
4562
|
+
* { itemIdentifier: { uri: hypercert1Uri, cid: hypercert1Cid }, itemWeight: "0.5" },
|
|
4563
|
+
* { itemIdentifier: { uri: hypercert2Uri, cid: hypercert2Cid }, itemWeight: "0.3" },
|
|
4564
|
+
* { itemIdentifier: { uri: hypercert3Uri, cid: hypercert3Cid }, itemWeight: "0.2" },
|
|
4524
4565
|
* ],
|
|
4525
4566
|
* banner: coverImageBlob,
|
|
4526
4567
|
* });
|
|
4527
4568
|
* ```
|
|
4528
4569
|
*/
|
|
4529
4570
|
async createCollection(params) {
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
if (uploadResult.success) {
|
|
4540
|
-
bannerRef = {
|
|
4541
|
-
$type: "blob",
|
|
4542
|
-
ref: { $link: uploadResult.data.blob.ref.toString() },
|
|
4543
|
-
mimeType: uploadResult.data.blob.mimeType,
|
|
4544
|
-
size: uploadResult.data.blob.size,
|
|
4545
|
-
};
|
|
4546
|
-
}
|
|
4547
|
-
}
|
|
4548
|
-
const collectionRecord = {
|
|
4549
|
-
$type: HYPERCERT_COLLECTIONS.COLLECTION,
|
|
4550
|
-
title: params.title,
|
|
4551
|
-
claims: params.claims.map((c) => ({ claim: { uri: c.uri, cid: c.cid }, weight: c.weight })),
|
|
4552
|
-
createdAt,
|
|
4553
|
-
};
|
|
4554
|
-
if (params.shortDescription) {
|
|
4555
|
-
collectionRecord.shortDescription = params.shortDescription;
|
|
4556
|
-
}
|
|
4557
|
-
if (bannerRef) {
|
|
4558
|
-
collectionRecord.banner = bannerRef;
|
|
4559
|
-
}
|
|
4560
|
-
const validation = validate(collectionRecord, HYPERCERT_COLLECTIONS.COLLECTION, "main", false);
|
|
4561
|
-
if (!validation.success) {
|
|
4562
|
-
throw new ValidationError(`Invalid collection record: ${validation.error?.message}`);
|
|
4563
|
-
}
|
|
4564
|
-
const result = await this.agent.com.atproto.repo.createRecord({
|
|
4565
|
-
repo: this.repoDid,
|
|
4566
|
-
collection: HYPERCERT_COLLECTIONS.COLLECTION,
|
|
4567
|
-
record: collectionRecord,
|
|
4568
|
-
});
|
|
4569
|
-
if (!result.success) {
|
|
4570
|
-
throw new NetworkError("Failed to create collection");
|
|
4571
|
-
}
|
|
4572
|
-
this.emit("collectionCreated", { uri: result.data.uri, cid: result.data.cid });
|
|
4573
|
-
return { uri: result.data.uri, cid: result.data.cid };
|
|
4571
|
+
const createdAt = new Date().toISOString();
|
|
4572
|
+
const collectionRecord = {
|
|
4573
|
+
$type: HYPERCERT_COLLECTIONS.COLLECTION,
|
|
4574
|
+
title: params.title,
|
|
4575
|
+
items: [],
|
|
4576
|
+
createdAt,
|
|
4577
|
+
};
|
|
4578
|
+
if (params.type) {
|
|
4579
|
+
collectionRecord.type = params.type;
|
|
4574
4580
|
}
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4581
|
+
if (params.shortDescription) {
|
|
4582
|
+
collectionRecord.shortDescription = params.shortDescription;
|
|
4583
|
+
}
|
|
4584
|
+
if (params.description) {
|
|
4585
|
+
collectionRecord.description = params.description;
|
|
4586
|
+
}
|
|
4587
|
+
if (params.avatar) {
|
|
4588
|
+
collectionRecord.avatar = await this.resolveCollectionImageInput(params.avatar);
|
|
4579
4589
|
}
|
|
4590
|
+
if (params.banner) {
|
|
4591
|
+
collectionRecord.banner = await this.resolveCollectionImageInput(params.banner, true);
|
|
4592
|
+
}
|
|
4593
|
+
if (params.items !== undefined) {
|
|
4594
|
+
collectionRecord.items = params.items;
|
|
4595
|
+
}
|
|
4596
|
+
if (params.location) {
|
|
4597
|
+
const locationResult = await this.resolveLocation(params.location);
|
|
4598
|
+
collectionRecord.location = locationResult;
|
|
4599
|
+
}
|
|
4600
|
+
const validation = validate(collectionRecord, HYPERCERT_COLLECTIONS.COLLECTION, "main", false);
|
|
4601
|
+
if (!validation.success) {
|
|
4602
|
+
throw new ValidationError(`Invalid collection record: ${validation.error?.message}`);
|
|
4603
|
+
}
|
|
4604
|
+
const result = await this.agent.com.atproto.repo.createRecord({
|
|
4605
|
+
repo: this.repoDid,
|
|
4606
|
+
collection: HYPERCERT_COLLECTIONS.COLLECTION,
|
|
4607
|
+
record: collectionRecord,
|
|
4608
|
+
});
|
|
4609
|
+
if (!result.success) {
|
|
4610
|
+
throw new NetworkError("Failed to create collection");
|
|
4611
|
+
}
|
|
4612
|
+
const createCollectionResult = {
|
|
4613
|
+
uri: result.data.uri,
|
|
4614
|
+
cid: result.data.cid,
|
|
4615
|
+
record: collectionRecord,
|
|
4616
|
+
};
|
|
4617
|
+
this.emit("collectionCreated", createCollectionResult);
|
|
4618
|
+
return createCollectionResult;
|
|
4580
4619
|
}
|
|
4581
4620
|
/**
|
|
4582
4621
|
* Gets a collection by its AT-URI.
|
|
@@ -4669,114 +4708,32 @@ class HypercertOperationsImpl extends EventEmitter {
|
|
|
4669
4708
|
/**
|
|
4670
4709
|
* Creates a new project that organizes multiple hypercert activities.
|
|
4671
4710
|
*
|
|
4672
|
-
*
|
|
4711
|
+
* A project is a collection with type='project' and optional location sidecar.
|
|
4712
|
+
* This method delegates to createCollection and adds the type field.
|
|
4673
4713
|
*
|
|
4674
4714
|
* @param params - Project creation parameters
|
|
4675
|
-
* @returns Promise resolving to created project URI and CID
|
|
4715
|
+
* @returns Promise resolving to created project URI and CID with optional location URI
|
|
4676
4716
|
*
|
|
4677
4717
|
* @example
|
|
4678
4718
|
* ```typescript
|
|
4679
4719
|
* const result = await repo.hypercerts.createProject({
|
|
4680
4720
|
* title: "Climate Impact 2024",
|
|
4681
4721
|
* shortDescription: "Year-long climate initiative",
|
|
4682
|
-
*
|
|
4683
|
-
* { uri: activity1Uri, cid: activity1Cid,
|
|
4684
|
-
* { uri: activity2Uri, cid: activity2Cid,
|
|
4722
|
+
* items: [
|
|
4723
|
+
* { itemIdentifier: { uri: activity1Uri, cid: activity1Cid }, itemWeight: "0.6" },
|
|
4724
|
+
* { itemIdentifier: { uri: activity2Uri, cid: activity2Cid }, itemWeight: "0.4" }
|
|
4685
4725
|
* ]
|
|
4686
4726
|
* });
|
|
4687
4727
|
* console.log(`Created project: ${result.uri}`);
|
|
4688
4728
|
* ```
|
|
4689
4729
|
*/
|
|
4690
4730
|
async createProject(params) {
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
const uint8Array = new Uint8Array(arrayBuffer);
|
|
4698
|
-
const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
|
|
4699
|
-
encoding: params.avatar.type || "image/jpeg",
|
|
4700
|
-
});
|
|
4701
|
-
if (uploadResult.success) {
|
|
4702
|
-
avatarRef = {
|
|
4703
|
-
$type: "blob",
|
|
4704
|
-
ref: { $link: uploadResult.data.blob.ref.toString() },
|
|
4705
|
-
mimeType: uploadResult.data.blob.mimeType,
|
|
4706
|
-
size: uploadResult.data.blob.size,
|
|
4707
|
-
};
|
|
4708
|
-
}
|
|
4709
|
-
else {
|
|
4710
|
-
throw new NetworkError("Failed to upload avatar image");
|
|
4711
|
-
}
|
|
4712
|
-
}
|
|
4713
|
-
// Upload banner blob if provided
|
|
4714
|
-
let bannerRef;
|
|
4715
|
-
if (params.banner) {
|
|
4716
|
-
const arrayBuffer = await params.banner.arrayBuffer();
|
|
4717
|
-
const uint8Array = new Uint8Array(arrayBuffer);
|
|
4718
|
-
const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
|
|
4719
|
-
encoding: params.banner.type || "image/jpeg",
|
|
4720
|
-
});
|
|
4721
|
-
if (uploadResult.success) {
|
|
4722
|
-
bannerRef = {
|
|
4723
|
-
$type: "blob",
|
|
4724
|
-
ref: { $link: uploadResult.data.blob.ref.toString() },
|
|
4725
|
-
mimeType: uploadResult.data.blob.mimeType,
|
|
4726
|
-
size: uploadResult.data.blob.size,
|
|
4727
|
-
};
|
|
4728
|
-
}
|
|
4729
|
-
else {
|
|
4730
|
-
throw new NetworkError("Failed to upload banner image");
|
|
4731
|
-
}
|
|
4732
|
-
}
|
|
4733
|
-
// Build project record as a collection with type='project'
|
|
4734
|
-
// Collections require 'items' array, so we map activities to items
|
|
4735
|
-
const items = params.activities?.map((a) => ({
|
|
4736
|
-
itemIdentifier: { uri: a.uri, cid: a.cid },
|
|
4737
|
-
itemWeight: a.weight,
|
|
4738
|
-
})) || [];
|
|
4739
|
-
const projectRecord = {
|
|
4740
|
-
$type: HYPERCERT_COLLECTIONS.COLLECTION,
|
|
4741
|
-
type: "project",
|
|
4742
|
-
title: params.title,
|
|
4743
|
-
shortDescription: params.shortDescription,
|
|
4744
|
-
items,
|
|
4745
|
-
createdAt,
|
|
4746
|
-
};
|
|
4747
|
-
// Add optional fields
|
|
4748
|
-
if (params.description) {
|
|
4749
|
-
projectRecord.description = params.description;
|
|
4750
|
-
}
|
|
4751
|
-
if (avatarRef) {
|
|
4752
|
-
projectRecord.avatar = avatarRef;
|
|
4753
|
-
}
|
|
4754
|
-
if (bannerRef) {
|
|
4755
|
-
projectRecord.banner = bannerRef;
|
|
4756
|
-
}
|
|
4757
|
-
// Validate against collection lexicon
|
|
4758
|
-
const validation = validate(projectRecord, HYPERCERT_COLLECTIONS.COLLECTION, "main", false);
|
|
4759
|
-
if (!validation.success) {
|
|
4760
|
-
throw new ValidationError(`Invalid project record: ${validation.error?.message}`);
|
|
4761
|
-
}
|
|
4762
|
-
// Create record
|
|
4763
|
-
const result = await this.agent.com.atproto.repo.createRecord({
|
|
4764
|
-
repo: this.repoDid,
|
|
4765
|
-
collection: HYPERCERT_COLLECTIONS.COLLECTION,
|
|
4766
|
-
record: projectRecord,
|
|
4767
|
-
});
|
|
4768
|
-
if (!result.success) {
|
|
4769
|
-
throw new NetworkError("Failed to create project");
|
|
4770
|
-
}
|
|
4771
|
-
// Emit event
|
|
4772
|
-
this.emit("projectCreated", { uri: result.data.uri, cid: result.data.cid });
|
|
4773
|
-
return { uri: result.data.uri, cid: result.data.cid };
|
|
4774
|
-
}
|
|
4775
|
-
catch (error) {
|
|
4776
|
-
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
4777
|
-
throw error;
|
|
4778
|
-
throw new NetworkError(`Failed to create project: ${error instanceof Error ? error.message : "Unknown"}`, error);
|
|
4779
|
-
}
|
|
4731
|
+
const result = await this.createCollection({
|
|
4732
|
+
...params,
|
|
4733
|
+
type: "project",
|
|
4734
|
+
});
|
|
4735
|
+
this.emit("projectCreated", { uri: result.uri, cid: result.cid });
|
|
4736
|
+
return result;
|
|
4780
4737
|
}
|
|
4781
4738
|
/**
|
|
4782
4739
|
* Gets a project by its AT-URI.
|
|
@@ -4890,7 +4847,8 @@ class HypercertOperationsImpl extends EventEmitter {
|
|
|
4890
4847
|
/**
|
|
4891
4848
|
* Updates an existing project.
|
|
4892
4849
|
*
|
|
4893
|
-
*
|
|
4850
|
+
* A project is a collection with type='project'. This method delegates to
|
|
4851
|
+
* updateCollection and handles the avatar field.
|
|
4894
4852
|
*
|
|
4895
4853
|
* @param uri - AT-URI of the project to update
|
|
4896
4854
|
* @param updates - Fields to update
|
|
@@ -4905,8 +4863,73 @@ class HypercertOperationsImpl extends EventEmitter {
|
|
|
4905
4863
|
* ```
|
|
4906
4864
|
*/
|
|
4907
4865
|
async updateProject(uri, updates) {
|
|
4866
|
+
// Verify it's a project before updating
|
|
4867
|
+
const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
4868
|
+
if (!uriMatch) {
|
|
4869
|
+
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
4870
|
+
}
|
|
4871
|
+
const [, , collection, rkey] = uriMatch;
|
|
4872
|
+
const existing = await this.agent.com.atproto.repo.getRecord({
|
|
4873
|
+
repo: this.repoDid,
|
|
4874
|
+
collection,
|
|
4875
|
+
rkey,
|
|
4876
|
+
});
|
|
4877
|
+
if (!existing.success) {
|
|
4878
|
+
throw new NetworkError(`Project not found: ${uri}`);
|
|
4879
|
+
}
|
|
4880
|
+
const record = existing.data.value;
|
|
4881
|
+
if (record.type !== "project") {
|
|
4882
|
+
throw new ValidationError(`Record is not a project (type='${record.type}')`);
|
|
4883
|
+
}
|
|
4884
|
+
// Delegate to updateCollection
|
|
4885
|
+
const result = await this.updateCollection(uri, updates);
|
|
4886
|
+
this.emit("projectUpdated", { uri: result.uri, cid: result.cid });
|
|
4887
|
+
return result;
|
|
4888
|
+
}
|
|
4889
|
+
/**
|
|
4890
|
+
* Deletes a project.
|
|
4891
|
+
*
|
|
4892
|
+
* A project is a collection with type='project'. This method delegates to
|
|
4893
|
+
* deleteCollection after verifying the record is a project.
|
|
4894
|
+
*
|
|
4895
|
+
* @param uri - AT-URI of the project to delete
|
|
4896
|
+
*
|
|
4897
|
+
* @example
|
|
4898
|
+
* ```typescript
|
|
4899
|
+
* await repo.hypercerts.deleteProject(projectUri);
|
|
4900
|
+
* console.log("Project deleted");
|
|
4901
|
+
* ```
|
|
4902
|
+
*/
|
|
4903
|
+
async deleteProject(uri) {
|
|
4904
|
+
const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
4905
|
+
if (!uriMatch) {
|
|
4906
|
+
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
4907
|
+
}
|
|
4908
|
+
const [, , collection, rkey] = uriMatch;
|
|
4909
|
+
const existing = await this.agent.com.atproto.repo.getRecord({
|
|
4910
|
+
repo: this.repoDid,
|
|
4911
|
+
collection,
|
|
4912
|
+
rkey,
|
|
4913
|
+
});
|
|
4914
|
+
if (!existing.success) {
|
|
4915
|
+
throw new NetworkError(`Project not found: ${uri}`);
|
|
4916
|
+
}
|
|
4917
|
+
const record = existing.data.value;
|
|
4918
|
+
if (record.type !== "project") {
|
|
4919
|
+
throw new ValidationError(`Record is not a project (type='${record.type}')`);
|
|
4920
|
+
}
|
|
4921
|
+
await this.deleteCollection(uri);
|
|
4922
|
+
this.emit("projectDeleted", { uri });
|
|
4923
|
+
}
|
|
4924
|
+
/**
|
|
4925
|
+
* Updates a collection.
|
|
4926
|
+
*
|
|
4927
|
+
* @param uri - AT-URI of the collection to update
|
|
4928
|
+
* @param updates - Fields to update
|
|
4929
|
+
* @returns Promise resolving to updated collection URI and CID
|
|
4930
|
+
*/
|
|
4931
|
+
async updateCollection(uri, updates) {
|
|
4908
4932
|
try {
|
|
4909
|
-
// Parse URI and fetch existing record
|
|
4910
4933
|
const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
4911
4934
|
if (!uriMatch) {
|
|
4912
4935
|
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
@@ -4918,100 +4941,74 @@ class HypercertOperationsImpl extends EventEmitter {
|
|
|
4918
4941
|
rkey,
|
|
4919
4942
|
});
|
|
4920
4943
|
if (!existing.success) {
|
|
4921
|
-
throw new NetworkError(`
|
|
4944
|
+
throw new NetworkError(`Collection not found: ${uri}`);
|
|
4922
4945
|
}
|
|
4923
4946
|
const existingRecord = existing.data.value;
|
|
4924
|
-
// Verify it's actually a project
|
|
4925
|
-
if (existingRecord.type !== "project") {
|
|
4926
|
-
throw new ValidationError(`Record is not a project (type='${existingRecord.type}')`);
|
|
4927
|
-
}
|
|
4928
|
-
// Merge updates with existing record
|
|
4929
4947
|
const recordForUpdate = {
|
|
4930
4948
|
...existingRecord,
|
|
4931
|
-
// MUST preserve type, createdAt, and items structure
|
|
4932
|
-
type: "project",
|
|
4933
4949
|
createdAt: existingRecord.createdAt,
|
|
4950
|
+
type: existingRecord.type,
|
|
4934
4951
|
};
|
|
4935
|
-
// Apply simple field updates
|
|
4936
4952
|
if (updates.title !== undefined)
|
|
4937
4953
|
recordForUpdate.title = updates.title;
|
|
4938
4954
|
if (updates.shortDescription !== undefined)
|
|
4939
4955
|
recordForUpdate.shortDescription = updates.shortDescription;
|
|
4940
4956
|
if (updates.description !== undefined)
|
|
4941
4957
|
recordForUpdate.description = updates.description;
|
|
4942
|
-
//
|
|
4958
|
+
// Explicitly reject type changes
|
|
4959
|
+
if (updates.type !== undefined && updates.type !== existingRecord.type) {
|
|
4960
|
+
throw new ValidationError(`Cannot change collection type from '${existingRecord.type}' to '${updates.type}'`);
|
|
4961
|
+
}
|
|
4943
4962
|
delete recordForUpdate.avatar;
|
|
4944
4963
|
if (updates.avatar !== undefined) {
|
|
4945
4964
|
if (updates.avatar === null) {
|
|
4946
4965
|
// Remove avatar
|
|
4947
4966
|
}
|
|
4948
4967
|
else {
|
|
4949
|
-
|
|
4950
|
-
const uint8Array = new Uint8Array(arrayBuffer);
|
|
4951
|
-
const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
|
|
4952
|
-
encoding: updates.avatar.type || "image/jpeg",
|
|
4953
|
-
});
|
|
4954
|
-
if (uploadResult.success) {
|
|
4955
|
-
recordForUpdate.avatar = {
|
|
4956
|
-
$type: "blob",
|
|
4957
|
-
ref: uploadResult.data.blob.ref,
|
|
4958
|
-
mimeType: uploadResult.data.blob.mimeType,
|
|
4959
|
-
size: uploadResult.data.blob.size,
|
|
4960
|
-
};
|
|
4961
|
-
}
|
|
4962
|
-
else {
|
|
4963
|
-
throw new NetworkError("Failed to upload avatar image");
|
|
4964
|
-
}
|
|
4968
|
+
recordForUpdate.avatar = await this.resolveCollectionImageInput(updates.avatar);
|
|
4965
4969
|
}
|
|
4966
4970
|
}
|
|
4967
4971
|
else if (existingRecord.avatar) {
|
|
4968
4972
|
recordForUpdate.avatar = existingRecord.avatar;
|
|
4969
4973
|
}
|
|
4970
|
-
// Handle banner update
|
|
4971
4974
|
delete recordForUpdate.banner;
|
|
4972
4975
|
if (updates.banner !== undefined) {
|
|
4973
4976
|
if (updates.banner === null) {
|
|
4974
4977
|
// Remove banner
|
|
4975
4978
|
}
|
|
4976
4979
|
else {
|
|
4977
|
-
|
|
4978
|
-
const uint8Array = new Uint8Array(arrayBuffer);
|
|
4979
|
-
const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
|
|
4980
|
-
encoding: updates.banner.type || "image/jpeg",
|
|
4981
|
-
});
|
|
4982
|
-
if (uploadResult.success) {
|
|
4983
|
-
recordForUpdate.banner = {
|
|
4984
|
-
$type: "blob",
|
|
4985
|
-
ref: uploadResult.data.blob.ref,
|
|
4986
|
-
mimeType: uploadResult.data.blob.mimeType,
|
|
4987
|
-
size: uploadResult.data.blob.size,
|
|
4988
|
-
};
|
|
4989
|
-
}
|
|
4990
|
-
else {
|
|
4991
|
-
throw new NetworkError("Failed to upload banner image");
|
|
4992
|
-
}
|
|
4980
|
+
recordForUpdate.banner = await this.resolveCollectionImageInput(updates.banner, true);
|
|
4993
4981
|
}
|
|
4994
4982
|
}
|
|
4995
4983
|
else if (existingRecord.banner) {
|
|
4996
4984
|
recordForUpdate.banner = existingRecord.banner;
|
|
4997
4985
|
}
|
|
4998
|
-
|
|
4999
|
-
if (updates.
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
4986
|
+
delete recordForUpdate.location;
|
|
4987
|
+
if (updates.location !== undefined) {
|
|
4988
|
+
if (updates.location === null) {
|
|
4989
|
+
// Remove location
|
|
4990
|
+
}
|
|
4991
|
+
else {
|
|
4992
|
+
const resolvedLocation = await this.resolveLocation(updates.location);
|
|
4993
|
+
if (!resolvedLocation) {
|
|
4994
|
+
throw new ValidationError("resolveLocation: failed to resolve location");
|
|
4995
|
+
}
|
|
4996
|
+
recordForUpdate.location = resolvedLocation;
|
|
4997
|
+
}
|
|
4998
|
+
}
|
|
4999
|
+
else if (existingRecord.location) {
|
|
5000
|
+
recordForUpdate.location = existingRecord.location;
|
|
5004
5001
|
}
|
|
5005
|
-
|
|
5006
|
-
|
|
5002
|
+
if (updates.items) {
|
|
5003
|
+
recordForUpdate.items = updates.items;
|
|
5004
|
+
}
|
|
5005
|
+
else if (existingRecord.items) {
|
|
5007
5006
|
recordForUpdate.items = existingRecord.items;
|
|
5008
5007
|
}
|
|
5009
|
-
// Validate merged record
|
|
5010
5008
|
const validation = validate(recordForUpdate, HYPERCERT_COLLECTIONS.COLLECTION, "main", false);
|
|
5011
5009
|
if (!validation.success) {
|
|
5012
|
-
throw new ValidationError(`Invalid
|
|
5010
|
+
throw new ValidationError(`Invalid collection record: ${validation.error?.message}`);
|
|
5013
5011
|
}
|
|
5014
|
-
// Update record
|
|
5015
5012
|
const result = await this.agent.com.atproto.repo.putRecord({
|
|
5016
5013
|
repo: this.repoDid,
|
|
5017
5014
|
collection,
|
|
@@ -5019,68 +5016,196 @@ class HypercertOperationsImpl extends EventEmitter {
|
|
|
5019
5016
|
record: recordForUpdate,
|
|
5020
5017
|
});
|
|
5021
5018
|
if (!result.success) {
|
|
5022
|
-
throw new NetworkError("Failed to update
|
|
5019
|
+
throw new NetworkError("Failed to update collection");
|
|
5023
5020
|
}
|
|
5024
|
-
|
|
5025
|
-
this.emit("projectUpdated", { uri: result.data.uri, cid: result.data.cid });
|
|
5021
|
+
this.emit("collectionUpdated", { uri: result.data.uri, cid: result.data.cid });
|
|
5026
5022
|
return { uri: result.data.uri, cid: result.data.cid };
|
|
5027
5023
|
}
|
|
5028
5024
|
catch (error) {
|
|
5029
5025
|
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
5030
5026
|
throw error;
|
|
5031
|
-
throw new NetworkError(`Failed to update
|
|
5027
|
+
throw new NetworkError(`Failed to update collection: ${error instanceof Error ? error.message : "Unknown"}`, error);
|
|
5032
5028
|
}
|
|
5033
5029
|
}
|
|
5034
5030
|
/**
|
|
5035
|
-
* Deletes a
|
|
5031
|
+
* Deletes a collection.
|
|
5036
5032
|
*
|
|
5037
|
-
*
|
|
5033
|
+
* @param uri - AT-URI of the collection to delete
|
|
5034
|
+
*/
|
|
5035
|
+
async deleteCollection(uri) {
|
|
5036
|
+
try {
|
|
5037
|
+
const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
5038
|
+
if (!uriMatch) {
|
|
5039
|
+
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
5040
|
+
}
|
|
5041
|
+
const [, , collection, rkey] = uriMatch;
|
|
5042
|
+
const result = await this.agent.com.atproto.repo.deleteRecord({
|
|
5043
|
+
repo: this.repoDid,
|
|
5044
|
+
collection,
|
|
5045
|
+
rkey,
|
|
5046
|
+
});
|
|
5047
|
+
if (!result.success) {
|
|
5048
|
+
throw new NetworkError("Failed to delete collection");
|
|
5049
|
+
}
|
|
5050
|
+
this.emit("collectionDeleted", { uri });
|
|
5051
|
+
}
|
|
5052
|
+
catch (error) {
|
|
5053
|
+
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
5054
|
+
throw error;
|
|
5055
|
+
throw new NetworkError(`Failed to delete collection: ${error instanceof Error ? error.message : "Unknown"}`, error);
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
/**
|
|
5059
|
+
* Attaches a location to a collection.
|
|
5038
5060
|
*
|
|
5039
|
-
* @param uri - AT-URI of the
|
|
5061
|
+
* @param uri - AT-URI of the collection
|
|
5062
|
+
* @param location - Location data
|
|
5063
|
+
* @returns Promise resolving to location record result
|
|
5064
|
+
*/
|
|
5065
|
+
async attachLocationToCollection(uri, location) {
|
|
5066
|
+
try {
|
|
5067
|
+
const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
5068
|
+
if (!uriMatch) {
|
|
5069
|
+
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
5070
|
+
}
|
|
5071
|
+
const [, , collection, rkey] = uriMatch;
|
|
5072
|
+
const existing = await this.agent.com.atproto.repo.getRecord({
|
|
5073
|
+
repo: this.repoDid,
|
|
5074
|
+
collection,
|
|
5075
|
+
rkey,
|
|
5076
|
+
});
|
|
5077
|
+
if (!existing.success) {
|
|
5078
|
+
throw new NetworkError(`Collection not found: ${uri}`);
|
|
5079
|
+
}
|
|
5080
|
+
const resolvedLocation = await this.resolveLocation(location);
|
|
5081
|
+
if (!resolvedLocation) {
|
|
5082
|
+
throw new ValidationError("attachLocationToCollection: failed to resolve location");
|
|
5083
|
+
}
|
|
5084
|
+
const recordForUpdate = {
|
|
5085
|
+
...existing.data.value,
|
|
5086
|
+
location: resolvedLocation,
|
|
5087
|
+
};
|
|
5088
|
+
const updateResult = await this.agent.com.atproto.repo.putRecord({
|
|
5089
|
+
repo: this.repoDid,
|
|
5090
|
+
collection,
|
|
5091
|
+
rkey,
|
|
5092
|
+
record: recordForUpdate,
|
|
5093
|
+
});
|
|
5094
|
+
if (!updateResult.success) {
|
|
5095
|
+
throw new NetworkError("Failed to update collection with location");
|
|
5096
|
+
}
|
|
5097
|
+
this.emit("locationAttachedToCollection", {
|
|
5098
|
+
uri: resolvedLocation.uri,
|
|
5099
|
+
cid: resolvedLocation.cid,
|
|
5100
|
+
collectionUri: uri,
|
|
5101
|
+
});
|
|
5102
|
+
return { uri: resolvedLocation.uri, cid: resolvedLocation.cid };
|
|
5103
|
+
}
|
|
5104
|
+
catch (error) {
|
|
5105
|
+
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
5106
|
+
throw error;
|
|
5107
|
+
throw new NetworkError(`Failed to attach location: ${error instanceof Error ? error.message : "Unknown"}`, error);
|
|
5108
|
+
}
|
|
5109
|
+
}
|
|
5110
|
+
/**
|
|
5111
|
+
* Removes a location from a collection.
|
|
5040
5112
|
*
|
|
5041
|
-
* @
|
|
5042
|
-
* ```typescript
|
|
5043
|
-
* await repo.hypercerts.deleteProject(projectUri);
|
|
5044
|
-
* console.log("Project deleted");
|
|
5045
|
-
* ```
|
|
5113
|
+
* @param uri - AT-URI of the collection
|
|
5046
5114
|
*/
|
|
5047
|
-
async
|
|
5115
|
+
async removeLocationFromCollection(uri) {
|
|
5048
5116
|
try {
|
|
5049
|
-
// Parse URI
|
|
5050
5117
|
const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
5051
5118
|
if (!uriMatch) {
|
|
5052
5119
|
throw new ValidationError(`Invalid URI format: ${uri}`);
|
|
5053
5120
|
}
|
|
5054
5121
|
const [, , collection, rkey] = uriMatch;
|
|
5055
|
-
// Verify it's actually a project before deleting
|
|
5056
5122
|
const existing = await this.agent.com.atproto.repo.getRecord({
|
|
5057
5123
|
repo: this.repoDid,
|
|
5058
5124
|
collection,
|
|
5059
5125
|
rkey,
|
|
5060
5126
|
});
|
|
5061
|
-
if (existing.success) {
|
|
5062
|
-
|
|
5063
|
-
if (record.type !== "project") {
|
|
5064
|
-
throw new ValidationError(`Record is not a project (type='${record.type}')`);
|
|
5065
|
-
}
|
|
5127
|
+
if (!existing.success) {
|
|
5128
|
+
throw new NetworkError(`Collection not found: ${uri}`);
|
|
5066
5129
|
}
|
|
5067
|
-
|
|
5068
|
-
|
|
5130
|
+
const recordForUpdate = { ...existing.data.value };
|
|
5131
|
+
delete recordForUpdate.location;
|
|
5132
|
+
const result = await this.agent.com.atproto.repo.putRecord({
|
|
5069
5133
|
repo: this.repoDid,
|
|
5070
5134
|
collection,
|
|
5071
5135
|
rkey,
|
|
5136
|
+
record: recordForUpdate,
|
|
5072
5137
|
});
|
|
5073
5138
|
if (!result.success) {
|
|
5074
|
-
throw new NetworkError("Failed to
|
|
5139
|
+
throw new NetworkError("Failed to remove location from collection");
|
|
5075
5140
|
}
|
|
5076
|
-
|
|
5077
|
-
this.emit("projectDeleted", { uri });
|
|
5141
|
+
this.emit("locationRemovedFromCollection", { collectionUri: uri });
|
|
5078
5142
|
}
|
|
5079
5143
|
catch (error) {
|
|
5080
5144
|
if (error instanceof ValidationError || error instanceof NetworkError)
|
|
5081
5145
|
throw error;
|
|
5082
|
-
throw new NetworkError(`Failed to
|
|
5146
|
+
throw new NetworkError(`Failed to remove location: ${error instanceof Error ? error.message : "Unknown"}`, error);
|
|
5147
|
+
}
|
|
5148
|
+
}
|
|
5149
|
+
/**
|
|
5150
|
+
* Attaches a location to a project.
|
|
5151
|
+
*
|
|
5152
|
+
* @param uri - AT-URI of the project
|
|
5153
|
+
* @param location - Location data
|
|
5154
|
+
* @returns Promise resolving to location record result
|
|
5155
|
+
*/
|
|
5156
|
+
async attachLocationToProject(uri, location) {
|
|
5157
|
+
const result = await this.attachLocationToCollection(uri, location);
|
|
5158
|
+
this.emit("locationAttachedToProject", {
|
|
5159
|
+
uri: result.uri,
|
|
5160
|
+
cid: result.cid,
|
|
5161
|
+
projectUri: uri,
|
|
5162
|
+
});
|
|
5163
|
+
return result;
|
|
5164
|
+
}
|
|
5165
|
+
/**
|
|
5166
|
+
* Removes a location from a project.
|
|
5167
|
+
*
|
|
5168
|
+
* @param uri - AT-URI of the project
|
|
5169
|
+
*/
|
|
5170
|
+
async removeLocationFromProject(uri) {
|
|
5171
|
+
await this.removeLocationFromCollection(uri);
|
|
5172
|
+
this.emit("locationRemovedFromProject", { projectUri: uri });
|
|
5173
|
+
}
|
|
5174
|
+
/**
|
|
5175
|
+
* Creates an app.certified.location record.
|
|
5176
|
+
*
|
|
5177
|
+
* @param location - Location parameters
|
|
5178
|
+
* @returns Promise resolving to location record URI and CID
|
|
5179
|
+
*/
|
|
5180
|
+
async createLocationRecord(location) {
|
|
5181
|
+
if (!location.srs) {
|
|
5182
|
+
throw new ValidationError("srs (Spatial Reference System) is required. Example: 'EPSG:4326' for WGS84 coordinates, or 'http://www.opengis.net/def/crs/OGC/1.3/CRS84' for CRS84.");
|
|
5183
|
+
}
|
|
5184
|
+
const { $type, createdAt, location: locationValue, lpVersion, ...rest } = location;
|
|
5185
|
+
if (locationValue === undefined) {
|
|
5186
|
+
throw new ValidationError("location is required to create a location record.");
|
|
5187
|
+
}
|
|
5188
|
+
const resolvedLocationValue = await this.resolveLocationValue(locationValue);
|
|
5189
|
+
const locationRecord = {
|
|
5190
|
+
...rest,
|
|
5191
|
+
lpVersion: lpVersion ?? "1.0",
|
|
5192
|
+
$type: $type ?? HYPERCERT_COLLECTIONS.LOCATION,
|
|
5193
|
+
createdAt: createdAt ?? new Date().toISOString(),
|
|
5194
|
+
location: resolvedLocationValue,
|
|
5195
|
+
};
|
|
5196
|
+
const validation = validate(locationRecord, HYPERCERT_COLLECTIONS.LOCATION, "main", false);
|
|
5197
|
+
if (!validation.success) {
|
|
5198
|
+
throw new ValidationError(`Invalid location record: ${validation.error?.message}`);
|
|
5199
|
+
}
|
|
5200
|
+
const result = await this.agent.com.atproto.repo.createRecord({
|
|
5201
|
+
repo: this.repoDid,
|
|
5202
|
+
collection: HYPERCERT_COLLECTIONS.LOCATION,
|
|
5203
|
+
record: locationRecord,
|
|
5204
|
+
});
|
|
5205
|
+
if (!result.success) {
|
|
5206
|
+
throw new NetworkError("Failed to create location record");
|
|
5083
5207
|
}
|
|
5208
|
+
return { uri: result.data.uri, cid: result.data.cid };
|
|
5084
5209
|
}
|
|
5085
5210
|
}
|
|
5086
5211
|
|