Skip to main content

Overview

CCDigital integrates with Hyperledger Fabric to provide immutable document traceability and audit logging. The blockchain layer operates alongside the MySQL database, recording document metadata and access events on-chain. Architecture:
  • MySQL: Primary transactional data store
  • Fabric: Audit trail and document registry
  • Node.js Scripts: CLI interface to Fabric chaincode
Blockchain functions as an additional trust layer, not the primary data source. All operations first complete in MySQL, then optionally sync to Fabric.

Service Components

FabricLedgerCliService

Package: co.edu.unbosque.ccdigital.service Purpose: Query documents directly from Fabric ledger Key Features:
  • Lists documents for a person from chaincode
  • Executes list-docs.js Node.js script
  • Parses JSON responses into typed DTOs
  • Does NOT query MySQL database

FabricAuditCliService

Package: co.edu.unbosque.ccdigital.service Purpose: Record and query audit events on Fabric Key Features:
  • Records access events (view, download, approval)
  • Lists audit events per person or globally
  • Executes record-access-event.js and list-access-events.js
  • Returns typed FabricAuditEventView objects

ExternalToolsService

Package: co.edu.unbosque.ccdigital.service Purpose: Generic command execution service for external tools Key Features:
  • Executes Node.js and Python scripts
  • Captures stdout/stderr in parallel
  • Timeout management (default 180 seconds)
  • Structured result with exit code and timestamps

BlockchainTraceDetailService

Package: co.edu.unbosque.ccdigital.service Purpose: Detailed blockchain transaction inspection for admin reports

Configuration

Fabric Scripts (Node.js)

# Working directory for Fabric client
external-tools.fabric.workdir=/path/to/fabric-client

# Node.js binary
external-tools.fabric.node-bin=node

# Script paths (relative to workdir or absolute)
external-tools.fabric.list-docs-script=list-docs.js
external-tools.fabric.block-reader-script=read-block-by-ref.js
external-tools.fabric.record-access-script=record-access-event.js
external-tools.fabric.list-access-script=list-access-events.js
external-tools.fabric.sync-all-script=sync-db-to-ledger.js
external-tools.fabric.sync-person-script=sync-db-to-ledger.js

# Timeout for script execution (seconds)
external-tools.default-timeout-seconds=180
Environment Variables:
  • FABRIC_WORKDIR
  • FABRIC_NODE_BIN
  • FABRIC_LIST_DOCS_SCRIPT
  • FABRIC_SYNC_ALL_SCRIPT
  • FABRIC_SYNC_PERSON_SCRIPT
  • EXTERNAL_TOOLS_TIMEOUT_SECONDS

FabricLedgerCliService Methods

Executes list-docs.js and returns raw stdout.Command Format:
node list-docs.js {idType} {idNumber}
Parameters:
  • idType - ID type (CC, TI, etc.)
  • idNumber - ID number
Returns: String - Raw stdout from scriptThrows: RuntimeException if exit code != 0Example:
String json = fabricLedgerCliService.listDocsRaw("CC", "1234567890");
Executes list-docs.js and returns typed document list.Parameters:
  • idType - ID type
  • idNumber - ID number
Returns: List<FabricDocView> with fields:
  • docId - Document ID on ledger
  • title - Document title
  • issuingEntity - Issuer name
  • createdAt - Creation timestamp
  • sizeBytes - File size
  • filePath - Storage path
Parsing Logic:
  1. Extracts first JSON array from stdout (from [ to ])
  2. Maps JSON properties to FabricDocView record
  3. Handles mixed output (logs + JSON)
Example:
List<FabricDocView> docs = fabricLedgerCliService.listDocsView("CC", "1234567890");
for (FabricDocView doc : docs) {
    System.out.println(doc.title() + " - " + doc.sizeBytes() + " bytes");
}
Finds a specific document by ID from Fabric.Parameters:
  • idType - ID type
  • idNumber - ID number
  • docId - Document ID to find
Returns: Optional<FabricDocView>Usage:
Optional<FabricDocView> doc = fabricLedgerCliService.findDocById(
    "CC", "1234567890", "DOC-2024-001"
);
if (doc.isPresent()) {
    System.out.println("Found: " + doc.get().title());
}

FabricAuditCliService Methods

Records an audit event on Fabric (write transaction).Command Structure:
public record AuditCommand(
    String idType,
    String idNumber,
    String eventType,
    Long requestId,
    Long personDocumentId,
    String docId,
    String documentTitle,
    Long issuerEntityId,
    String issuerName,
    String action,          // VIEW, DOWNLOAD, APPROVE, REJECT
    String result,          // OK, FAIL
    String reason,
    String actorType,       // USER, ADMIN, ISSUER
    String actorId,
    String source           // WEB, API, ADMIN_GOVERNANCE
)
Executes:
node record-access-event.js {idType} {idNumber} {eventType} \
  {requestId} {personDocumentId} {docId} {documentTitle} \
  {issuerEntityId} {issuerName} {action} {result} {reason} \
  {actorType} {actorId} {source}
Returns: FabricAuditEventView - Event record with txIdArgument Handling:
  • Empty values are replaced with "-"
  • All strings are trimmed
Example:
FabricAuditEventView event = fabricAuditCliService.recordEvent(
    new AuditCommand(
        "CC", "1234567890",
        "DOCUMENT_ACCESS",
        123L, 456L, "DOC-001", "Diploma Universitario",
        10L, "Universidad El Bosque",
        "VIEW", "OK", "Usuario consultó documento desde dashboard",
        "USER", "1234567890", "WEB"
    )
);
System.out.println("Tx ID: " + event.txId());
Lists all audit events for a specific person.Command:
node list-access-events.js --person {idType} {idNumber}
Returns: List<FabricAuditEventView>Example:
List<FabricAuditEventView> events = fabricAuditCliService.listEventsForPerson(
    "CC", "1234567890"
);
System.out.println("Found " + events.size() + " events");
Lists all audit events on the ledger (global view).Command:
node list-access-events.js --all
Returns: List<FabricAuditEventView>Usage:
List<FabricAuditEventView> allEvents = fabricAuditCliService.listAllEvents();
This method can return large datasets. Use pagination or filtering at the chaincode level for production.

ExternalToolsService Methods

Synchronizes all database records to Fabric ledger.Command:
node {sync-all-script} --all
Returns: ExecResult with:
  • exitCode - 0 = success
  • stdout - Command output
  • stderr - Error output
  • startedAt - Execution start timestamp
  • finishedAt - Execution end timestamp
  • command - Executed command tokens
Example:
ExecResult result = externalToolsService.runFabricSyncAll();
if (result.isOk()) {
    System.out.println("Sync completed: " + result.getStdout());
} else {
    System.err.println("Sync failed: " + result.getStderr());
}
Synchronizes documents for a specific person.Command:
node {sync-person-script} --person {idType} {idNumber}
Parameters:
  • idType - ID type
  • idNumber - ID number
Returns: ExecResultExample:
ExecResult result = externalToolsService.runFabricSyncPerson(
    "CC", "1234567890"
);
Generic command execution with full control.Parameters:
  • command - Command tokens (e.g., ["node", "script.js", "arg1"])
  • workdir - Working directory (can be empty)
  • extraEnv - Additional environment variables
Returns: ExecResultFeatures:
  • Parallel stdout/stderr capture (prevents deadlock)
  • Configurable timeout (default 180s)
  • Exit code 124 for timeout
  • Exit code 2 for configuration errors
Example:
ExecResult result = externalToolsService.exec(
    List.of("node", "custom-script.js", "--arg", "value"),
    "/path/to/workdir",
    Map.of("CUSTOM_VAR", "value")
);

Synchronization Workflows

Global Sync (Admin Dashboard)

  1. Admin clicks “Sync All to Fabric”
  2. ExternalToolsService.runFabricSyncAll() called
  3. Script reads all documents from MySQL
  4. For each document:
    • Checks if exists on ledger
    • Creates or updates record
  5. Returns summary (created/updated/failed)

Per-Person Sync

  1. Admin selects person
  2. Clicks “Sync Person to Fabric”
  3. ExternalToolsService.runFabricSyncPerson() called
  4. Script filters by idType and idNumber
  5. Syncs only that person’s documents

FabricAuditEventView Structure

public record FabricAuditEventView(
    String txId,                // Fabric transaction ID
    String idType,              // ID type
    String idNumber,            // ID number
    String eventType,           // Event classification
    String requestId,           // Access request ID
    String personDocumentId,    // Document ID
    String docId,               // Fabric doc ID
    String documentTitle,       // Document title
    String issuerEntityId,      // Issuer entity ID
    String issuerName,          // Issuer name
    String action,              // Action performed
    String result,              // Result (OK/FAIL)
    String reason,              // Reason/notes
    String actorType,           // Actor type
    String actorId,             // Actor identifier
    String source,              // Source system
    String createdAt            // Timestamp (ISO 8601)
)

ExecResult Structure

public class ExecResult {
    private final int exitCode;       // 0 = success, 124 = timeout
    private final String stdout;      // Standard output
    private final String stderr;      // Error output
    private final Instant startedAt;  // Start timestamp
    private final Instant finishedAt; // End timestamp
    private final List<String> command; // Command tokens
    
    public boolean isOk() { return exitCode == 0; }
}

Error Handling

Script Execution Errors

Exit CodeMeaningAction
0SuccessProcess output
1Script errorCheck stderr for details
2Configuration errorVerify properties
124TimeoutIncrease timeout or optimize script

Configuration Validation

private void validateFabricCliConfig() {
    if (isBlank(fabricWorkdir)) {
        throw new IllegalArgumentException(
            "Falta configurar external-tools.fabric.workdir"
        );
    }
    // Additional checks...
}

JSON Extraction

Both services handle mixed output (logs + JSON): Array Extraction:
private static String extractJsonArray(String stdout) {
    int start = stdout.indexOf('[');
    int end = stdout.lastIndexOf(']');
    if (start < 0 || end <= start) return "[]";
    return stdout.substring(start, end + 1).trim();
}
Object Extraction:
private String extractJsonObject(String stdout) {
    int start = stdout.indexOf('{');
    int end = stdout.lastIndexOf('}');
    if (start < 0 || end <= start) {
        throw new IllegalArgumentException("No JSON in output");
    }
    return stdout.substring(start, end + 1).trim();
}

Best Practices

  1. Always validate configuration before executing scripts
  2. Handle timeouts gracefully - blockchain operations can be slow
  3. Log both stdout and stderr for troubleshooting
  4. Use controlled error messages (avoid exposing internal paths)
  5. Sanitize arguments - replace empty values with "-"
  6. Parse JSON defensively - extract arrays/objects from mixed output
  • File Storage Service - Document file management (FileStorageService:154)
  • Identity Services - User access state sync (UserAccessGovernanceService:111)
  • AdminReportService - Blockchain trace aggregation for reports

See Also

Build docs developers (and LLMs) love