You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

329 lines
11 KiB

#!/usr/bin/env node
/**
* Seed Test Projects for Localhost Testing
*
* Creates test project data for localhost API server testing.
* Can be run standalone or integrated into your local dev server.
*
* @author Matthew Raymer
* @version 1.0.0
*/
const http = require('http');
/**
* Generate a valid ULID (26 characters, Crockford base32)
* Simplified version for testing - not cryptographically secure
*/
function generateULID() {
const timestamp = Date.now();
// Crockford base32 alphabet (0-9, A-Z excluding I, L, O, U)
const alphabet = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
const timestampPart = timestamp.toString(36).toUpperCase().padStart(10, '0');
const randomPart = Array.from({ length: 16 }, () =>
alphabet[Math.floor(Math.random() * alphabet.length)]
).join('');
return (timestampPart + randomPart).substring(0, 26);
}
/**
* Generate a valid plan handle ID in URI format
* Default format: https://endorser.ch/entity/{ULID}
*/
function generateValidPlanHandleId() {
return `https://endorser.ch/entity/${generateULID()}`;
}
/**
* Generate a test JWT ID with timestamp prefix for sorting
*/
function generateJwtId(timestamp = Date.now()) {
const random = Math.random().toString(36).substring(2, 8);
const hash = Math.random().toString(36).substring(2, 10);
return `${Math.floor(timestamp / 1000)}_${random}_${hash}`;
}
/**
* Generate test project data
*/
function generateTestProject(handleId, index = 0) {
const now = Date.now();
const startTime = new Date(now + (index * 86400000)); // 1 day apart
const endTime = new Date(startTime.getTime() + (30 * 86400000)); // 30 days later
return {
jwtId: generateJwtId(now + (index * 1000)),
handleId: handleId,
name: `Test Project ${index + 1}`,
description: `This is test project ${index + 1} for localhost prefetch testing. Created for User Zero starred plans querying.`,
issuerDid: "did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F",
agentDid: "did:test:agent_" + index,
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
locLat: 40.7128 + (index * 0.1), // Vary location slightly
locLon: -74.0060 + (index * 0.1),
url: `https://test-project-${index + 1}.timesafari.test`,
version: "1.0.0"
};
}
/**
* Generate complete test project with previous claim
*/
function generateTestProjectWithClaim(handleId, index = 0) {
const project = generateTestProject(handleId, index);
const previousClaimJwtId = generateJwtId(Date.now() - (86400000 * 2)); // 2 days ago
return {
planSummary: project,
previousClaim: {
jwtId: previousClaimJwtId,
claimType: "project_update",
claimData: {
message: `Previous update for ${handleId}`,
version: "0.9.0"
},
metadata: {
createdAt: new Date(Date.now() - (86400000 * 2)).toISOString(),
updatedAt: new Date(Date.now() - (86400000)).toISOString()
}
}
};
}
/**
* Default test project IDs from config
*
* Plan handle IDs must be valid URIs (RFC 3986 format):
* - Default format: https://endorser.ch/entity/{26-char-ULID}
* - ULID is 26 characters, Crockford base32 encoded
* - Any valid URI scheme is accepted: http://, https://, did:, etc.
*
* For real testing, use actual plan handle IDs from your TimeSafari database.
* To get valid IDs:
* 1. Create projects in TimeSafari app and note handleId from API responses
* 2. Query your local database: SELECT handleId FROM plans LIMIT 5
* 3. Check starred projects: settings.starredPlanHandleIds
*
* The IDs below are auto-generated valid URIs for testing structure only.
* Replace with real IDs from your database for actual prefetch testing.
*/
const DEFAULT_TEST_PROJECT_IDS = [
// Auto-generate valid format IDs for testing
generateValidPlanHandleId(),
generateValidPlanHandleId(),
generateValidPlanHandleId(),
generateValidPlanHandleId(),
generateValidPlanHandleId()
];
/**
* Generate all test projects
*/
function generateAllTestProjects(projectIds = DEFAULT_TEST_PROJECT_IDS) {
return projectIds.map((handleId, index) =>
generateTestProjectWithClaim(handleId, index)
);
}
/**
* Seed projects to localhost API server
*
* **IMPORTANT**: The TimeSafari API creates plans via POST `/api/v2/claim` with PlanAction JWTs.
* This function attempts to POST to `/api/test/seed-projects` if your API has a custom test seed endpoint.
*
* For real TimeSafari APIs:
* 1. Create plans via the TimeSafari app UI (recommended)
* 2. Import PlanAction JWTs via POST `/api/v2/claim` (requires DID signing)
* 3. Use direct database inserts (not recommended)
*
* This seed function is primarily for custom test endpoints or the test-api-server-with-seed.js
*/
function seedToLocalhost(apiUrl, projectIds = DEFAULT_TEST_PROJECT_IDS) {
return new Promise((resolve, reject) => {
const projects = generateAllTestProjects(projectIds);
console.log(`📦 Generating ${projects.length} test projects...`);
projects.forEach((project, index) => {
console.log(` ${index + 1}. ${project.planSummary.handleId} - ${project.planSummary.name}`);
});
console.log('');
console.log('⚠️ NOTE: This attempts to POST to /api/test/seed-projects');
console.log(' If your API doesn\'t have this endpoint, create plans via:');
console.log(' 1. TimeSafari App UI (easiest)');
console.log(' 2. POST /api/v2/claim with PlanAction JWT (requires DID signing)');
console.log(' 3. Direct database inserts');
console.log('');
// If your API has a seed endpoint, use this:
const seedUrl = `${apiUrl}/api/test/seed-projects`;
const postData = JSON.stringify({ projects });
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData)
}
};
const req = http.request(seedUrl, options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
if (res.statusCode === 200 || res.statusCode === 201) {
console.log('✅ Test projects seeded successfully');
console.log(`📊 Response: ${data}`);
resolve(JSON.parse(data));
} else {
console.error(`❌ Seed failed: ${res.statusCode} ${res.statusMessage}`);
console.error(`Response: ${data}`);
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
}
});
});
req.on('error', (error) => {
console.error(`❌ Request error: ${error.message}`);
reject(error);
});
req.write(postData);
req.end();
});
}
/**
* Generate mock API server response data
*
* Returns the data structure that your localhost API should return
*/
function getMockApiResponse(projectIds = DEFAULT_TEST_PROJECT_IDS, afterId = null) {
const allProjects = generateAllTestProjects(projectIds);
// Filter by afterId if provided (simple comparison by jwtId)
let filteredProjects = allProjects;
if (afterId) {
const afterIndex = allProjects.findIndex(p => p.planSummary.jwtId === afterId);
if (afterIndex >= 0) {
filteredProjects = allProjects.slice(afterIndex + 1);
} else {
// If afterId not found, return all (for testing)
filteredProjects = allProjects;
}
}
const nextAfterId = filteredProjects.length > 0
? filteredProjects[filteredProjects.length - 1].planSummary.jwtId
: null;
return {
data: filteredProjects,
hitLimit: false,
pagination: {
hasMore: false,
nextAfterId: nextAfterId
}
};
}
/**
* Export project data as JSON file
*/
function exportToJSON(filename = 'test-projects.json', projectIds = DEFAULT_TEST_PROJECT_IDS) {
const fs = require('fs');
const projects = generateAllTestProjects(projectIds);
const data = {
projects: projects,
generatedAt: new Date().toISOString(),
count: projects.length
};
fs.writeFileSync(filename, JSON.stringify(data, null, 2));
console.log(`💾 Exported ${projects.length} test projects to ${filename}`);
return data;
}
// CLI usage
if (require.main === module) {
const args = process.argv.slice(2);
const command = args[0];
switch (command) {
case 'export':
{
const filename = args[1] || 'test-projects.json';
const projectIds = args[2] ? args[2].split(',') : DEFAULT_TEST_PROJECT_IDS;
// Warn if using auto-generated IDs (they'll be valid URIs but not from real database)
console.log('📝 Using provided plan handle IDs.');
console.log(' Note: For real testing, use IDs from your TimeSafari database.');
console.log(' Valid format: https://endorser.ch/entity/{ULID} or any valid URI');
console.log('');
exportToJSON(filename, projectIds);
}
break;
case 'seed':
{
const apiUrl = args[1] || 'http://localhost:3000';
const projectIds = args[2] ? args[2].split(',') : DEFAULT_TEST_PROJECT_IDS;
// Validate plan IDs are valid URIs (RFC 3986)
const invalidIds = projectIds.filter(id => !id.match(/^[A-Za-z][A-Za-z0-9+.-]+:/));
if (invalidIds.length > 0) {
console.error('❌ ERROR: Invalid plan handle ID format.');
console.error(' Plan handle IDs must be valid URIs (RFC 3986).');
console.error(' Format: https://endorser.ch/entity/{ULID}');
console.error(' Example: https://endorser.ch/entity/01GQBE7Q0RQQAGJMEEW6RSGKTF');
console.error(` Invalid IDs: ${invalidIds.join(', ')}`);
console.error('');
console.error(' Usage: node scripts/seed-test-projects.js seed <apiUrl> "<uri1>,<uri2>,<uri3>"');
process.exit(1);
}
seedToLocalhost(apiUrl, projectIds)
.then(() => {
console.log('✅ Seeding complete');
process.exit(0);
})
.catch((error) => {
console.error('❌ Seeding failed:', error.message);
process.exit(1);
});
}
break;
case 'generate':
default:
{
const projectIds = args[1] ? args[1].split(',') : DEFAULT_TEST_PROJECT_IDS;
console.log('📝 Generated test projects with valid URI format handle IDs.');
console.log(' Format: https://endorser.ch/entity/{ULID}');
console.log(' For real testing, replace with IDs from your TimeSafari database.');
const projects = generateAllTestProjects(projectIds);
console.log(JSON.stringify({ data: projects }, null, 2));
}
break;
}
}
module.exports = {
generateTestProject,
generateTestProjectWithClaim,
generateAllTestProjects,
getMockApiResponse,
seedToLocalhost,
exportToJSON,
DEFAULT_TEST_PROJECT_IDS,
generateJwtId
};