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