Browse Source

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.
master
Matthew Raymer 3 days ago
parent
commit
ed5dcfbbd1
  1. 6
      docs/getting-valid-plan-ids.md
  2. 564
      docs/hydrate-plan-implementation-guide.md
  3. 2
      docs/localhost-testing-guide.md

6
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

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

2
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

Loading…
Cancel
Save