Browse Source
Add comprehensive implementation guide for creating PlanAction claims via hydratePlan() function pattern, following established hydrateGive() and hydrateOffer() patterns. Changes: - Add hydrate-plan-implementation-guide.md with complete implementation details, usage examples, and testing recommendations - Link implementation guide from getting-valid-plan-ids.md Method 6 - Link implementation guide from localhost-testing-guide.md Option B The guide provides: - Complete hydratePlan() function implementation - PlanActionClaim interface structure - Helper functions (createAndSubmitPlan, editAndSubmitPlan) - Usage examples and edge cases - Testing recommendations and security considerations This complements plan creation documentation by showing how to programmatically construct valid PlanAction JWTs for POST /api/v2/claim.master
3 changed files with 572 additions and 0 deletions
@ -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<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 |
||||
|
|
||||
|
```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<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: |
||||
|
|
||||
|
```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 |
||||
|
|
||||
Loading…
Reference in new issue