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