docs(testing): add PlanAction JWT hydration implementation guide
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.
This commit is contained in:
@@ -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
|
||||
|
||||
564
docs/hydrate-plan-implementation-guide.md
Normal file
564
docs/hydrate-plan-implementation-guide.md
Normal file
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user