diff --git a/docs/getting-valid-plan-ids.md b/docs/getting-valid-plan-ids.md index 798c713..fabcf7e 100644 --- a/docs/getting-valid-plan-ids.md +++ b/docs/getting-valid-plan-ids.md @@ -78,6 +78,12 @@ Plans are created by importing JWT claims containing a `PlanAction`: - JWT must be signed with a valid DID key - Response includes `handleId` and `planId` if creation succeeds +**Implementation Guide**: See [`docs/hydrate-plan-implementation-guide.md`](./hydrate-plan-implementation-guide.md) for complete implementation details, including: +- `hydratePlan()` function pattern +- PlanActionClaim interface structure +- Helper functions for create/edit operations +- Usage examples and testing recommendations + **Note**: This method requires generating signed JWTs with DID keys, which is complex. **Method 1 (TimeSafari App UI) is recommended** for creating test plans. ## Plan Handle ID Format diff --git a/docs/hydrate-plan-implementation-guide.md b/docs/hydrate-plan-implementation-guide.md new file mode 100644 index 0000000..68d727f --- /dev/null +++ b/docs/hydrate-plan-implementation-guide.md @@ -0,0 +1,564 @@ +# 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 + diff --git a/docs/localhost-testing-guide.md b/docs/localhost-testing-guide.md index b8714d6..a2c2b7e 100644 --- a/docs/localhost-testing-guide.md +++ b/docs/localhost-testing-guide.md @@ -70,6 +70,8 @@ If you have your own localhost API server, you must create plans first. Plans ar **Note**: Creating PlanAction JWTs requires DID signing and proper claim structure, which is complex. For quick testing, **Option A (test API server) is recommended**. +**Implementation Reference**: If you need to programmatically create PlanAction JWTs, see [`docs/hydrate-plan-implementation-guide.md`](./hydrate-plan-implementation-guide.md) for the complete implementation pattern, including `hydratePlan()` function and helper utilities. + Then verify your setup: ```bash