You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

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

  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

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;
}

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 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:

// 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