#!/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 * * Makes POST requests to create projects in your local API */ 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}`); }); // 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 ",,"'); 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 };