15 KiB
Directive: hydratePlan Implementation Guide
Author: Matthew Raymer Date: 2025-10-29 12:38:40 UTC Status: 🎯 ACTIVE - Implementation directive for PlanActionClaim hydration
Overview
This directive provides a complete implementation guide for creating a
hydratePlan() function that follows the same pattern as hydrateGive() and
hydrateOffer(). This function constructs and hydrates PlanActionClaim
entities (projects) for submission to the endorser server.
Pattern Reference
The hydratePlan() function should follow the established pattern from:
hydrateGive()(src/libs/endorserServer.ts:831-920)hydrateOffer()(src/libs/endorserServer.ts:1018-1072)
Key Pattern Elements
- Function Signature: Accepts optional original claim and parameters
- Clone or Create: Clone existing claim or create new base structure
- Edit Detection: Handle
lastClaimIdfor edit operations - Field Hydration: Set optional fields only when provided
- Cleanup: Use
undefinedto remove fields (not empty strings) - Return: Return properly typed claim object
PlanActionClaim Interface
export interface PlanActionClaim extends ClaimObject {
"@context": "https://schema.org";
"@type": "PlanAction";
name: string; // REQUIRED
agent?: { identifier: string }; // Optional: creator DID
description?: string; // Optional: project description
endTime?: string; // Optional: ISO 8601 date string
identifier?: string; // Auto-set by server on first create
image?: string; // Optional: image URL
lastClaimId?: string; // Required for edits
location?: { // Optional: geographic location
geo: {
"@type": "GeoCoordinates";
latitude: number;
longitude: number;
};
};
startTime?: string; // Optional: ISO 8601 date string
url?: string; // Optional: project URL
}
Implementation
Function Signature
/**
* Construct PlanAction VC for submission to server
*
* Creates or updates a PlanAction claim (project) with all optional fields
* properly handled. Follows the same pattern as hydrateGive() and
* hydrateOffer().
*
* @param vcClaimOrig - Optional existing claim to clone and modify
* @param name - Required project name
* @param agentDid - Optional DID of the project creator
* @param description - Optional project description
* @param startTime - Optional ISO 8601 start date/time string
* @param endTime - Optional ISO 8601 end date/time string
* @param imageUrl - Optional image URL for the project
* @param url - Optional project URL
* @param latitude - Optional location latitude
* @param longitude - Optional location longitude
* @param lastClaimId - Required when editing an existing project
*
* @returns Properly hydrated PlanActionClaim ready for submission
*
* @example
* // Create new project
* const claim = hydratePlan(
* undefined,
* "Community Garden Project",
* "did:ethr:0x123...",
* "Building a community garden",
* "2025-06-01T00:00:00Z",
* "2025-08-31T23:59:59Z"
* );
*
* @example
* // Edit existing project
* const updated = hydratePlan(
* existingClaim,
* "Updated Project Name",
* "did:ethr:0x123...",
* "New description",
* undefined,
* undefined,
* undefined,
* undefined,
* undefined,
* undefined,
* "01ABC123..." // lastClaimId
* );
*/
export function hydratePlan(
vcClaimOrig?: PlanActionClaim,
name?: string,
agentDid?: string,
description?: string,
startTime?: string,
endTime?: string,
imageUrl?: string,
url?: string,
latitude?: number,
longitude?: number,
lastClaimId?: string,
): PlanActionClaim {
// Clone existing claim or create base structure
const vcClaim: PlanActionClaim = vcClaimOrig
? R.clone(vcClaimOrig)
: {
"@context": SCHEMA_ORG_CONTEXT,
"@type": "PlanAction",
};
// Handle edit operation (lastClaimId indicates edit)
if (lastClaimId) {
vcClaim.lastClaimId = lastClaimId;
// Remove identifier on edit - server assigns new one
delete vcClaim.identifier;
}
// Set required name field (must be provided for new claims)
if (name) {
vcClaim.name = name;
}
// Handle agent (creator DID)
if (agentDid) {
vcClaim.agent = { identifier: agentDid };
} else {
// Remove agent if not provided (allows clearing)
delete vcClaim.agent;
}
// Handle description
vcClaim.description = description || undefined;
// Handle start time (ISO 8601 format required)
if (startTime) {
// Validate it's a proper ISO 8601 string
try {
const date = new Date(startTime);
if (isNaN(date.getTime())) {
throw new Error("Invalid startTime format");
}
vcClaim.startTime = startTime;
} catch {
// Invalid date - remove the field
delete vcClaim.startTime;
}
} else {
delete vcClaim.startTime;
}
// Handle end time (ISO 8601 format required)
if (endTime) {
try {
const date = new Date(endTime);
if (isNaN(date.getTime())) {
throw new Error("Invalid endTime format");
}
vcClaim.endTime = endTime;
} catch {
delete vcClaim.endTime;
}
} else {
delete vcClaim.endTime;
}
// Handle image URL
vcClaim.image = imageUrl || undefined;
// Handle project URL
vcClaim.url = url || undefined;
// Handle location (both latitude and longitude required)
if (latitude !== undefined && longitude !== undefined) {
// Validate coordinates are numbers
if (
typeof latitude === "number" &&
typeof longitude === "number" &&
!isNaN(latitude) &&
!isNaN(longitude)
) {
vcClaim.location = {
geo: {
"@type": "GeoCoordinates",
latitude,
longitude,
},
};
} else {
// Invalid coordinates - remove location
delete vcClaim.location;
}
} else {
// Remove location if either coordinate missing
delete vcClaim.location;
}
return vcClaim;
}
Helper Functions (Recommended)
Following the pattern from createAndSubmitGive() and
createAndSubmitOffer(), create wrapper functions:
Create and Submit
/**
* Create and submit a new PlanAction claim
*
* @param axios - Axios instance for HTTP requests
* @param apiServer - Endorser API server URL
* @param issuerDid - DID to sign the claim (must match agentDid or
* be authorized)
* @param name - Required project name
* @param agentDid - Optional creator DID
* @param description - Optional project description
* @param startTime - Optional ISO 8601 start time
* @param endTime - Optional ISO 8601 end time
* @param imageUrl - Optional image URL
* @param url - Optional project URL
* @param latitude - Optional location latitude
* @param longitude - Optional location longitude
*
* @returns Promise with submission result (handleId, claimId, etc.)
*/
export async function createAndSubmitPlan(
axios: Axios,
apiServer: string,
issuerDid: string,
name: string,
agentDid?: string,
description?: string,
startTime?: string,
endTime?: string,
imageUrl?: string,
url?: string,
latitude?: number,
longitude?: number,
): Promise<CreateAndSubmitClaimResult> {
const vcClaim = hydratePlan(
undefined,
name,
agentDid,
description,
startTime,
endTime,
imageUrl,
url,
latitude,
longitude,
undefined, // No lastClaimId for new claims
);
return createAndSubmitClaim(
vcClaim as GenericVerifiableCredential,
issuerDid,
apiServer,
axios,
);
}
Edit and Submit
/**
* Edit and submit an existing PlanAction claim
*
* @param axios - Axios instance for HTTP requests
* @param apiServer - Endorser API server URL
* @param fullClaim - Existing claim wrapper with ID
* @param issuerDid - DID to sign the claim
* @param name - Updated project name (if changed)
* @param agentDid - Updated creator DID (if changed)
* @param description - Updated description (if changed)
* @param startTime - Updated start time (if changed)
* @param endTime - Updated end time (if changed)
* @param imageUrl - Updated image URL (if changed)
* @param url - Updated project URL (if changed)
* @param latitude - Updated location latitude (if changed)
* @param longitude - Updated location longitude (if changed)
*
* @returns Promise with submission result
*/
export async function editAndSubmitPlan(
axios: Axios,
apiServer: string,
fullClaim: GenericCredWrapper<PlanActionClaim>,
issuerDid: string,
name?: string,
agentDid?: string,
description?: string,
startTime?: string,
endTime?: string,
imageUrl?: string,
url?: string,
latitude?: number,
longitude?: number,
): Promise<CreateAndSubmitClaimResult> {
const vcClaim = hydratePlan(
fullClaim.claim,
name,
agentDid,
description,
startTime,
endTime,
imageUrl,
url,
latitude,
longitude,
fullClaim.id, // Use existing claim ID for edit
);
return createAndSubmitClaim(
vcClaim as GenericVerifiableCredential,
issuerDid,
apiServer,
axios,
);
}
Required Dependencies
Ensure these imports are available:
import * as R from "ramda"; // For R.clone()
import { Axios } from "axios";
import { PlanActionClaim } from "../interfaces/claims";
import {
GenericCredWrapper,
GenericVerifiableCredential,
CreateAndSubmitClaimResult
} from "../interfaces/common";
// Constants
export const SCHEMA_ORG_CONTEXT = "https://schema.org";
Usage Examples
Example 1: Create Simple Project
const claim = hydratePlan(
undefined, // New claim
"My Project", // Required name
"did:ethr:0x123...", // Creator DID
"Project description" // Description
);
// Result:
// {
// "@context": "https://schema.org",
// "@type": "PlanAction",
// "name": "My Project",
// "agent": { "identifier": "did:ethr:0x123..." },
// "description": "Project description"
// }
Example 2: Create Project with Dates and Location
const claim = hydratePlan(
undefined,
"Community Event",
"did:ethr:0x456...",
"Annual community gathering",
"2025-07-01T10:00:00Z", // Start time
"2025-07-03T18:00:00Z", // End time
"https://example.com/img.jpg", // Image
"https://example.com/event", // URL
40.7128, // Latitude
-74.0060 // Longitude
);
Example 3: Edit Existing Project
const existingClaim: PlanActionClaim = {
"@context": "https://schema.org",
"@type": "PlanAction",
"name": "Old Name",
"identifier": "01ABC123...",
// ... other fields
};
const updated = hydratePlan(
existingClaim, // Original claim to clone
"New Name", // Updated name
"did:ethr:0x123...", // Keep same agent
"Updated description", // Updated description
undefined, // Keep existing startTime
undefined, // Keep existing endTime
undefined, // Keep existing image
undefined, // Keep existing url
undefined, // Keep existing location
undefined, // Keep existing location
"01ABC123..." // Required: lastClaimId
);
// Result will have:
// - lastClaimId: "01ABC123..."
// - identifier: undefined (removed for edit)
// - Updated name and description
// - All other fields preserved from original
Example 4: Remove Location from Existing Project
const updated = hydratePlan(
existingClaim,
existingClaim.name, // Keep name
existingClaim.agent?.identifier,
existingClaim.description,
existingClaim.startTime,
existingClaim.endTime,
existingClaim.image,
existingClaim.url,
undefined, // latitude undefined = remove location
undefined, // longitude undefined = remove location
"01ABC123..."
);
Important Notes
Date/Time Handling
- Format: All date/time strings must be ISO 8601 format
- Validation: Function validates date strings but caller should ensure proper format
- Timezone: Include timezone information (Z for UTC, or +/- offset)
- Example:
"2025-06-01T10:00:00Z"or"2025-06-01T10:00:00-05:00"
Location Handling
- Both Required: Both
latitudeandlongitudemust be provided together, or both omitted - Validation: Coordinates must be valid numbers (not NaN)
- Removal: Setting either to
undefinedremoves the entire location object - Range: Latitude: -90 to 90, Longitude: -180 to 180 (validation recommended)
Edit Operations
- lastClaimId Required: Must provide
lastClaimIdwhen editing - identifier Removed: Server assigns new identifier on edit
- Field Preservation: Only explicitly changed fields are modified; others preserved from original
- Undefined vs Missing: Use
undefinedto explicitly remove optional fields
Field Cleanup
- Use undefined: Set fields to
undefinedrather than empty strings - Delete for Edit: For edits, explicitly delete fields that should be removed
- Optional Field Pattern: Check existence before setting to preserve optional nature
Testing Recommendations
Unit Tests
Test the following scenarios:
-
New Claim Creation
- Minimum required fields (name only)
- All optional fields provided
- Validate structure matches interface
-
Edit Operations
- Clone existing claim correctly
- lastClaimId sets identifier removal
- Partial field updates preserve other fields
-
Field Validation
- Invalid date strings removed
- Location requires both coordinates
- Undefined fields properly handled
-
Edge Cases
- Empty string handling (should become undefined)
- NaN coordinate values
- Missing required fields on new claims
Integration Tests
Test with actual server submission:
// Test createAndSubmitPlan
const result = await createAndSubmitPlan(
axiosInstance,
"https://test-api.endorser.ch",
testDid,
"Test Project",
testDid,
"Test description"
);
expect(result.success).toBe(true);
expect(result.handleId).toBeDefined();
Security Considerations
- DID Validation: Validate agentDid matches issuerDid or is authorized
- Date Validation: Ensure date strings don't contain injection attempts
- URL Validation: Validate image and URL fields are proper URLs
- Coordinate Validation: Ensure lat/lng are within valid ranges
References
- hydrateGive():
src/libs/endorserServer.ts:831-920 - hydrateOffer():
src/libs/endorserServer.ts:1018-1072 - PlanActionClaim Interface:
src/interfaces/claims.ts:76-91 - Pattern Example:
src/views/NewEditProjectView.vue:536-597
Implementation Checklist
- Implement
hydratePlan()function with all parameters - Add date/time validation logic
- Add location coordinate validation
- Implement
createAndSubmitPlan()wrapper - Implement
editAndSubmitPlan()wrapper - Add comprehensive JSDoc comments
- Write unit tests for all scenarios
- Write integration tests with server
- Update any existing code to use new function
- Document breaking changes if refactoring existing code
Status: Active implementation directive Priority: Medium Estimated Effort: 2-4 hours for complete implementation including tests Dependencies: Requires ramda, axios, and existing claim interfaces Stakeholders: Development team implementing claim hydration utilities