Skip to content

Commit 61fe2cf

Browse files
committed
feat: add updateMeasurement function
1 parent 2e03d8d commit 61fe2cf

File tree

7 files changed

+360
-24
lines changed

7 files changed

+360
-24
lines changed

.changeset/update-measurement-api.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ Update measurement API to align with lexicon beta.12+ schema
1616
**Breaking Changes (sdk-react):**
1717

1818
- Removed `OrgHypercertsDefs` export (WorkScope types removed from lexicon)
19-
- `UpdateHypercertParams.workScope` now accepts `string | { uri: string; cid: string } | { $type: string }`
19+
- `UpdateHypercertParams.workScope` now accepts `string | StrongRef` (i.e., `string | { uri: string; cid: string }`)
2020

2121
**New Features:**
2222

23+
- Added `updateMeasurement()` method with `UpdateMeasurementParams` type (subject is immutable)
2324
- Support for `locations` array to specify where measurements were taken
2425
- Added `startDate` and `endDate` for measurement timeframes
2526
- Added `methodType` for short methodology identifiers

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

Lines changed: 115 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
type UpdateCollectionParams,
3636
type UpdateProjectParams,
3737
type CreateMeasurementParams,
38+
type UpdateMeasurementParams,
3839
} from "../services/hypercerts/types.js";
3940
import type {
4041
CreateHypercertEvidenceParams,
@@ -1190,6 +1191,45 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
11901191
}
11911192
}
11921193

1194+
/**
1195+
* Applies updates to an existing measurement record, preserving immutable fields.
1196+
*
1197+
* @param existing - The existing measurement record
1198+
* @param updates - The updates to apply
1199+
* @returns Promise resolving to the updated measurement record
1200+
* @internal
1201+
*/
1202+
private async applyMeasurementUpdates(
1203+
existing: HypercertMeasurement,
1204+
updates: UpdateMeasurementParams,
1205+
): Promise<HypercertMeasurement> {
1206+
const record: HypercertMeasurement = {
1207+
...existing,
1208+
// Preserve immutable fields
1209+
$type: existing.$type,
1210+
subject: existing.subject,
1211+
createdAt: existing.createdAt,
1212+
};
1213+
1214+
if (updates.metric !== undefined) record.metric = updates.metric;
1215+
if (updates.unit !== undefined) record.unit = updates.unit;
1216+
if (updates.value !== undefined) record.value = updates.value;
1217+
if (updates.startDate !== undefined) record.startDate = updates.startDate;
1218+
if (updates.endDate !== undefined) record.endDate = updates.endDate;
1219+
if (updates.methodType !== undefined) record.methodType = updates.methodType;
1220+
if (updates.methodURI !== undefined) record.methodURI = updates.methodURI;
1221+
if (updates.evidenceURI !== undefined) record.evidenceURI = updates.evidenceURI;
1222+
if (updates.measurers !== undefined) record.measurers = updates.measurers;
1223+
if (updates.comment !== undefined) record.comment = updates.comment;
1224+
if (updates.commentFacets !== undefined) record.commentFacets = updates.commentFacets;
1225+
1226+
if (updates.locations !== undefined) {
1227+
record.locations = await this.processLocations(updates.locations);
1228+
}
1229+
1230+
return record;
1231+
}
1232+
11931233
/**
11941234
* Processes contribution parameters, creating contributor/contribution records if necessary.
11951235
*
@@ -1530,7 +1570,7 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
15301570
* @example Basic measurement
15311571
* ```typescript
15321572
* await repo.hypercerts.addMeasurement({
1533-
* subjectUri: hypercertUri,
1573+
* subject: hypercertUri,
15341574
* metric: "Carbon Offset",
15351575
* unit: "tons CO2e",
15361576
* value: "150",
@@ -1549,31 +1589,26 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
15491589
* locations: [{ uri: "at://...", cid: "..." }],
15501590
* measurers: ["did:plc:auditor"],
15511591
* methodType: "satellite-imagery",
1552-
* methodUri: "https://example.com/methodology",
1553-
* evidenceUris: ["https://example.com/audit-report"],
1592+
* methodURI: "https://example.com/methodology",
1593+
* evidenceURI: ["https://example.com/audit-report"],
15541594
* comment: "Verified via satellite imagery",
15551595
* });
15561596
* ```
15571597
*/
15581598
async addMeasurement(params: CreateMeasurementParams): Promise<CreateResult> {
15591599
try {
1560-
// Resolve subject to get CID
15611600
const subject = await this.resolveToStrongRef(params.subject);
15621601
const createdAt = new Date().toISOString();
15631602

1564-
// Resolve locations if provided (reuse existing processLocations helper)
15651603
const locationRefs = await this.processLocations(params.locations);
15661604

1567-
// Cast to Record<string, unknown> to avoid type mismatches between
1568-
// different Bluesky client packages (@atproto/api vs @atcute/bluesky)
15691605
const measurementRecord: HypercertMeasurement = {
15701606
$type: HYPERCERT_COLLECTIONS.MEASUREMENT,
15711607
subject: { uri: subject.uri, cid: subject.cid },
15721608
metric: params.metric,
15731609
unit: params.unit,
15741610
value: params.value,
15751611
createdAt,
1576-
// Optional fields
15771612
startDate: params.startDate,
15781613
endDate: params.endDate,
15791614
locations: locationRefs,
@@ -1607,6 +1642,78 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
16071642
}
16081643
}
16091644

1645+
/**
1646+
* Updates a measurement record.
1647+
*
1648+
* Note: The `subject` field is immutable and cannot be changed after creation.
1649+
*
1650+
* @param uri - AT-URI of the measurement to update
1651+
* @param updates - Fields to update (subject is excluded as it's immutable)
1652+
* @returns Promise resolving to updated measurement URI and CID
1653+
* @throws {@link ValidationError} if validation fails or URI format is invalid
1654+
* @throws {@link NetworkError} if the measurement is not found or update fails
1655+
*
1656+
* @example Updating a measurement value
1657+
* ```typescript
1658+
* await repo.hypercerts.updateMeasurement(
1659+
* "at://did:plc:test/org.hypercerts.claim.measurement/xyz",
1660+
* { value: "200", comment: "Re-verified measurement" }
1661+
* );
1662+
* ```
1663+
*/
1664+
async updateMeasurement(uri: string, updates: UpdateMeasurementParams): Promise<UpdateResult> {
1665+
try {
1666+
const uriMatch = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
1667+
if (!uriMatch) {
1668+
throw new ValidationError(`Invalid URI format: ${uri}`);
1669+
}
1670+
const [, , collection, rkey] = uriMatch;
1671+
1672+
if (collection !== HYPERCERT_COLLECTIONS.MEASUREMENT) {
1673+
throw new ValidationError(
1674+
`URI must target a measurement collection. Expected '${HYPERCERT_COLLECTIONS.MEASUREMENT}', got '${collection}'`,
1675+
);
1676+
}
1677+
1678+
const existing = await this.agent.com.atproto.repo.getRecord({
1679+
repo: this.repoDid,
1680+
collection,
1681+
rkey,
1682+
});
1683+
1684+
if (!existing.success) {
1685+
throw new NetworkError(`Measurement not found: ${uri}`);
1686+
}
1687+
1688+
const recordForUpdate = await this.applyMeasurementUpdates(existing.data.value as HypercertMeasurement, updates);
1689+
1690+
const validation = validate(recordForUpdate, HYPERCERT_COLLECTIONS.MEASUREMENT, "main", false);
1691+
if (!validation.success) {
1692+
throw new ValidationError(`Invalid measurement record: ${validation.error?.message}`);
1693+
}
1694+
1695+
const result = await this.agent.com.atproto.repo.putRecord({
1696+
repo: this.repoDid,
1697+
collection,
1698+
rkey,
1699+
record: recordForUpdate,
1700+
});
1701+
1702+
if (!result.success) {
1703+
throw new NetworkError("Failed to update measurement");
1704+
}
1705+
1706+
this.emit("measurementUpdated", { uri: result.data.uri, cid: result.data.cid });
1707+
return { uri: result.data.uri, cid: result.data.cid };
1708+
} catch (error) {
1709+
if (error instanceof ValidationError || error instanceof NetworkError) throw error;
1710+
throw new NetworkError(
1711+
`Failed to update measurement: ${error instanceof Error ? error.message : "Unknown"}`,
1712+
error,
1713+
);
1714+
}
1715+
}
1716+
16101717
/**
16111718
* Creates an evaluation record for a hypercert or other subject.
16121719
*

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

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type {
2121
UpdateCollectionParams,
2222
UpdateProjectParams,
2323
CreateMeasurementParams,
24+
UpdateMeasurementParams,
2425
StrongRef,
2526
} from "../services/hypercerts/types.js";
2627
import type {
@@ -844,6 +845,11 @@ export interface HypercertEvents {
844845
* Emitted when a location is removed from a project.
845846
*/
846847
locationRemovedFromProject: { projectUri: string };
848+
849+
/**
850+
* Emitted when a measurement is updated.
851+
*/
852+
measurementUpdated: { uri: string; cid: string };
847853
}
848854

849855
/**
@@ -976,7 +982,7 @@ export interface HypercertOperations extends EventEmitter<HypercertEvents> {
976982
* @example Basic measurement
977983
* ```typescript
978984
* await repo.hypercerts.addMeasurement({
979-
* subjectUri: hypercertUri,
985+
* subject: hypercertUri,
980986
* metric: "Carbon Offset",
981987
* unit: "tons CO2e",
982988
* value: "150",
@@ -995,14 +1001,35 @@ export interface HypercertOperations extends EventEmitter<HypercertEvents> {
9951001
* locations: [{ uri: "at://...", cid: "..." }],
9961002
* measurers: ["did:plc:auditor"],
9971003
* methodType: "satellite-imagery",
998-
* methodUri: "https://example.com/methodology",
999-
* evidenceUris: ["https://example.com/audit-report"],
1004+
* methodURI: "https://example.com/methodology",
1005+
* evidenceURI: ["https://example.com/audit-report"],
10001006
* comment: "Verified via satellite imagery",
10011007
* });
10021008
* ```
10031009
*/
10041010
addMeasurement(params: CreateMeasurementParams): Promise<CreateResult>;
10051011

1012+
/**
1013+
* Updates a measurement record.
1014+
*
1015+
* Note: The `subject` field is immutable and cannot be changed after creation.
1016+
*
1017+
* @param uri - AT-URI of the measurement to update
1018+
* @param updates - Fields to update (subject is excluded as it's immutable)
1019+
* @returns Promise resolving to updated measurement URI and CID
1020+
* @throws {@link ValidationError} if validation fails or URI format is invalid
1021+
* @throws {@link NetworkError} if the measurement is not found or update fails
1022+
*
1023+
* @example Updating a measurement value
1024+
* ```typescript
1025+
* await repo.hypercerts.updateMeasurement(
1026+
* "at://did:plc:test/org.hypercerts.claim.measurement/xyz",
1027+
* { value: "200", comment: "Re-verified measurement" }
1028+
* );
1029+
* ```
1030+
*/
1031+
updateMeasurement(uri: string, updates: UpdateMeasurementParams): Promise<UpdateResult>;
1032+
10061033
/**
10071034
* Creates an evaluation record.
10081035
*

packages/sdk-core/src/services/hypercerts/types.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
// Re-export BlobRef from ATProto lexicon
1010
export { BlobRef } from "@atproto/lexicon";
1111
export type { JsonBlobRef } from "@atproto/lexicon";
12-
import type { OverrideProperties, SetOptional } from "type-fest";
12+
import type { Except, OverrideProperties, SetOptional } from "type-fest";
1313

1414
// Re-export everything from lexicon package
1515
export {
@@ -385,6 +385,14 @@ export type CreateProjectResult = CreateCollectionResult;
385385
// Measurement Types
386386
// ============================================================================
387387

388+
/**
389+
* SDK type for measurement records.
390+
* Alias for the lexicon Main type, used in SDK operations.
391+
*
392+
* @see HypercertMeasurement - Equivalent alias
393+
*/
394+
export type Measurement = OrgHypercertsClaimMeasurement.Main;
395+
388396
/**
389397
* SDK input parameters for creating a measurement.
390398
*
@@ -419,6 +427,7 @@ export type CreateProjectResult = CreateCollectionResult;
419427
* };
420428
* ```
421429
*/
430+
422431
export type CreateMeasurementParams = OverrideProperties<
423432
SetOptional<OrgHypercertsClaimMeasurement.Main, "$type" | "createdAt">,
424433
{
@@ -437,3 +446,56 @@ export type CreateMeasurementParams = OverrideProperties<
437446
locations?: LocationParams[];
438447
}
439448
>;
449+
450+
/**
451+
* SDK input parameters for updating a measurement.
452+
* All fields are optional for partial/patch updates.
453+
*
454+
* Note: `subject` is excluded as it is immutable after creation.
455+
*
456+
* @example Updating a measurement's value
457+
* ```typescript
458+
* const updateParams: UpdateMeasurementParams = {
459+
* value: "200",
460+
* comment: "Updated measurement after re-verification",
461+
* };
462+
* ```
463+
*
464+
* @example Updating locations
465+
* ```typescript
466+
* const updateParams: UpdateMeasurementParams = {
467+
* locations: [{ uri: "at://did:plc:.../app.certified.location/new", cid: "..." }],
468+
* };
469+
* ```
470+
*/
471+
export type UpdateMeasurementParams = Partial<Except<CreateMeasurementParams, "subject">>;
472+
473+
/**
474+
* Union type for referencing measurements in SDK methods.
475+
*
476+
* Accepts:
477+
* 1. **string** - AT-URI pointing to an existing measurement record
478+
* 2. **StrongRef** - Direct reference with uri and cid
479+
* 3. **CreateMeasurementParams** - Inline measurement object for creation
480+
*
481+
* @example Using an AT-URI
482+
* ```typescript
483+
* const measurement: MeasurementParams = "at://did:plc:abc/org.hypercerts.claim.measurement/xyz";
484+
* ```
485+
*
486+
* @example Using a StrongRef
487+
* ```typescript
488+
* const measurement: MeasurementParams = { uri: "at://...", cid: "bafyrei..." };
489+
* ```
490+
*
491+
* @example Using inline params
492+
* ```typescript
493+
* const measurement: MeasurementParams = {
494+
* subject: "at://did:plc:abc/org.hypercerts.claim.activity/xyz",
495+
* metric: "Carbon Offset",
496+
* unit: "tons CO2e",
497+
* value: "150",
498+
* };
499+
* ```
500+
*/
501+
export type MeasurementParams = string | StrongRef | CreateMeasurementParams;

0 commit comments

Comments
 (0)