Skip to content

Commit 5087c83

Browse files
authored
Merge pull request #62 from hypercerts-org/fix/lexicon_record_types
Add required $type field to all record creation operations
2 parents 73c453b + 4b80edc commit 5087c83

File tree

7 files changed

+70
-19
lines changed

7 files changed

+70
-19
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@hypercerts-org/sdk-core": patch
3+
---
4+
5+
fix(sdk-core): add required $type field to all record creation operations
6+
7+
The AT Protocol requires all records to include a `$type` field, but the SDK was omitting it during record creation, causing validation errors like "Record/$type must be a string". This fix:
8+
9+
- Adds `$type` field to all record types (rights, claims, locations, contributions, measurements, evaluations, collections)
10+
- Fixes location record implementation to match `app.certified.location` lexicon schema
11+
- Makes `srs` (Spatial Reference System) field required for location records with proper validation
12+
- Updates interfaces and documentation to reflect required fields
13+
14+
Breaking change: `location.srs` is now required when creating locations (use "EPSG:4326" for standard WGS84 coordinates).

packages/lexicon/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hypercerts-org/lexicon",
3-
"version": "0.6.0",
3+
"version": "0.7.0",
44
"description": "ATProto lexicon definitions and TypeScript types for the Hypercerts protocol",
55
"type": "module",
66
"main": "dist/index.cjs",

packages/sdk-core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hypercerts-org/sdk-core",
3-
"version": "0.6.0",
3+
"version": "0.7.0",
44
"description": "Framework-agnostic ATProto SDK core for authentication, repository operations, and lexicon management",
55
"main": "dist/index.cjs",
66
"repository": {

packages/sdk-core/src/repository/HypercertOperationsImpl.ts

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,8 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
229229

230230
// Step 2: Create rights record
231231
this.emitProgress(params.onProgress, { name: "createRights", status: "start" });
232-
const rightsRecord: Omit<HypercertRights, "$type"> = {
232+
const rightsRecord: HypercertRights = {
233+
$type: HYPERCERT_COLLECTIONS.RIGHTS,
233234
rightsName: params.rights.name,
234235
rightsType: params.rights.type,
235236
rightsDescription: params.rights.description,
@@ -263,6 +264,7 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
263264
// Step 3: Create hypercert record
264265
this.emitProgress(params.onProgress, { name: "createHypercert", status: "start" });
265266
const hypercertRecord: Record<string, unknown> = {
267+
$type: HYPERCERT_COLLECTIONS.CLAIM,
266268
title: params.title,
267269
description: params.description,
268270
workScope: params.workScope,
@@ -641,6 +643,7 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
641643
* await repo.hypercerts.attachLocation(hypercertUri, {
642644
* value: "San Francisco, CA",
643645
* name: "SF Bay Area",
646+
* srs: "EPSG:4326",
644647
* });
645648
* ```
646649
*
@@ -662,34 +665,58 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
662665
location: { value: string; name?: string; description?: string; srs?: string; geojson?: Blob },
663666
): Promise<CreateResult> {
664667
try {
665-
// Get hypercert to get CID
666-
const hypercert = await this.get(hypercertUri);
668+
// Validate required srs field
669+
if (!location.srs) {
670+
throw new ValidationError(
671+
"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.",
672+
);
673+
}
674+
675+
// Validate that hypercert exists (unused but confirms hypercert is valid)
676+
await this.get(hypercertUri);
667677
const createdAt = new Date().toISOString();
668678

669-
let locationValue: string | BlobRef = location.value;
679+
// Determine location type and prepare location data
680+
let locationData: { $type: string; uri: string } | BlobRef;
681+
let locationType: string;
682+
670683
if (location.geojson) {
684+
// Upload GeoJSON as a blob
671685
const arrayBuffer = await location.geojson.arrayBuffer();
672686
const uint8Array = new Uint8Array(arrayBuffer);
673687
const uploadResult = await this.agent.com.atproto.repo.uploadBlob(uint8Array, {
674688
encoding: location.geojson.type || "application/geo+json",
675689
});
676690
if (uploadResult.success) {
677-
locationValue = {
691+
locationData = {
678692
$type: "blob",
679693
ref: { $link: uploadResult.data.blob.ref.toString() },
680694
mimeType: uploadResult.data.blob.mimeType,
681695
size: uploadResult.data.blob.size,
682696
};
697+
locationType = "geojson-point";
698+
} else {
699+
throw new NetworkError("Failed to upload GeoJSON blob");
683700
}
701+
} else {
702+
// Use value as a URI reference
703+
locationData = {
704+
$type: "app.certified.defs#uri",
705+
uri: location.value,
706+
};
707+
locationType = "coordinate-decimal";
684708
}
685709

686-
const locationRecord: Omit<HypercertLocation, "$type"> = {
687-
hypercert: { uri: hypercert.uri, cid: hypercert.cid },
688-
value: locationValue,
710+
// Build location record according to app.certified.location lexicon
711+
const locationRecord: HypercertLocation = {
712+
$type: HYPERCERT_COLLECTIONS.LOCATION,
713+
lpVersion: "1.0",
714+
srs: location.srs,
715+
locationType,
716+
location: locationData,
689717
createdAt,
690718
name: location.name,
691719
description: location.description,
692-
srs: location.srs,
693720
};
694721

695722
const validation = this.lexiconRegistry.validate(HYPERCERT_COLLECTIONS.LOCATION, locationRecord);
@@ -784,11 +811,13 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
784811
}): Promise<CreateResult> {
785812
try {
786813
const createdAt = new Date().toISOString();
787-
const contributionRecord: Omit<HypercertContribution, "$type"> = {
814+
const contributionRecord: HypercertContribution = {
815+
$type: HYPERCERT_COLLECTIONS.CONTRIBUTION,
788816
contributors: params.contributors,
789817
role: params.role,
790818
createdAt,
791819
description: params.description,
820+
hypercert: { uri: "", cid: "" }, // Will be set below if hypercertUri provided
792821
};
793822

794823
if (params.hypercertUri) {
@@ -863,7 +892,8 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
863892
const hypercert = await this.get(params.hypercertUri);
864893
const createdAt = new Date().toISOString();
865894

866-
const measurementRecord: Omit<HypercertMeasurement, "$type"> = {
895+
const measurementRecord: HypercertMeasurement = {
896+
$type: HYPERCERT_COLLECTIONS.MEASUREMENT,
867897
hypercert: { uri: hypercert.uri, cid: hypercert.cid },
868898
measurers: params.measurers,
869899
metric: params.metric,
@@ -922,7 +952,8 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
922952
const subject = await this.get(params.subjectUri);
923953
const createdAt = new Date().toISOString();
924954

925-
const evaluationRecord: Omit<HypercertEvaluation, "$type"> = {
955+
const evaluationRecord: HypercertEvaluation = {
956+
$type: HYPERCERT_COLLECTIONS.EVALUATION,
926957
subject: { uri: subject.uri, cid: subject.cid },
927958
evaluators: params.evaluators,
928959
summary: params.summary,
@@ -1007,6 +1038,7 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
10071038
}
10081039

10091040
const collectionRecord: Record<string, unknown> = {
1041+
$type: HYPERCERT_COLLECTIONS.COLLECTION,
10101042
title: params.title,
10111043
claims: params.claims.map((c) => ({ claim: { uri: c.uri, cid: c.cid }, weight: c.weight })),
10121044
createdAt,

packages/sdk-core/src/repository/interfaces.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -190,11 +190,11 @@ export interface CreateHypercertParams {
190190
description?: string;
191191

192192
/**
193-
* Spatial Reference System identifier.
193+
* Spatial Reference System identifier (required if location is provided).
194194
*
195195
* @example "EPSG:4326" for WGS84
196196
*/
197-
srs?: string;
197+
srs: string;
198198

199199
/**
200200
* GeoJSON file as a Blob for precise boundaries.
@@ -663,6 +663,11 @@ export interface HypercertOperations extends EventEmitter<HypercertEvents> {
663663
*
664664
* @param uri - AT-URI of the hypercert
665665
* @param location - Location data
666+
* @param location.value - Location value (address, coordinates, or description)
667+
* @param location.srs - Spatial Reference System (required). Use 'EPSG:4326' for WGS84 lat/lon coordinates.
668+
* @param location.name - Optional human-readable location name
669+
* @param location.description - Optional description of the location
670+
* @param location.geojson - Optional GeoJSON blob for precise boundaries
666671
* @returns Promise resolving to location record result
667672
*/
668673
attachLocation(
@@ -671,7 +676,7 @@ export interface HypercertOperations extends EventEmitter<HypercertEvents> {
671676
value: string;
672677
name?: string;
673678
description?: string;
674-
srs?: string;
679+
srs: string;
675680
geojson?: Blob;
676681
},
677682
): Promise<CreateResult>;

packages/sdk-core/tests/repository/HypercertOperationsImpl.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ describe("HypercertOperationsImpl", () => {
147147

148148
const result = await hypercertOps.create({
149149
...validParams,
150-
location: { value: "New York, NY" },
150+
location: { value: "New York, NY", srs: "EPSG:4326" },
151151
});
152152

153153
expect(result.locationUri).toBe("at://did:plc:test/org.hypercerts.claim.location/ghi");

packages/sdk-react/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hypercerts-org/sdk-react",
3-
"version": "0.6.0",
3+
"version": "0.7.0",
44
"description": "React hooks and components for the Hypercerts ATProto SDK",
55
"type": "module",
66
"main": "dist/index.cjs",

0 commit comments

Comments
 (0)