diff --git a/package.runtime.json b/package.runtime.json index de88964..81015a3 100644 --- a/package.runtime.json +++ b/package.runtime.json @@ -9,7 +9,9 @@ "dotenv": "^16.5.0", "sql.js": "^1.13.0", "uuid": "^10.0.0", - "axios": "^1.7.7" + "axios": "^1.7.7", + "openai": "^4.77.0", + "zod": "^3.24.1" }, "engines": { "node": ">=16.0.0" diff --git a/src/templates/template-repository.ts b/src/templates/template-repository.ts index db780c8..1a28c37 100644 --- a/src/templates/template-repository.ts +++ b/src/templates/template-repository.ts @@ -809,4 +809,85 @@ export class TemplateRepository { const results = this.db.prepare(query).all(complexity, limit, offset) as StoredTemplate[]; return results.map(t => this.decompressWorkflow(t)); } + + /** + * Get count of templates matching metadata search + */ + getSearchTemplatesByMetadataCount(filters: { + category?: string; + complexity?: 'simple' | 'medium' | 'complex'; + maxSetupMinutes?: number; + minSetupMinutes?: number; + requiredService?: string; + targetAudience?: string; + }): number { + let sql = ` + SELECT COUNT(*) as count FROM templates + WHERE metadata_json IS NOT NULL + `; + const params: any[] = []; + + if (filters.category) { + sql += ` AND json_extract(metadata_json, '$.categories') LIKE ?`; + params.push(`%"${filters.category}"%`); + } + + if (filters.complexity) { + sql += ` AND json_extract(metadata_json, '$.complexity') = ?`; + params.push(filters.complexity); + } + + if (filters.maxSetupMinutes !== undefined) { + sql += ` AND CAST(json_extract(metadata_json, '$.estimated_setup_minutes') AS INTEGER) <= ?`; + params.push(filters.maxSetupMinutes); + } + + if (filters.minSetupMinutes !== undefined) { + sql += ` AND CAST(json_extract(metadata_json, '$.estimated_setup_minutes') AS INTEGER) >= ?`; + params.push(filters.minSetupMinutes); + } + + if (filters.requiredService) { + sql += ` AND json_extract(metadata_json, '$.required_services') LIKE ?`; + params.push(`%"${filters.requiredService}"%`); + } + + if (filters.targetAudience) { + sql += ` AND json_extract(metadata_json, '$.target_audience') LIKE ?`; + params.push(`%"${filters.targetAudience}"%`); + } + + const result = this.db.prepare(sql).get(...params) as { count: number }; + return result?.count || 0; + } + + /** + * Get unique categories from metadata + */ + getUniqueCategories(): string[] { + const sql = ` + SELECT DISTINCT value as category + FROM templates, json_each(metadata_json, '$.categories') + WHERE metadata_json IS NOT NULL + ORDER BY category + `; + + const results = this.db.prepare(sql).all() as { category: string }[]; + return results.map(r => r.category); + } + + /** + * Get unique target audiences from metadata + */ + getUniqueTargetAudiences(): string[] { + const sql = ` + SELECT DISTINCT value as audience + FROM templates, json_each(metadata_json, '$.target_audience') + WHERE metadata_json IS NOT NULL + ORDER BY audience + `; + + const results = this.db.prepare(sql).all() as { audience: string }[]; + return results.map(r => r.audience); + } } \ No newline at end of file diff --git a/tests/unit/services/template-service.test.ts b/tests/unit/services/template-service.test.ts index d9517e4..f99f29b 100644 --- a/tests/unit/services/template-service.test.ts +++ b/tests/unit/services/template-service.test.ts @@ -350,8 +350,8 @@ describe('TemplateService', () => { expect(result).toEqual({ items: [ - { id: 1, name: 'Template A', views: 200, nodeCount: 2 }, - { id: 2, name: 'Template B', views: 150, nodeCount: 1 } + { id: 1, name: 'Template A', description: 'Description for template 1', views: 200, nodeCount: 2 }, + { id: 2, name: 'Template B', description: 'Description for template 2', views: 150, nodeCount: 1 } ], total: 50, limit: 10, diff --git a/tests/unit/templates/metadata-generator.test.ts b/tests/unit/templates/metadata-generator.test.ts index b560e79..8e24636 100644 --- a/tests/unit/templates/metadata-generator.test.ts +++ b/tests/unit/templates/metadata-generator.test.ts @@ -370,13 +370,20 @@ describe('MetadataGenerator', () => { }); it('should handle network timeouts gracefully in generateSingle', async () => { - // Mock OpenAI to simulate timeout - const mockClient = generator['client']; - const originalCreate = mockClient.chat.completions.create; + // Create a new generator with mocked OpenAI client + const mockClient = { + chat: { + completions: { + create: vi.fn().mockRejectedValue(new Error('Request timed out')) + } + } + }; - mockClient.chat.completions.create = vi.fn().mockRejectedValue( - new Error('Request timed out') - ); + // Override the client property using Object.defineProperty + Object.defineProperty(generator, 'client', { + value: mockClient, + writable: true + }); const template: MetadataRequest = { templateId: 555, @@ -388,9 +395,6 @@ describe('MetadataGenerator', () => { // Should return default metadata instead of throwing expect(result).toEqual(generator['getDefaultMetadata']()); - - // Restore original method - mockClient.chat.completions.create = originalCreate; }); });