Source: services/sparql/SPARQLHelper.js

/**
 * SPARQLHelper - Utility for creating SPARQL UPDATE queries and executing them
 * 
 * This helper provides simple templating methods for creating UPDATE queries
 * and posting them to SPARQL update endpoints.
 */

import fetch from 'node-fetch';
import logger from 'loglevel';

export default class SPARQLHelper {
    /**
     * @param {string} updateEndpoint - SPARQL update endpoint URL
     * @param {Object} options - Configuration options
     */
    constructor(updateEndpoint, options = {}) {
        this.updateEndpoint = updateEndpoint;
        this.options = {
            auth: options.auth || null, // { user: 'admin', password: 'admin123' }
            timeout: options.timeout || 30000,
            headers: options.headers || {},
            ...options
        };
    }

    /**
     * Create INSERT DATA query template
     * 
     * @param {string} graph - Named graph URI
     * @param {string} triples - RDF triples in Turtle syntax
     * @returns {string} - SPARQL UPDATE query
     */
    createInsertDataQuery(graph, triples) {
        return `
PREFIX ragno: <http://purl.org/stuff/ragno/>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
PREFIX dcterms: <http://purl.org/dc/terms/>
PREFIX prov: <http://www.w3.org/ns/prov#>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
PREFIX owl: <http://www.w3.org/2002/07/owl#>
PREFIX semem: <http://semem.hyperdata.it/>

INSERT DATA {
    GRAPH <${graph}> {
        ${triples}
    }
}`;
    }

    /**
     * Create INSERT query template with WHERE clause
     * 
     * @param {string} graph - Named graph URI
     * @param {string} insertTriples - Triples to insert
     * @param {string} whereClause - WHERE clause conditions
     * @returns {string} - SPARQL UPDATE query
     */
    createInsertQuery(graph, insertTriples, whereClause = '') {
        const whereSection = whereClause ? `WHERE {\n    ${whereClause}\n}` : '';
        
        return `
PREFIX ragno: <http://purl.org/stuff/ragno/>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
PREFIX dcterms: <http://purl.org/dc/terms/>
PREFIX prov: <http://www.w3.org/ns/prov#>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
PREFIX owl: <http://www.w3.org/2002/07/owl#>
PREFIX semem: <http://semem.hyperdata.it/>

INSERT {
    GRAPH <${graph}> {
        ${insertTriples}
    }
} ${whereSection}`;
    }

    /**
     * Create DELETE/INSERT query template
     * 
     * @param {string} graph - Named graph URI
     * @param {string} deleteTriples - Triples to delete
     * @param {string} insertTriples - Triples to insert
     * @param {string} whereClause - WHERE clause conditions
     * @returns {string} - SPARQL UPDATE query
     */
    createDeleteInsertQuery(graph, deleteTriples, insertTriples, whereClause) {
        return `
PREFIX ragno: <http://purl.org/stuff/ragno/>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
PREFIX dcterms: <http://purl.org/dc/terms/>
PREFIX prov: <http://www.w3.org/ns/prov#>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
PREFIX owl: <http://www.w3.org/2002/07/owl#>
PREFIX semem: <http://semem.hyperdata.it/>

DELETE {
    GRAPH <${graph}> {
        ${deleteTriples}
    }
}
INSERT {
    GRAPH <${graph}> {
        ${insertTriples}
    }
}
WHERE {
    ${whereClause}
}`;
    }

    /**
     * Create CLEAR GRAPH query
     * 
     * @param {string} graph - Named graph URI to clear
     * @returns {string} - SPARQL UPDATE query
     */
    createClearGraphQuery(graph) {
        return `CLEAR GRAPH <${graph}>`;
    }

    /**
     * Create DROP GRAPH query
     * 
     * @param {string} graph - Named graph URI to drop
     * @returns {string} - SPARQL UPDATE query
     */
    createDropGraphQuery(graph) {
        return `DROP GRAPH <${graph}>`;
    }

    /**
     * Execute SPARQL UPDATE query via HTTP POST
     * 
     * @param {string} query - SPARQL UPDATE query
     * @returns {Promise<Object>} - Response object with success/error status
     */
    async executeUpdate(query) {
        const startTime = Date.now();
        
        try {
            logger.info('Executing SPARQL UPDATE:', this.updateEndpoint);
            logger.debug('Query:', query.substring(0, 500) + (query.length > 500 ? '...' : ''));

            // Prepare headers
            const headers = {
                'Content-Type': 'application/sparql-update',
                'Accept': 'application/json, text/plain',
                ...this.options.headers
            };

            // Add Basic Authentication if provided
            if (this.options.auth) {
                const credentials = Buffer.from(`${this.options.auth.user}:${this.options.auth.password}`).toString('base64');
                headers['Authorization'] = `Basic ${credentials}`;
            }

            // Execute HTTP POST request
            const response = await fetch(this.updateEndpoint, {
                method: 'POST',
                headers,
                body: query,
                timeout: this.options.timeout
            });

            const responseTime = Date.now() - startTime;
            
            if (!response.ok) {
                const errorText = await response.text();
                logger.error(`SPARQL UPDATE failed (${response.status}): ${response.statusText}`);
                logger.error('Error response:', errorText);
                
                return {
                    success: false,
                    status: response.status,
                    statusText: response.statusText,
                    error: errorText,
                    responseTime
                };
            }

            const responseText = await response.text();
            logger.info(`SPARQL UPDATE completed successfully (${responseTime}ms)`);
            
            return {
                success: true,
                status: response.status,
                statusText: response.statusText,
                response: responseText,
                responseTime
            };

        } catch (error) {
            const responseTime = Date.now() - startTime;
            logger.error('SPARQL UPDATE request failed:', error.message);
            
            return {
                success: false,
                error: error.message,
                responseTime
            };
        }
    }

    /**
     * Execute SPARQL SELECT query
     * 
     * @param {string} query - SPARQL SELECT query
     * @returns {Promise<Object>} - Response object with results
     */
    async executeSelect(query) {
        const startTime = Date.now();
        
        try {
            logger.info('Executing SPARQL SELECT query...');
            logger.debug('Query:', query);

            // Convert update endpoint to query endpoint
            const queryEndpoint = this.updateEndpoint.replace('/update', '/query');

            // Prepare headers
            const headers = {
                'Content-Type': 'application/sparql-query',
                'Accept': 'application/sparql-results+json',
                ...this.options.headers
            };

            // Add Basic Authentication if provided
            if (this.options.auth) {
                const credentials = Buffer.from(`${this.options.auth.user}:${this.options.auth.password}`).toString('base64');
                headers['Authorization'] = `Basic ${credentials}`;
            }

            // Execute HTTP POST request
            const response = await fetch(queryEndpoint, {
                method: 'POST',
                headers,
                body: query,
                timeout: this.options.timeout
            });

            const responseTime = Date.now() - startTime;
            
            if (!response.ok) {
                const errorText = await response.text();
                logger.error(`SPARQL SELECT failed (${response.status}): ${response.statusText}`);
                logger.error('Error response:', errorText);
                
                return {
                    success: false,
                    status: response.status,
                    statusText: response.statusText,
                    error: errorText,
                    responseTime
                };
            }

            const data = await response.json();
            logger.info(`SPARQL SELECT completed successfully (${responseTime}ms)`);
            
            return {
                success: true,
                status: response.status,
                statusText: response.statusText,
                data: data,
                responseTime
            };

        } catch (error) {
            const responseTime = Date.now() - startTime;
            logger.error('SPARQL SELECT error:', error.message);
            
            return {
                success: false,
                error: error.message,
                responseTime
            };
        }
    }

    /**
     * Execute multiple SPARQL UPDATE queries in sequence
     * 
     * @param {string[]} queries - Array of SPARQL UPDATE queries
     * @returns {Promise<Object[]>} - Array of response objects
     */
    async executeUpdates(queries) {
        const results = [];
        
        for (let i = 0; i < queries.length; i++) {
            logger.info(`Executing update ${i + 1}/${queries.length}`);
            const result = await this.executeUpdate(queries[i]);
            results.push(result);
            
            // Stop on first failure unless configured to continue
            if (!result.success && !this.options.continueOnError) {
                logger.error(`Update ${i + 1} failed, stopping execution`);
                break;
            }
            
            // Add delay between requests if configured
            if (this.options.delay && i < queries.length - 1) {
                await new Promise(resolve => setTimeout(resolve, this.options.delay));
            }
        }
        
        return results;
    }

    /**
     * Batch multiple triples into a single INSERT DATA query
     * 
     * @param {string} graph - Named graph URI
     * @param {string[]} triplesBatch - Array of triple strings
     * @returns {string} - Combined INSERT DATA query
     */
    batchInsertData(graph, triplesBatch) {
        const combinedTriples = triplesBatch.join('\n        ');
        return this.createInsertDataQuery(graph, combinedTriples);
    }

    /**
     * Escape string literals for SPARQL
     * 
     * @param {string} str - String to escape
     * @returns {string} - Escaped string safe for SPARQL
     */
    static escapeString(str) {
        if (typeof str !== 'string') return str;
        
        return str
            .replace(/\\/g, '\\\\')  // Escape backslashes
            .replace(/"/g, '\\"')    // Escape quotes
            .replace(/\n/g, '\\n')   // Escape newlines
            .replace(/\r/g, '\\r')   // Escape carriage returns
            .replace(/\t/g, '\\t');  // Escape tabs
    }

    /**
     * Create a safe literal value for SPARQL
     * 
     * @param {string} value - Value to wrap
     * @param {string} datatype - XSD datatype URI (optional)
     * @param {string} lang - Language tag (optional)
     * @returns {string} - SPARQL literal
     */
    static createLiteral(value, datatype = null, lang = null) {
        const escaped = SPARQLHelper.escapeString(value);
        
        if (lang) {
            return `"${escaped}"@${lang}`;
        } else if (datatype) {
            return `"${escaped}"^^<${datatype}>`;
        } else {
            return `"${escaped}"`;
        }
    }

    /**
     * Create a safe URI for SPARQL
     * 
     * @param {string} uri - URI to wrap
     * @returns {string} - SPARQL URI
     */
    static createURI(uri) {
        return `<${uri}>`;
    }

    /**
     * Get execution statistics from results
     * 
     * @param {Object[]} results - Array of execution results
     * @returns {Object} - Statistics summary
     */
    static getExecutionStats(results) {
        const successful = results.filter(r => r.success).length;
        const failed = results.filter(r => !r.success).length;
        const totalTime = results.reduce((sum, r) => sum + (r.responseTime || 0), 0);
        const avgTime = results.length > 0 ? totalTime / results.length : 0;

        return {
            total: results.length,
            successful,
            failed,
            successRate: results.length > 0 ? (successful / results.length) * 100 : 0,
            totalTime,
            averageTime: avgTime,
            errors: results.filter(r => !r.success).map(r => r.error || r.statusText)
        };
    }
}