|
@@ -1,909 +1,64 @@
|
|
|
-import { Router, Request, Response } from 'express';
|
|
|
|
|
-import { v4 as uuidv4 } from 'uuid';
|
|
|
|
|
|
|
+import { Router } from 'express';
|
|
|
import { JobQueue } from '../queue/JobQueue';
|
|
import { JobQueue } from '../queue/JobQueue';
|
|
|
-import { ScraperJob } from '../types';
|
|
|
|
|
-import { logger } from '../utils/logger';
|
|
|
|
|
import { ShopDatabase } from '../database/Database';
|
|
import { ShopDatabase } from '../database/Database';
|
|
|
-
|
|
|
|
|
-export function createRouter(jobQueue: JobQueue, db?: ShopDatabase): Router {
|
|
|
|
|
- const router = Router();
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * POST /jobs - Create a new scraping job
|
|
|
|
|
- */
|
|
|
|
|
- router.post('/jobs', async (req: Request, res: Response): Promise<void> => {
|
|
|
|
|
- try {
|
|
|
|
|
- const { url, custom_id } = req.body;
|
|
|
|
|
-
|
|
|
|
|
- if (!url) {
|
|
|
|
|
- res.status(400).json({ error: 'URL is required' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Validate URL format
|
|
|
|
|
- try {
|
|
|
|
|
- new URL(url);
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- res.status(400).json({ error: 'Invalid URL format' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- let shopId: string | undefined;
|
|
|
|
|
-
|
|
|
|
|
- // If database is available, create or find shop
|
|
|
|
|
- if (db) {
|
|
|
|
|
- const { SitemapParser } = require('../scraper/SitemapParser');
|
|
|
|
|
- const { sitemapUrl, webshopType } = SitemapParser.getSitemapUrl(url);
|
|
|
|
|
-
|
|
|
|
|
- let shop = db.getShopByUrl(url);
|
|
|
|
|
- if (!shop) {
|
|
|
|
|
- // Validate custom_id format if provided
|
|
|
|
|
- if (custom_id && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(custom_id)) {
|
|
|
|
|
- res.status(400).json({ error: 'custom_id must be a valid UUID format' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Check if custom_id is already in use
|
|
|
|
|
- if (custom_id && db.getShopByCustomId(custom_id)) {
|
|
|
|
|
- res.status(409).json({ error: 'custom_id is already in use' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- shop = db.createShop(url, sitemapUrl, webshopType, custom_id);
|
|
|
|
|
- logger.info(`Created new shop: ${shop.id}${shop.custom_id ? ` with custom ID ${shop.custom_id}` : ''}`);
|
|
|
|
|
-
|
|
|
|
|
- // Create initial schedule - we'll get more info from sitemap during first scrape
|
|
|
|
|
- const { ScrapeScheduler } = require('../scheduler/ScrapeScheduler');
|
|
|
|
|
- // Note: Initial schedule will be created after first successful scrape
|
|
|
|
|
|
|
+import { EndpointRegistry } from './core/EndpointRegistry';
|
|
|
|
|
+import { authMiddleware } from './middleware/auth';
|
|
|
|
|
+import { BaseEndpointComponent } from './core/types';
|
|
|
|
|
+
|
|
|
|
|
+// Import all endpoint components
|
|
|
|
|
+import { DocumentationEndpoint } from './components/DocumentationEndpoint';
|
|
|
|
|
+import { JobsEndpoint } from './components/JobsEndpoint';
|
|
|
|
|
+import { ShopsEndpoint } from './components/ShopsEndpoint';
|
|
|
|
|
+import { WebhooksEndpoint } from './components/WebhooksEndpoint';
|
|
|
|
|
+import { CustomUrlsEndpoint } from './components/CustomUrlsEndpoint';
|
|
|
|
|
+import { HealthEndpoint } from './components/HealthEndpoint';
|
|
|
|
|
+
|
|
|
|
|
+export function createRouter(jobQueue: JobQueue, apiKey: string, db?: ShopDatabase): Router {
|
|
|
|
|
+ // Create the endpoint registry
|
|
|
|
|
+ const registry = new EndpointRegistry();
|
|
|
|
|
+
|
|
|
|
|
+ // Create auth middleware instance
|
|
|
|
|
+ const auth = authMiddleware(apiKey);
|
|
|
|
|
+
|
|
|
|
|
+ // Register all endpoint components
|
|
|
|
|
+ const components: BaseEndpointComponent[] = [
|
|
|
|
|
+ new HealthEndpoint(jobQueue),
|
|
|
|
|
+ new JobsEndpoint(jobQueue, db),
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ // Only register database-dependent endpoints if database is available
|
|
|
|
|
+ if (db) {
|
|
|
|
|
+ components.push(
|
|
|
|
|
+ new ShopsEndpoint(db),
|
|
|
|
|
+ new WebhooksEndpoint(db),
|
|
|
|
|
+ new CustomUrlsEndpoint(db)
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Register documentation endpoint (needs registry reference)
|
|
|
|
|
+ const docEndpoint = new DocumentationEndpoint(registry);
|
|
|
|
|
+ components.push(docEndpoint);
|
|
|
|
|
+
|
|
|
|
|
+ // Register all endpoints with appropriate middleware
|
|
|
|
|
+ for (const component of components) {
|
|
|
|
|
+ const endpoints = component.getEndpoints();
|
|
|
|
|
+ for (const endpoint of endpoints) {
|
|
|
|
|
+ const middleware: Array<(req: any, res: any, next?: any) => void | Promise<void>> = endpoint.doc.requiresAuth ? [auth, ...(endpoint.middleware || [])] : endpoint.middleware || [];
|
|
|
|
|
+
|
|
|
|
|
+ registry.register({
|
|
|
|
|
+ method: endpoint.method,
|
|
|
|
|
+ path: endpoint.path,
|
|
|
|
|
+ handler: endpoint.handler,
|
|
|
|
|
+ middleware,
|
|
|
|
|
+ documentation: {
|
|
|
|
|
+ ...endpoint.doc,
|
|
|
|
|
+ method: endpoint.method,
|
|
|
|
|
+ path: endpoint.path
|
|
|
}
|
|
}
|
|
|
- shopId = shop.id;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Create new job
|
|
|
|
|
- const job: ScraperJob = {
|
|
|
|
|
- id: uuidv4(),
|
|
|
|
|
- sitemapUrl: url,
|
|
|
|
|
- status: 'pending',
|
|
|
|
|
- createdAt: new Date(),
|
|
|
|
|
- updatedAt: new Date(),
|
|
|
|
|
- shopId
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- jobQueue.addJob(job);
|
|
|
|
|
-
|
|
|
|
|
- logger.info(`New job created: ${job.id} for shop: ${shopId || 'N/A'}`);
|
|
|
|
|
-
|
|
|
|
|
- res.status(201).json({
|
|
|
|
|
- job_id: job.id,
|
|
|
|
|
- shop_id: shopId,
|
|
|
|
|
- status: job.status,
|
|
|
|
|
- message: 'Job created successfully'
|
|
|
|
|
- });
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- logger.error('Error creating job', error);
|
|
|
|
|
- res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * GET /jobs/:id - Get job status and result
|
|
|
|
|
- */
|
|
|
|
|
- router.get('/jobs/:id', (req: Request, res: Response): void => {
|
|
|
|
|
- try {
|
|
|
|
|
- const { id } = req.params;
|
|
|
|
|
- const job = jobQueue.getJob(id);
|
|
|
|
|
-
|
|
|
|
|
- if (!job) {
|
|
|
|
|
- res.status(404).json({ error: 'Job not found' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const response: any = {
|
|
|
|
|
- job_id: job.id,
|
|
|
|
|
- status: job.status,
|
|
|
|
|
- created_at: job.createdAt,
|
|
|
|
|
- updated_at: job.updatedAt
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- if (job.status === 'completed' && job.result) {
|
|
|
|
|
- response.result = job.result;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (job.status === 'failed' && job.error) {
|
|
|
|
|
- response.error = job.error;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- res.json(response);
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- logger.error('Error fetching job', error);
|
|
|
|
|
- res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * GET /jobs - List all jobs
|
|
|
|
|
- */
|
|
|
|
|
- router.get('/jobs', (req: Request, res: Response): void => {
|
|
|
|
|
- try {
|
|
|
|
|
- const jobs = jobQueue.getAllJobs();
|
|
|
|
|
- const stats = jobQueue.getStats();
|
|
|
|
|
-
|
|
|
|
|
- res.json({
|
|
|
|
|
- stats,
|
|
|
|
|
- jobs: jobs.map(job => ({
|
|
|
|
|
- job_id: job.id,
|
|
|
|
|
- status: job.status,
|
|
|
|
|
- sitemap_url: job.sitemapUrl,
|
|
|
|
|
- created_at: job.createdAt,
|
|
|
|
|
- updated_at: job.updatedAt
|
|
|
|
|
- }))
|
|
|
|
|
});
|
|
});
|
|
|
- } catch (error) {
|
|
|
|
|
- logger.error('Error listing jobs', error);
|
|
|
|
|
- res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * GET /shops - List all shops
|
|
|
|
|
- */
|
|
|
|
|
- router.get('/shops', (req: Request, res: Response): void => {
|
|
|
|
|
- try {
|
|
|
|
|
- if (!db) {
|
|
|
|
|
- res.status(503).json({ error: 'Database not available' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const shops = db.getAllShops();
|
|
|
|
|
- const shopsWithAnalytics = shops.map(shop => {
|
|
|
|
|
- const analytics = db.getShopAnalytics(shop.id);
|
|
|
|
|
- return {
|
|
|
|
|
- ...shop,
|
|
|
|
|
- analytics
|
|
|
|
|
- };
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- res.json({ shops: shopsWithAnalytics });
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- logger.error('Error listing shops', error);
|
|
|
|
|
- res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
}
|
|
}
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * GET /shops/:id - Get detailed shop information with analytics (without full results)
|
|
|
|
|
- */
|
|
|
|
|
- router.get('/shops/:id', (req: Request, res: Response): void => {
|
|
|
|
|
- try {
|
|
|
|
|
- if (!db) {
|
|
|
|
|
- res.status(503).json({ error: 'Database not available' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const { id } = req.params;
|
|
|
|
|
- const shop = db.getShopByAnyId(id);
|
|
|
|
|
-
|
|
|
|
|
- if (!shop) {
|
|
|
|
|
- res.status(404).json({ error: 'Shop not found' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Get analytics
|
|
|
|
|
- const analytics = db.getShopAnalytics(shop.id);
|
|
|
|
|
-
|
|
|
|
|
- // Get scrape history
|
|
|
|
|
- const scrapeHistory = db.getScrapeHistory(shop.id, 10);
|
|
|
|
|
-
|
|
|
|
|
- // Get latest content (without full content, just metadata)
|
|
|
|
|
- const latestContent = db.getLatestContentByType(shop.id);
|
|
|
|
|
-
|
|
|
|
|
- // Get scheduled jobs
|
|
|
|
|
- const scheduledJobs = db.getScheduledJobs(shop.id);
|
|
|
|
|
-
|
|
|
|
|
- // Build response with content change markers and URLs only (no content)
|
|
|
|
|
- const contentMetadata: any = {
|
|
|
|
|
- shipping_informations: latestContent.shipping.map(c => ({
|
|
|
|
|
- url: c.url,
|
|
|
|
|
- changed: Boolean(c.changed),
|
|
|
|
|
- last_updated: c.updated_at
|
|
|
|
|
- })),
|
|
|
|
|
- contacts: latestContent.contacts.map(c => ({
|
|
|
|
|
- url: c.url,
|
|
|
|
|
- changed: Boolean(c.changed),
|
|
|
|
|
- last_updated: c.updated_at
|
|
|
|
|
- })),
|
|
|
|
|
- terms_of_conditions: latestContent.terms.map(c => ({
|
|
|
|
|
- url: c.url,
|
|
|
|
|
- changed: Boolean(c.changed),
|
|
|
|
|
- last_updated: c.updated_at
|
|
|
|
|
- })),
|
|
|
|
|
- faq: latestContent.faq.map(c => ({
|
|
|
|
|
- url: c.url,
|
|
|
|
|
- changed: Boolean(c.changed),
|
|
|
|
|
- last_updated: c.updated_at
|
|
|
|
|
- }))
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- res.json({
|
|
|
|
|
- shop: {
|
|
|
|
|
- id: shop.id,
|
|
|
|
|
- custom_id: shop.custom_id,
|
|
|
|
|
- url: shop.url,
|
|
|
|
|
- sitemap_url: shop.sitemap_url,
|
|
|
|
|
- webshop_type: shop.webshop_type,
|
|
|
|
|
- created_at: shop.created_at,
|
|
|
|
|
- updated_at: shop.updated_at
|
|
|
|
|
- },
|
|
|
|
|
- analytics: {
|
|
|
|
|
- total_scrapes: analytics?.total_scrapes || 0,
|
|
|
|
|
- last_scraped_at: analytics?.last_scraped_at || null,
|
|
|
|
|
- next_scrape_at: analytics?.next_scrape_at || null,
|
|
|
|
|
- total_urls_found: analytics?.total_urls_found || 0,
|
|
|
|
|
- average_scrape_time: analytics?.average_scrape_time || null,
|
|
|
|
|
- average_page_scrape_time: analytics?.average_page_scrape_time || null
|
|
|
|
|
- },
|
|
|
|
|
- content_metadata: contentMetadata,
|
|
|
|
|
- scrape_history: scrapeHistory.map(h => ({
|
|
|
|
|
- id: h.id,
|
|
|
|
|
- job_id: h.job_id,
|
|
|
|
|
- started_at: h.started_at,
|
|
|
|
|
- completed_at: h.completed_at,
|
|
|
|
|
- status: h.status,
|
|
|
|
|
- error: h.error,
|
|
|
|
|
- scrape_time_ms: h.scrape_time_ms,
|
|
|
|
|
- urls_found: h.urls_found
|
|
|
|
|
- })),
|
|
|
|
|
- scheduled_jobs: scheduledJobs.map(j => ({
|
|
|
|
|
- id: j.id,
|
|
|
|
|
- next_run_at: j.next_run_at,
|
|
|
|
|
- frequency: j.frequency,
|
|
|
|
|
- last_modified: j.last_modified,
|
|
|
|
|
- status: j.status,
|
|
|
|
|
- enabled: j.enabled,
|
|
|
|
|
- created_at: j.created_at
|
|
|
|
|
- }))
|
|
|
|
|
- });
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- logger.error('Error fetching shop details', error);
|
|
|
|
|
- res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * GET /shops/:id/results - Get shop results with full content (supports filters)
|
|
|
|
|
- */
|
|
|
|
|
- router.get('/shops/:id/results', (req: Request, res: Response): void => {
|
|
|
|
|
- try {
|
|
|
|
|
- if (!db) {
|
|
|
|
|
- res.status(503).json({ error: 'Database not available' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const { id } = req.params;
|
|
|
|
|
- const shop = db.getShopByAnyId(id);
|
|
|
|
|
-
|
|
|
|
|
- if (!shop) {
|
|
|
|
|
- res.status(404).json({ error: 'Shop not found' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Parse query parameters
|
|
|
|
|
- const limit = req.query.limit ? parseInt(req.query.limit as string) : undefined;
|
|
|
|
|
- const dateFrom = req.query.date_from as string | undefined;
|
|
|
|
|
- const dateTo = req.query.date_to as string | undefined;
|
|
|
|
|
- const contentType = req.query.content_type as string | undefined;
|
|
|
|
|
-
|
|
|
|
|
- // Get content with filters
|
|
|
|
|
- const allContent = db.getAllContent(shop.id, {
|
|
|
|
|
- limit,
|
|
|
|
|
- dateFrom,
|
|
|
|
|
- dateTo,
|
|
|
|
|
- contentType
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // Group by type and URL (latest version of each URL)
|
|
|
|
|
- const grouped: { [key: string]: any[] } = {
|
|
|
|
|
- shipping_informations: [],
|
|
|
|
|
- contacts: [],
|
|
|
|
|
- terms_of_conditions: [],
|
|
|
|
|
- faq: []
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- const urlMap = new Map<string, any>();
|
|
|
|
|
- for (const content of allContent) {
|
|
|
|
|
- const key = `${content.content_type}:${content.url}`;
|
|
|
|
|
- if (!urlMap.has(key)) {
|
|
|
|
|
- urlMap.set(key, {
|
|
|
|
|
- url: content.url,
|
|
|
|
|
- content: content.content,
|
|
|
|
|
- changed: Boolean(content.changed),
|
|
|
|
|
- last_updated: content.updated_at
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Organize by type
|
|
|
|
|
- for (const content of allContent) {
|
|
|
|
|
- const key = `${content.content_type}:${content.url}`;
|
|
|
|
|
- const urlKey = urlMap.get(key);
|
|
|
|
|
-
|
|
|
|
|
- if (!urlKey) continue;
|
|
|
|
|
-
|
|
|
|
|
- const typeKey = content.content_type === 'shipping' ? 'shipping_informations' :
|
|
|
|
|
- content.content_type === 'contacts' ? 'contacts' :
|
|
|
|
|
- content.content_type === 'terms' ? 'terms_of_conditions' :
|
|
|
|
|
- 'faq';
|
|
|
|
|
-
|
|
|
|
|
- // Only add if not already added
|
|
|
|
|
- if (!grouped[typeKey].find((c: any) => c.url === urlKey.url)) {
|
|
|
|
|
- grouped[typeKey].push(urlKey);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- res.json({
|
|
|
|
|
- shop_id: shop.id,
|
|
|
|
|
- filters: {
|
|
|
|
|
- limit: limit || null,
|
|
|
|
|
- date_from: dateFrom || null,
|
|
|
|
|
- date_to: dateTo || null,
|
|
|
|
|
- content_type: contentType || null
|
|
|
|
|
- },
|
|
|
|
|
- results: grouped
|
|
|
|
|
- });
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- logger.error('Error fetching shop results', error);
|
|
|
|
|
- res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * PATCH /shops/:id/schedule - Enable or disable scheduled scraping
|
|
|
|
|
- */
|
|
|
|
|
- router.patch('/shops/:id/schedule', (req: Request, res: Response): void => {
|
|
|
|
|
- try {
|
|
|
|
|
- if (!db) {
|
|
|
|
|
- res.status(503).json({ error: 'Database not available' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const { id } = req.params;
|
|
|
|
|
- const { enabled } = req.body;
|
|
|
|
|
-
|
|
|
|
|
- if (typeof enabled !== 'boolean') {
|
|
|
|
|
- res.status(400).json({ error: 'enabled field must be a boolean' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const shop = db.getShopById(id);
|
|
|
|
|
- if (!shop) {
|
|
|
|
|
- res.status(404).json({ error: 'Shop not found' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- db.setScheduleEnabled(shop.id, enabled);
|
|
|
|
|
-
|
|
|
|
|
- res.json({
|
|
|
|
|
- shop_id: shop.id,
|
|
|
|
|
- schedule_enabled: enabled,
|
|
|
|
|
- message: `Schedule ${enabled ? 'enabled' : 'disabled'} successfully`
|
|
|
|
|
- });
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- logger.error('Error updating schedule status', error);
|
|
|
|
|
- res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * PATCH /shops/:id/custom-id - Set or update custom ID for a shop
|
|
|
|
|
- */
|
|
|
|
|
- router.patch('/shops/:id/custom-id', (req: Request, res: Response): void => {
|
|
|
|
|
- try {
|
|
|
|
|
- if (!db) {
|
|
|
|
|
- res.status(503).json({ error: 'Database not available' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const { id } = req.params;
|
|
|
|
|
- const { custom_id } = req.body;
|
|
|
|
|
-
|
|
|
|
|
- const shop = db.getShopByAnyId(id);
|
|
|
|
|
- if (!shop) {
|
|
|
|
|
- res.status(404).json({ error: 'Shop not found' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Validate custom_id format if provided
|
|
|
|
|
- if (custom_id !== null && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(custom_id)) {
|
|
|
|
|
- res.status(400).json({ error: 'custom_id must be a valid UUID format or null' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Check if custom_id is already in use by another shop
|
|
|
|
|
- if (custom_id) {
|
|
|
|
|
- const existingShop = db.getShopByCustomId(custom_id);
|
|
|
|
|
- if (existingShop && existingShop.id !== shop.id) {
|
|
|
|
|
- res.status(409).json({ error: 'custom_id is already in use' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const success = db.setCustomId(shop.id, custom_id);
|
|
|
|
|
- if (!success) {
|
|
|
|
|
- res.status(500).json({ error: 'Failed to update custom_id' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- res.json({
|
|
|
|
|
- shop_id: shop.id,
|
|
|
|
|
- custom_id: custom_id,
|
|
|
|
|
- message: custom_id ? `Custom ID set to ${custom_id}` : 'Custom ID removed'
|
|
|
|
|
- });
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- logger.error('Error updating custom ID', error);
|
|
|
|
|
- res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * DELETE /shops/:id - Delete a shop and all related data
|
|
|
|
|
- */
|
|
|
|
|
- router.delete('/shops/:id', (req: Request, res: Response): void => {
|
|
|
|
|
- try {
|
|
|
|
|
- if (!db) {
|
|
|
|
|
- res.status(503).json({ error: 'Database not available' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const { id } = req.params;
|
|
|
|
|
- const shop = db.getShopByAnyId(id);
|
|
|
|
|
-
|
|
|
|
|
- if (!shop) {
|
|
|
|
|
- res.status(404).json({ error: 'Shop not found' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- db.deleteShop(shop.id);
|
|
|
|
|
-
|
|
|
|
|
- res.json({
|
|
|
|
|
- message: 'Shop and all related data deleted successfully',
|
|
|
|
|
- shop_id: shop.id
|
|
|
|
|
- });
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- logger.error('Error deleting shop', error);
|
|
|
|
|
- res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * POST /shops/:id/webhooks - Create or replace webhook for a shop
|
|
|
|
|
- */
|
|
|
|
|
- router.post('/shops/:id/webhooks', (req: Request, res: Response): void => {
|
|
|
|
|
- try {
|
|
|
|
|
- if (!db) {
|
|
|
|
|
- res.status(503).json({ error: 'Database not available' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const { id } = req.params;
|
|
|
|
|
- const { url, secret } = req.body;
|
|
|
|
|
-
|
|
|
|
|
- if (!url) {
|
|
|
|
|
- res.status(400).json({ error: 'url field is required' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Validate URL format
|
|
|
|
|
- try {
|
|
|
|
|
- new URL(url);
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- res.status(400).json({ error: 'Invalid URL format' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const shop = db.getShopById(id);
|
|
|
|
|
- if (!shop) {
|
|
|
|
|
- res.status(404).json({ error: 'Shop not found' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Check if webhook already exists
|
|
|
|
|
- const existingWebhook = db.getWebhook(shop.id);
|
|
|
|
|
- if (existingWebhook) {
|
|
|
|
|
- // Update existing webhook
|
|
|
|
|
- db.updateWebhook(shop.id, url, secret);
|
|
|
|
|
- const updated = db.getWebhook(shop.id);
|
|
|
|
|
-
|
|
|
|
|
- res.json({
|
|
|
|
|
- message: 'Webhook updated successfully',
|
|
|
|
|
- webhook: {
|
|
|
|
|
- id: updated!.id,
|
|
|
|
|
- shop_id: updated!.shop_id,
|
|
|
|
|
- url: updated!.url,
|
|
|
|
|
- enabled: updated!.enabled,
|
|
|
|
|
- created_at: updated!.created_at,
|
|
|
|
|
- updated_at: updated!.updated_at
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- } else {
|
|
|
|
|
- // Create new webhook
|
|
|
|
|
- const webhookId = db.createWebhook(shop.id, url, secret);
|
|
|
|
|
- const webhook = db.getWebhook(shop.id);
|
|
|
|
|
-
|
|
|
|
|
- res.status(201).json({
|
|
|
|
|
- message: 'Webhook created successfully',
|
|
|
|
|
- webhook: {
|
|
|
|
|
- id: webhook!.id,
|
|
|
|
|
- shop_id: webhook!.shop_id,
|
|
|
|
|
- url: webhook!.url,
|
|
|
|
|
- enabled: webhook!.enabled,
|
|
|
|
|
- created_at: webhook!.created_at,
|
|
|
|
|
- updated_at: webhook!.updated_at
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- logger.error('Error creating/updating webhook', error);
|
|
|
|
|
- res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * GET /shops/:id/webhooks - Get webhook configuration for a shop
|
|
|
|
|
- */
|
|
|
|
|
- router.get('/shops/:id/webhooks', (req: Request, res: Response): void => {
|
|
|
|
|
- try {
|
|
|
|
|
- if (!db) {
|
|
|
|
|
- res.status(503).json({ error: 'Database not available' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const { id } = req.params;
|
|
|
|
|
- const shop = db.getShopByAnyId(id);
|
|
|
|
|
-
|
|
|
|
|
- if (!shop) {
|
|
|
|
|
- res.status(404).json({ error: 'Shop not found' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const webhook = db.getWebhook(shop.id);
|
|
|
|
|
-
|
|
|
|
|
- if (!webhook) {
|
|
|
|
|
- res.status(404).json({ error: 'No webhook configured for this shop' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Get recent deliveries
|
|
|
|
|
- const deliveries = db.getWebhookDeliveries(shop.id, 10);
|
|
|
|
|
-
|
|
|
|
|
- res.json({
|
|
|
|
|
- webhook: {
|
|
|
|
|
- id: webhook.id,
|
|
|
|
|
- shop_id: webhook.shop_id,
|
|
|
|
|
- url: webhook.url,
|
|
|
|
|
- enabled: webhook.enabled,
|
|
|
|
|
- created_at: webhook.created_at,
|
|
|
|
|
- updated_at: webhook.updated_at
|
|
|
|
|
- },
|
|
|
|
|
- recent_deliveries: deliveries.map(d => ({
|
|
|
|
|
- id: d.id,
|
|
|
|
|
- event: d.event,
|
|
|
|
|
- status: d.status,
|
|
|
|
|
- response_code: d.response_code,
|
|
|
|
|
- error: d.error,
|
|
|
|
|
- attempted_at: d.attempted_at
|
|
|
|
|
- }))
|
|
|
|
|
- });
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- logger.error('Error fetching webhook', error);
|
|
|
|
|
- res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * PATCH /shops/:id/webhooks - Enable or disable webhook
|
|
|
|
|
- */
|
|
|
|
|
- router.patch('/shops/:id/webhooks', (req: Request, res: Response): void => {
|
|
|
|
|
- try {
|
|
|
|
|
- if (!db) {
|
|
|
|
|
- res.status(503).json({ error: 'Database not available' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const { id } = req.params;
|
|
|
|
|
- const { enabled } = req.body;
|
|
|
|
|
-
|
|
|
|
|
- if (typeof enabled !== 'boolean') {
|
|
|
|
|
- res.status(400).json({ error: 'enabled field must be a boolean' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const shop = db.getShopById(id);
|
|
|
|
|
- if (!shop) {
|
|
|
|
|
- res.status(404).json({ error: 'Shop not found' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const webhook = db.getWebhook(shop.id);
|
|
|
|
|
- if (!webhook && enabled) {
|
|
|
|
|
- res.status(404).json({ error: 'No webhook configured for this shop' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- db.setWebhookEnabled(shop.id, enabled);
|
|
|
|
|
-
|
|
|
|
|
- res.json({
|
|
|
|
|
- shop_id: shop.id,
|
|
|
|
|
- webhook_enabled: enabled,
|
|
|
|
|
- message: `Webhook ${enabled ? 'enabled' : 'disabled'} successfully`
|
|
|
|
|
- });
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- logger.error('Error updating webhook status', error);
|
|
|
|
|
- res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * DELETE /shops/:id/webhooks - Delete webhook for a shop
|
|
|
|
|
- */
|
|
|
|
|
- router.delete('/shops/:id/webhooks', (req: Request, res: Response): void => {
|
|
|
|
|
- try {
|
|
|
|
|
- if (!db) {
|
|
|
|
|
- res.status(503).json({ error: 'Database not available' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const { id } = req.params;
|
|
|
|
|
- const shop = db.getShopByAnyId(id);
|
|
|
|
|
-
|
|
|
|
|
- if (!shop) {
|
|
|
|
|
- res.status(404).json({ error: 'Shop not found' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const webhook = db.getWebhook(shop.id);
|
|
|
|
|
- if (!webhook) {
|
|
|
|
|
- res.status(404).json({ error: 'No webhook configured for this shop' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- db.deleteWebhook(shop.id);
|
|
|
|
|
-
|
|
|
|
|
- res.json({
|
|
|
|
|
- message: 'Webhook deleted successfully',
|
|
|
|
|
- shop_id: shop.id
|
|
|
|
|
- });
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- logger.error('Error deleting webhook', error);
|
|
|
|
|
- res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * POST /shops/:id/custom-urls - Add a custom URL to scrape for a shop
|
|
|
|
|
- */
|
|
|
|
|
- router.post('/shops/:id/custom-urls', (req: Request, res: Response): void => {
|
|
|
|
|
- try {
|
|
|
|
|
- if (!db) {
|
|
|
|
|
- res.status(503).json({ error: 'Database not available' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const { id } = req.params;
|
|
|
|
|
- const { url, content_type } = req.body;
|
|
|
|
|
-
|
|
|
|
|
- // Validate required fields
|
|
|
|
|
- if (!url || !content_type) {
|
|
|
|
|
- res.status(400).json({ error: 'url and content_type fields are required' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Validate content_type
|
|
|
|
|
- const validContentTypes = ['shipping', 'contacts', 'terms', 'faq'];
|
|
|
|
|
- if (!validContentTypes.includes(content_type)) {
|
|
|
|
|
- res.status(400).json({
|
|
|
|
|
- error: 'content_type must be one of: shipping, contacts, terms, faq'
|
|
|
|
|
- });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Validate URL format
|
|
|
|
|
- let parsedUrl: URL;
|
|
|
|
|
- try {
|
|
|
|
|
- parsedUrl = new URL(url);
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- res.status(400).json({ error: 'Invalid URL format' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Check if shop exists
|
|
|
|
|
- const shop = db.getShopById(id);
|
|
|
|
|
- if (!shop) {
|
|
|
|
|
- res.status(404).json({ error: 'Shop not found' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Validate that the URL belongs to the same domain as the shop
|
|
|
|
|
- const shopUrl = new URL(shop.url);
|
|
|
|
|
- if (parsedUrl.hostname !== shopUrl.hostname) {
|
|
|
|
|
- res.status(400).json({
|
|
|
|
|
- error: `URL must belong to the same domain as the shop (${shopUrl.hostname})`
|
|
|
|
|
- });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Check if URL was already scraped
|
|
|
|
|
- const alreadyScraped = db.isUrlAlreadyScraped(shop.id, url);
|
|
|
|
|
- if (alreadyScraped) {
|
|
|
|
|
- res.status(409).json({
|
|
|
|
|
- error: 'This URL has already been scraped from the sitemap',
|
|
|
|
|
- message: 'URL already exists in scraped content'
|
|
|
|
|
- });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Create custom URL
|
|
|
|
|
- try {
|
|
|
|
|
- const customUrl = db.createCustomUrl(shop.id, url, content_type);
|
|
|
|
|
-
|
|
|
|
|
- res.status(201).json({
|
|
|
|
|
- message: 'Custom URL added successfully',
|
|
|
|
|
- custom_url: {
|
|
|
|
|
- id: customUrl.id,
|
|
|
|
|
- shop_id: customUrl.shop_id,
|
|
|
|
|
- url: customUrl.url,
|
|
|
|
|
- content_type: customUrl.content_type,
|
|
|
|
|
- enabled: customUrl.enabled,
|
|
|
|
|
- created_at: customUrl.created_at
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- } catch (error: any) {
|
|
|
|
|
- if (error.message?.includes('already been added')) {
|
|
|
|
|
- res.status(409).json({ error: error.message });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- throw error;
|
|
|
|
|
- }
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- logger.error('Error creating custom URL', error);
|
|
|
|
|
- res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * GET /shops/:id/custom-urls - List all custom URLs for a shop
|
|
|
|
|
- */
|
|
|
|
|
- router.get('/shops/:id/custom-urls', (req: Request, res: Response): void => {
|
|
|
|
|
- try {
|
|
|
|
|
- if (!db) {
|
|
|
|
|
- res.status(503).json({ error: 'Database not available' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const { id } = req.params;
|
|
|
|
|
- const shop = db.getShopByAnyId(id);
|
|
|
|
|
-
|
|
|
|
|
- if (!shop) {
|
|
|
|
|
- res.status(404).json({ error: 'Shop not found' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const customUrls = db.getCustomUrls(shop.id);
|
|
|
|
|
-
|
|
|
|
|
- res.json({
|
|
|
|
|
- shop_id: shop.id,
|
|
|
|
|
- custom_urls: customUrls.map(cu => ({
|
|
|
|
|
- id: cu.id,
|
|
|
|
|
- url: cu.url,
|
|
|
|
|
- content_type: cu.content_type,
|
|
|
|
|
- enabled: cu.enabled,
|
|
|
|
|
- created_at: cu.created_at
|
|
|
|
|
- }))
|
|
|
|
|
- });
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- logger.error('Error fetching custom URLs', error);
|
|
|
|
|
- res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * PATCH /shops/:id/custom-urls/:customUrlId - Enable or disable a custom URL
|
|
|
|
|
- */
|
|
|
|
|
- router.patch('/shops/:id/custom-urls/:customUrlId', (req: Request, res: Response): void => {
|
|
|
|
|
- try {
|
|
|
|
|
- if (!db) {
|
|
|
|
|
- res.status(503).json({ error: 'Database not available' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const { id, customUrlId } = req.params;
|
|
|
|
|
- const { enabled } = req.body;
|
|
|
|
|
-
|
|
|
|
|
- if (typeof enabled !== 'boolean') {
|
|
|
|
|
- res.status(400).json({ error: 'enabled field must be a boolean' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const shop = db.getShopById(id);
|
|
|
|
|
- if (!shop) {
|
|
|
|
|
- res.status(404).json({ error: 'Shop not found' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const customUrl = db.getCustomUrlById(customUrlId);
|
|
|
|
|
- if (!customUrl) {
|
|
|
|
|
- res.status(404).json({ error: 'Custom URL not found' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (customUrl.shop_id !== shop.id) {
|
|
|
|
|
- res.status(403).json({ error: 'Custom URL does not belong to this shop' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- db.setCustomUrlEnabled(customUrlId, enabled);
|
|
|
|
|
-
|
|
|
|
|
- res.json({
|
|
|
|
|
- message: `Custom URL ${enabled ? 'enabled' : 'disabled'} successfully`,
|
|
|
|
|
- custom_url_id: customUrlId,
|
|
|
|
|
- enabled
|
|
|
|
|
- });
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- logger.error('Error updating custom URL', error);
|
|
|
|
|
- res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * DELETE /shops/:id/custom-urls/:customUrlId - Delete a custom URL
|
|
|
|
|
- */
|
|
|
|
|
- router.delete('/shops/:id/custom-urls/:customUrlId', (req: Request, res: Response): void => {
|
|
|
|
|
- try {
|
|
|
|
|
- if (!db) {
|
|
|
|
|
- res.status(503).json({ error: 'Database not available' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const { id, customUrlId } = req.params;
|
|
|
|
|
-
|
|
|
|
|
- const shop = db.getShopById(id);
|
|
|
|
|
- if (!shop) {
|
|
|
|
|
- res.status(404).json({ error: 'Shop not found' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const customUrl = db.getCustomUrlById(customUrlId);
|
|
|
|
|
- if (!customUrl) {
|
|
|
|
|
- res.status(404).json({ error: 'Custom URL not found' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (customUrl.shop_id !== shop.id) {
|
|
|
|
|
- res.status(403).json({ error: 'Custom URL does not belong to this shop' });
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- db.deleteCustomUrl(customUrlId);
|
|
|
|
|
-
|
|
|
|
|
- res.json({
|
|
|
|
|
- message: 'Custom URL deleted successfully',
|
|
|
|
|
- custom_url_id: customUrlId
|
|
|
|
|
- });
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- logger.error('Error deleting custom URL', error);
|
|
|
|
|
- res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- /**
|
|
|
|
|
- * GET /health - Health check endpoint
|
|
|
|
|
- */
|
|
|
|
|
- router.get('/health', (req: Request, res: Response): void => {
|
|
|
|
|
- res.json({
|
|
|
|
|
- status: 'ok',
|
|
|
|
|
- timestamp: new Date().toISOString(),
|
|
|
|
|
- queue_stats: jobQueue.getStats()
|
|
|
|
|
- });
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- return router;
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ // Build and return the router
|
|
|
|
|
+ return registry.buildRouter();
|
|
|
|
|
+}
|