# 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 1. **Function Signature**: Accepts optional original claim and parameters 2. **Clone or Create**: Clone existing claim or create new base structure 3. **Edit Detection**: Handle `lastClaimId` for edit operations 4. **Field Hydration**: Set optional fields only when provided 5. **Cleanup**: Use `undefined` to remove fields (not empty strings) 6. **Return**: Return properly typed claim object ## PlanActionClaim Interface ```typescript 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 ```typescript /** * 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 ```typescript /** * 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 { 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 ```typescript /** * 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, issuerDid: string, name?: string, agentDid?: string, description?: string, startTime?: string, endTime?: string, imageUrl?: string, url?: string, latitude?: number, longitude?: number, ): Promise { 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: ```typescript 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 ```typescript 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 ```typescript 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 ```typescript 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 ```typescript 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 `latitude` and `longitude` must be provided together, or both omitted - **Validation**: Coordinates must be valid numbers (not NaN) - **Removal**: Setting either to `undefined` removes the entire location object - **Range**: Latitude: -90 to 90, Longitude: -180 to 180 (validation recommended) ### Edit Operations - **lastClaimId Required**: Must provide `lastClaimId` when 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 `undefined` to explicitly remove optional fields ### Field Cleanup - **Use undefined**: Set fields to `undefined` rather 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: 1. **New Claim Creation** - Minimum required fields (name only) - All optional fields provided - Validate structure matches interface 2. **Edit Operations** - Clone existing claim correctly - lastClaimId sets identifier removal - Partial field updates preserve other fields 3. **Field Validation** - Invalid date strings removed - Location requires both coordinates - Undefined fields properly handled 4. **Edge Cases** - Empty string handling (should become undefined) - NaN coordinate values - Missing required fields on new claims ### Integration Tests Test with actual server submission: ```typescript // 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 1. **DID Validation**: Validate agentDid matches issuerDid or is authorized 2. **Date Validation**: Ensure date strings don't contain injection attempts 3. **URL Validation**: Validate image and URL fields are proper URLs 4. **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