Incremental

Added:

Constraints, Bindings, Search param tabs.
This commit is contained in:
Joshua Hare 2025-04-22 13:33:44 +10:00
parent 454203a583
commit ebb691449b
4 changed files with 569 additions and 1851 deletions

241
app.py
View File

@ -401,13 +401,126 @@ def view_ig(processed_ig_id):
optional_usage_elements=optional_usage_elements,
config=current_app.config)
#---------------------------------------------------------------------------------------OLD backup-----------------------------------
# @app.route('/get-structure')
# def get_structure():
# package_name = request.args.get('package_name')
# package_version = request.args.get('package_version')
# resource_type = request.args.get('resource_type')
# # Keep view parameter for potential future use or caching, though not used directly in this revised logic
# view = request.args.get('view', 'snapshot') # Default to snapshot view processing
# if not all([package_name, package_version, resource_type]):
# logger.warning("get_structure: Missing query parameters: package_name=%s, package_version=%s, resource_type=%s", package_name, package_version, resource_type)
# return jsonify({"error": "Missing required query parameters: package_name, package_version, resource_type"}), 400
# packages_dir = current_app.config.get('FHIR_PACKAGES_DIR')
# if not packages_dir:
# logger.error("FHIR_PACKAGES_DIR not configured.")
# return jsonify({"error": "Server configuration error: Package directory not set."}), 500
# tgz_filename = services.construct_tgz_filename(package_name, package_version)
# tgz_path = os.path.join(packages_dir, tgz_filename)
# sd_data = None
# fallback_used = False
# source_package_id = f"{package_name}#{package_version}"
# logger.debug(f"Attempting to find SD for '{resource_type}' in {tgz_filename}")
# # --- Fetch SD Data (Keep existing logic including fallback) ---
# if os.path.exists(tgz_path):
# try:
# sd_data, _ = services.find_and_extract_sd(tgz_path, resource_type)
# # Add error handling as before...
# except Exception as e:
# logger.error(f"Unexpected error extracting SD '{resource_type}' from {tgz_path}: {e}", exc_info=True)
# return jsonify({"error": f"Unexpected error reading StructureDefinition: {str(e)}"}), 500
# else:
# logger.warning(f"Package file not found: {tgz_path}")
# # Try fallback... (keep existing fallback logic)
# if sd_data is None:
# logger.info(f"SD for '{resource_type}' not found in {source_package_id}. Attempting fallback to {services.CANONICAL_PACKAGE_ID}.")
# core_package_name, core_package_version = services.CANONICAL_PACKAGE
# core_tgz_filename = services.construct_tgz_filename(core_package_name, core_package_version)
# core_tgz_path = os.path.join(packages_dir, core_tgz_filename)
# if not os.path.exists(core_tgz_path):
# # Handle missing core package / download if needed...
# logger.error(f"Core package {services.CANONICAL_PACKAGE_ID} not found locally.")
# return jsonify({"error": f"SD for '{resource_type}' not found in primary package, and core package is missing."}), 500
# # (Add download logic here if desired)
# try:
# sd_data, _ = services.find_and_extract_sd(core_tgz_path, resource_type)
# if sd_data is not None:
# fallback_used = True
# source_package_id = services.CANONICAL_PACKAGE_ID
# logger.info(f"Found SD for '{resource_type}' in fallback package {source_package_id}.")
# # Add error handling as before...
# except Exception as e:
# logger.error(f"Unexpected error extracting SD '{resource_type}' from fallback {core_tgz_path}: {e}", exc_info=True)
# return jsonify({"error": f"Unexpected error reading fallback StructureDefinition: {str(e)}"}), 500
# # --- Check if SD data was found ---
# if not sd_data:
# logger.error(f"SD for '{resource_type}' not found in primary or fallback package.")
# return jsonify({"error": f"StructureDefinition for '{resource_type}' not found."}), 404
# # --- *** START Backend Modification *** ---
# # Extract snapshot and differential elements
# snapshot_elements = sd_data.get('snapshot', {}).get('element', [])
# differential_elements = sd_data.get('differential', {}).get('element', [])
# # Create a set of element IDs from the differential for efficient lookup
# # Using element 'id' is generally more reliable than 'path' for matching
# differential_ids = {el.get('id') for el in differential_elements if el.get('id')}
# logger.debug(f"Found {len(differential_ids)} unique IDs in differential.")
# enriched_elements = []
# if snapshot_elements:
# logger.debug(f"Processing {len(snapshot_elements)} snapshot elements to add isInDifferential flag.")
# for element in snapshot_elements:
# element_id = element.get('id')
# # Add the isInDifferential flag
# element['isInDifferential'] = bool(element_id and element_id in differential_ids)
# enriched_elements.append(element)
# # Clean narrative from enriched elements (should have been done in find_and_extract_sd, but double check)
# enriched_elements = [services.remove_narrative(el) for el in enriched_elements]
# else:
# # Fallback: If no snapshot, log warning. Maybe return differential only?
# # Returning only differential might break frontend filtering logic.
# # For now, return empty, but log clearly.
# logger.warning(f"No snapshot found for {resource_type} in {source_package_id}. Returning empty element list.")
# enriched_elements = [] # Or consider returning differential and handle in JS
# # --- *** END Backend Modification *** ---
# # Retrieve must_support_paths from DB (keep existing logic)
# must_support_paths = []
# processed_ig = ProcessedIg.query.filter_by(package_name=package_name, version=package_version).first()
# if processed_ig and processed_ig.must_support_elements:
# # Use the profile ID (which is likely the resource_type for profiles) as the key
# must_support_paths = processed_ig.must_support_elements.get(resource_type, [])
# logger.debug(f"Retrieved {len(must_support_paths)} Must Support paths for '{resource_type}' from processed IG DB record.")
# else:
# logger.debug(f"No processed IG record or no must_support_elements found in DB for {package_name}#{package_version}, resource {resource_type}")
# # Construct the response
# response_data = {
# 'elements': enriched_elements, # Return the processed list
# 'must_support_paths': must_support_paths,
# 'fallback_used': fallback_used,
# 'source_package': source_package_id
# # Removed raw structure_definition to reduce payload size, unless needed elsewhere
# }
# # Use Response object for consistent JSON formatting
# return Response(json.dumps(response_data, indent=2), mimetype='application/json') # Use indent=2 for readability if debugging
#-----------------------------------------------------------------------------------------------------------------------------------
@app.route('/get-structure')
def get_structure():
package_name = request.args.get('package_name')
package_version = request.args.get('package_version')
resource_type = request.args.get('resource_type')
# Keep view parameter for potential future use or caching, though not used directly in this revised logic
view = request.args.get('view', 'snapshot') # Default to snapshot view processing
resource_type = request.args.get('resource_type') # This is the StructureDefinition ID/Name
view = request.args.get('view', 'snapshot') # Keep for potential future use
if not all([package_name, package_version, resource_type]):
logger.warning("get_structure: Missing query parameters: package_name=%s, package_version=%s, resource_type=%s", package_name, package_version, resource_type)
@ -418,101 +531,131 @@ def get_structure():
logger.error("FHIR_PACKAGES_DIR not configured.")
return jsonify({"error": "Server configuration error: Package directory not set."}), 500
# Paths for primary and core packages
tgz_filename = services.construct_tgz_filename(package_name, package_version)
tgz_path = os.path.join(packages_dir, tgz_filename)
core_package_name, core_package_version = services.CANONICAL_PACKAGE
core_tgz_filename = services.construct_tgz_filename(core_package_name, core_package_version)
core_tgz_path = os.path.join(packages_dir, core_tgz_filename)
sd_data = None
search_params_data = [] # Initialize search params list
fallback_used = False
source_package_id = f"{package_name}#{package_version}"
base_resource_type_for_sp = None # Variable to store the base type for SP search
logger.debug(f"Attempting to find SD for '{resource_type}' in {tgz_filename}")
# --- Fetch SD Data (Keep existing logic including fallback) ---
if os.path.exists(tgz_path):
# --- Fetch SD Data (Primary Package) ---
primary_package_exists = os.path.exists(tgz_path)
core_package_exists = os.path.exists(core_tgz_path)
if primary_package_exists:
try:
sd_data, _ = services.find_and_extract_sd(tgz_path, resource_type)
# Add error handling as before...
if sd_data:
base_resource_type_for_sp = sd_data.get('type')
logger.debug(f"Determined base resource type '{base_resource_type_for_sp}' from primary SD '{resource_type}'")
except Exception as e:
logger.error(f"Unexpected error extracting SD '{resource_type}' from {tgz_path}: {e}", exc_info=True)
return jsonify({"error": f"Unexpected error reading StructureDefinition: {str(e)}"}), 500
else:
logger.warning(f"Package file not found: {tgz_path}")
# Try fallback... (keep existing fallback logic)
logger.error(f"Unexpected error extracting SD '{resource_type}' from primary package {tgz_path}: {e}", exc_info=True)
sd_data = None # Ensure sd_data is None if extraction failed
# --- Fallback SD Check (if primary failed or file didn't exist) ---
if sd_data is None:
logger.info(f"SD for '{resource_type}' not found in {source_package_id}. Attempting fallback to {services.CANONICAL_PACKAGE_ID}.")
core_package_name, core_package_version = services.CANONICAL_PACKAGE
core_tgz_filename = services.construct_tgz_filename(core_package_name, core_package_version)
core_tgz_path = os.path.join(packages_dir, core_tgz_filename)
if not os.path.exists(core_tgz_path):
# Handle missing core package / download if needed...
logger.error(f"Core package {services.CANONICAL_PACKAGE_ID} not found locally.")
return jsonify({"error": f"SD for '{resource_type}' not found in primary package, and core package is missing."}), 500
# (Add download logic here if desired)
logger.info(f"SD for '{resource_type}' not found or failed to load from {source_package_id}. Attempting fallback to {services.CANONICAL_PACKAGE_ID}.")
if not core_package_exists:
logger.error(f"Core package {services.CANONICAL_PACKAGE_ID} not found locally at {core_tgz_path}.")
error_message = f"SD for '{resource_type}' not found in primary package, and core package is missing." if primary_package_exists else f"Primary package {package_name}#{package_version} and core package are missing."
return jsonify({"error": error_message}), 500 if primary_package_exists else 404
try:
sd_data, _ = services.find_and_extract_sd(core_tgz_path, resource_type)
if sd_data is not None:
fallback_used = True
source_package_id = services.CANONICAL_PACKAGE_ID
logger.info(f"Found SD for '{resource_type}' in fallback package {source_package_id}.")
# Add error handling as before...
base_resource_type_for_sp = sd_data.get('type') # Store base type from fallback SD
logger.info(f"Found SD for '{resource_type}' in fallback package {source_package_id}. Base type: '{base_resource_type_for_sp}'")
except Exception as e:
logger.error(f"Unexpected error extracting SD '{resource_type}' from fallback {core_tgz_path}: {e}", exc_info=True)
return jsonify({"error": f"Unexpected error reading fallback StructureDefinition: {str(e)}"}), 500
# --- Check if SD data was found ---
# --- Check if SD data was ultimately found ---
if not sd_data:
logger.error(f"SD for '{resource_type}' not found in primary or fallback package.")
# This case should ideally be covered by the checks above, but as a final safety net:
logger.error(f"SD for '{resource_type}' could not be found in primary or fallback packages.")
return jsonify({"error": f"StructureDefinition for '{resource_type}' not found."}), 404
# --- *** START Backend Modification *** ---
# Extract snapshot and differential elements
# --- Fetch Search Parameters (Primary Package First) ---
if base_resource_type_for_sp and primary_package_exists:
try:
logger.info(f"Fetching SearchParameters for base type '{base_resource_type_for_sp}' from primary package {tgz_path}")
search_params_data = services.find_and_extract_search_params(tgz_path, base_resource_type_for_sp)
except Exception as e:
logger.error(f"Error extracting SearchParameters for '{base_resource_type_for_sp}' from primary package {tgz_path}: {e}", exc_info=True)
search_params_data = [] # Continue with empty list on error
elif not primary_package_exists:
logger.warning(f"Original package {tgz_path} not found, cannot search it for specific SearchParameters.")
elif not base_resource_type_for_sp:
logger.warning(f"Base resource type could not be determined for '{resource_type}', cannot search for SearchParameters.")
# --- Fetch Search Parameters (Fallback to Core Package if needed) ---
if not search_params_data and base_resource_type_for_sp and core_package_exists:
logger.info(f"No relevant SearchParameters found in primary package for '{base_resource_type_for_sp}'. Searching core package {core_tgz_path}.")
try:
search_params_data = services.find_and_extract_search_params(core_tgz_path, base_resource_type_for_sp)
if search_params_data:
logger.info(f"Found {len(search_params_data)} SearchParameters for '{base_resource_type_for_sp}' in core package.")
except Exception as e:
logger.error(f"Error extracting SearchParameters for '{base_resource_type_for_sp}' from core package {core_tgz_path}: {e}", exc_info=True)
search_params_data = [] # Continue with empty list on error
elif not search_params_data and not core_package_exists:
logger.warning(f"Core package {core_tgz_path} not found, cannot perform fallback search for SearchParameters.")
# --- Prepare Snapshot/Differential Elements (Existing Logic) ---
snapshot_elements = sd_data.get('snapshot', {}).get('element', [])
differential_elements = sd_data.get('differential', {}).get('element', [])
# Create a set of element IDs from the differential for efficient lookup
# Using element 'id' is generally more reliable than 'path' for matching
differential_ids = {el.get('id') for el in differential_elements if el.get('id')}
logger.debug(f"Found {len(differential_ids)} unique IDs in differential.")
enriched_elements = []
if snapshot_elements:
logger.debug(f"Processing {len(snapshot_elements)} snapshot elements to add isInDifferential flag.")
for element in snapshot_elements:
element_id = element.get('id')
# Add the isInDifferential flag
element['isInDifferential'] = bool(element_id and element_id in differential_ids)
enriched_elements.append(element)
# Clean narrative from enriched elements (should have been done in find_and_extract_sd, but double check)
enriched_elements = [services.remove_narrative(el) for el in enriched_elements]
else:
# Fallback: If no snapshot, log warning. Maybe return differential only?
# Returning only differential might break frontend filtering logic.
# For now, return empty, but log clearly.
logger.warning(f"No snapshot found for {resource_type} in {source_package_id}. Returning empty element list.")
enriched_elements = [] # Or consider returning differential and handle in JS
enriched_elements = []
# --- *** END Backend Modification *** ---
# Retrieve must_support_paths from DB (keep existing logic)
# --- Retrieve Must Support Paths from DB (Existing Logic - slightly refined key lookup) ---
must_support_paths = []
processed_ig = ProcessedIg.query.filter_by(package_name=package_name, version=package_version).first()
if processed_ig and processed_ig.must_support_elements:
# Use the profile ID (which is likely the resource_type for profiles) as the key
must_support_paths = processed_ig.must_support_elements.get(resource_type, [])
logger.debug(f"Retrieved {len(must_support_paths)} Must Support paths for '{resource_type}' from processed IG DB record.")
ms_elements_dict = processed_ig.must_support_elements
if resource_type in ms_elements_dict:
must_support_paths = ms_elements_dict[resource_type]
logger.debug(f"Retrieved {len(must_support_paths)} Must Support paths using profile key '{resource_type}' from processed IG DB record.")
elif base_resource_type_for_sp and base_resource_type_for_sp in ms_elements_dict:
must_support_paths = ms_elements_dict[base_resource_type_for_sp]
logger.debug(f"Retrieved {len(must_support_paths)} Must Support paths using base type key '{base_resource_type_for_sp}' from processed IG DB record.")
else:
logger.debug(f"No specific Must Support paths found for keys '{resource_type}' or '{base_resource_type_for_sp}' in processed IG DB.")
else:
logger.debug(f"No processed IG record or no must_support_elements found in DB for {package_name}#{package_version}, resource {resource_type}")
logger.debug(f"No processed IG record or no must_support_elements found in DB for {package_name}#{package_version}")
# Construct the response
# --- Construct the final response ---
response_data = {
'elements': enriched_elements, # Return the processed list
'elements': enriched_elements,
'must_support_paths': must_support_paths,
'search_parameters': search_params_data, # Include potentially populated list
'fallback_used': fallback_used,
'source_package': source_package_id
# Removed raw structure_definition to reduce payload size, unless needed elsewhere
}
# Use Response object for consistent JSON formatting
return Response(json.dumps(response_data, indent=2), mimetype='application/json') # Use indent=2 for readability if debugging
# Use Response object for consistent JSON formatting and smaller payload
return Response(json.dumps(response_data, indent=None, separators=(',', ':')), mimetype='application/json')
@app.route('/get-example')
def get_example():

View File

@ -1933,6 +1933,85 @@ def process_fhir_input(input_mode, fhir_file, fhir_text, alias_file=None):
logger.error(f"Error processing input: {str(e)}", exc_info=True)
return None, None, None, f"Error processing input: {str(e)}"
# --- ADD THIS NEW FUNCTION TO services.py ---
def find_and_extract_search_params(tgz_path, base_resource_type):
"""
Finds and extracts SearchParameter resources relevant to a given base resource type
from a FHIR package tgz file.
"""
search_params = []
if not tgz_path or not os.path.exists(tgz_path):
logger.error(f"Package file not found for SearchParameter extraction: {tgz_path}")
return search_params # Return empty list on error
logger.debug(f"Searching for SearchParameters based on '{base_resource_type}' in {os.path.basename(tgz_path)}")
try:
with tarfile.open(tgz_path, "r:gz") as tar:
for member in tar:
# Basic filtering for JSON files in package directory, excluding common metadata
if not (member.isfile() and member.name.startswith('package/') and member.name.lower().endswith('.json')):
continue
if os.path.basename(member.name).lower() in ['package.json', '.index.json', 'validation-summary.json', 'validation-oo.json']:
continue
fileobj = None
try:
fileobj = tar.extractfile(member)
if fileobj:
content_bytes = fileobj.read()
content_string = content_bytes.decode('utf-8-sig')
data = json.loads(content_string)
# Check if it's a SearchParameter resource
if isinstance(data, dict) and data.get('resourceType') == 'SearchParameter':
# Check if the SearchParameter applies to the requested base resource type
sp_bases = data.get('base', []) # 'base' is a list of applicable resource types
if base_resource_type in sp_bases:
# Extract relevant information
param_info = {
'id': data.get('id'),
'url': data.get('url'),
'name': data.get('name'),
'description': data.get('description'),
'code': data.get('code'), # The actual parameter name used in searches
'type': data.get('type'), # e.g., token, reference, date
'expression': data.get('expression'), # FHIRPath expression
'base': sp_bases,
# NOTE: Conformance (mandatory/optional) usually comes from CapabilityStatement,
# which is not processed here. Add placeholders or leave out for now.
'conformance': 'N/A', # Placeholder
'is_mandatory': False # Placeholder
}
search_params.append(param_info)
logger.debug(f"Found relevant SearchParameter: {param_info.get('name')} (ID: {param_info.get('id')}) for base {base_resource_type}")
# --- Error handling for individual file processing ---
except json.JSONDecodeError as e:
logger.debug(f"Could not parse JSON for SearchParameter in {member.name}, skipping: {e}")
except UnicodeDecodeError as e:
logger.warning(f"Could not decode UTF-8 for SearchParameter in {member.name}, skipping: {e}")
except tarfile.TarError as e:
logger.warning(f"Tar error reading member {member.name} for SearchParameter, skipping: {e}")
except Exception as e:
logger.warning(f"Could not read/parse potential SearchParameter {member.name}, skipping: {e}", exc_info=False)
finally:
if fileobj:
fileobj.close()
# --- Error handling for opening/reading the tgz file ---
except tarfile.ReadError as e:
logger.error(f"Tar ReadError extracting SearchParameters from {tgz_path}: {e}")
except tarfile.TarError as e:
logger.error(f"TarError extracting SearchParameters from {tgz_path}: {e}")
except FileNotFoundError:
logger.error(f"Package file not found during SearchParameter extraction: {tgz_path}")
except Exception as e:
logger.error(f"Unexpected error extracting SearchParameters from {tgz_path}: {e}", exc_info=True)
logger.info(f"Found {len(search_params)} SearchParameters relevant to '{base_resource_type}' in {os.path.basename(tgz_path)}")
return search_params
# --- END OF NEW FUNCTION ---
# --- Standalone Test ---
if __name__ == '__main__':
logger.info("Running services.py directly for testing.")

View File

@ -153,6 +153,15 @@
<li class="nav-item">
<a class="nav-link" id="key-elements-tab" data-bs-toggle="tab" href="#key-elements" role="tab" aria-controls="key-elements" aria-selected="false">Key Elements</a>
</li>
<li class="nav-item">
<a class="nav-link" id="constraints-tab" data-bs-toggle="tab" href="#constraints-tab-pane" role="tab">Constraints</a>
</li>
<li class="nav-item">
<a class="nav-link" id="terminology-tab" data-bs-toggle="tab" href="#terminology-tab-pane" role="tab">Terminology</a>
</li>
<li class="nav-item">
<a class="nav-link" id="searchparams-tab" data-bs-toggle="tab" href="#searchparams-tab-pane" role="tab">Search Params</a>
</li>
</ul>
<div class="tab-content mt-3">
<div class="tab-pane fade show active" id="differential" role="tabpanel" aria-labelledby="differential-tab">
@ -195,6 +204,64 @@
</div>
<ul id="structure-tree-key-elements" class="list-group list-group-flush structure-tree-root"></ul>
</div>
<div class="tab-pane fade" id="constraints-tab-pane" role="tabpanel" aria-labelledby="constraints-tab">
<div class="mt-3 table-responsive">
<p>Constraints defined in this profile:</p>
<table class="table table-sm table-bordered table-striped" id="constraints-table">
<thead class="table-light">
<tr>
<th>Key</th>
<th>Severity</th>
<th>Path</th>
<th>Description</th>
<th>Expression (FHIRPath)</th>
</tr>
</thead>
<tbody>
<tr><td colspan="5">No constraints found or data not loaded.</td></tr>
</tbody>
</table>
</div>
</div>
<div class="tab-pane fade" id="terminology-tab-pane" role="tabpanel" aria-labelledby="terminology-tab">
<div class="mt-3 table-responsive">
<p>Terminology bindings defined in this profile:</p>
<table class="table table-sm table-bordered table-striped" id="bindings-table">
<thead class="table-light">
<tr>
<th>Path</th>
<th>Strength</th>
<th>ValueSet</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr><td colspan="4">No bindings found or data not loaded.</td></tr>
</tbody>
</table>
</div>
</div>
<div class="tab-pane fade" id="searchparams-tab-pane" role="tabpanel" aria-labelledby="searchparams-tab">
<div class="mt-3">
<p>Search parameters associated with this profile:</p>
<div class="alert alert-warning" role="alert">
<strong>Note:</strong> SearchParameter resources. This functionality is NEWLY implemented, please report any bugs)
</div>
<table class="table table-sm table-bordered table-striped" id="searchparams-table">
<thead class="table-light">
<tr>
<th>Parameter</th>
<th>Type</th>
<th>Conformance</th>
<th>Requirements / Description</th>
</tr>
</thead>
<tbody>
<tr><td colspan="4">Search Parameter data not available. Requires backend implementation.</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
@ -869,8 +936,9 @@ async function fetchStructure(resourceType, view) {
}
// --- Populate Structure for All Views - V2 (Builds full tree from snapshot first) ---
// --- Populate Structure for All Views - V4 (Add Search Param Logging) ---
async function populateStructure(resourceType) {
// ... (keep existing variable assignments and UI resets at the beginning) ...
if (structureTitle) structureTitle.textContent = `${resourceType} ({{ processed_ig.package_name }}#{{ processed_ig.version }})`;
if (rawStructureTitle) rawStructureTitle.textContent = `${resourceType} ({{ processed_ig.package_name }}#{{ processed_ig.version }})`;
if (structureLoading) structureLoading.style.display = 'block';
@ -880,33 +948,76 @@ async function populateStructure(resourceType) {
if (structureFallbackMessage) { structureFallbackMessage.style.display = 'none'; structureFallbackMessage.textContent = ''; }
const views = ['differential', 'snapshot', 'must-support', 'key-elements'];
views.forEach(v => { const tc = document.getElementById(`structure-tree-${v}`); if(tc) tc.innerHTML = ''; });
if (rawStructureContent) rawStructureContent.textContent = '';
const constraintsTableBody = document.getElementById('constraints-table')?.querySelector('tbody');
const bindingsTableBody = document.getElementById('bindings-table')?.querySelector('tbody');
const searchParamsTableBody = document.getElementById('searchparams-table')?.querySelector('tbody');
if (constraintsTableBody) constraintsTableBody.innerHTML = '<tr><td colspan="5" class="text-center text-muted fst-italic">Loading...</td></tr>';
if (bindingsTableBody) bindingsTableBody.innerHTML = '<tr><td colspan="4" class="text-center text-muted fst-italic">Loading...</td></tr>';
if (searchParamsTableBody) searchParamsTableBody.innerHTML = '<tr><td colspan="4" class="text-center text-muted fst-italic">Loading...</td></tr>';
if (rawStructureContent) rawStructureContent.textContent = 'Loading...';
try {
console.log(`PopulateStructure V2: Fetching snapshot for ${resourceType}`);
const snapshotData = await fetchStructure(resourceType, 'snapshot');
if (!snapshotData || !snapshotData.elements) { throw new Error(`Snapshot data missing for ${resourceType}.`); }
const snapshotElementsCopy = JSON.parse(JSON.stringify(snapshotData.elements));
const mustSupportPaths = new Set(snapshotData.must_support_paths || []);
const fallbackUsed = snapshotData.fallback_used || false; const fallbackSource = snapshotData.source_package || 'FHIR core';
if (rawStructureContent) { rawStructureContent.textContent = JSON.stringify(snapshotData, null, 4); if (rawStructureContent.classList.contains('hljs')) { rawStructureContent.classList.remove('hljs'); } hljs.highlightElement(rawStructureContent); if (fallbackUsed && structureFallbackMessage) { /* show fallback */ } }
else { if (rawStructureContent) rawStructureContent.textContent = '(Raw content display element not found)';}
console.log(`PopulateStructure V4: Fetching structure and search params for ${resourceType}`);
const structureBaseUrl = "{{ url_for('get_structure') }}";
const params = new URLSearchParams({ package_name: "{{ processed_ig.package_name }}", package_version: "{{ processed_ig.version }}", resource_type: resourceType });
const fetchUrl = `${structureBaseUrl}?${params.toString()}`;
const response = await fetch(fetchUrl);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP error ${response.status}: ${errorText}`);
}
const data = await response.json();
if (!data || !data.elements) { throw new Error(`Structure data or elements missing for ${resourceType}.`); }
const snapshotElementsCopy = JSON.parse(JSON.stringify(data.elements));
const mustSupportPaths = new Set(data.must_support_paths || []);
const searchParams = data.search_parameters || []; // Get search params
const fallbackUsed = data.fallback_used || false;
const fallbackSource = data.source_package || 'FHIR core';
// *** ADDED LOG: Check received search parameters ***
console.log('[populateStructure] Received search_parameters:', searchParams);
// *************************************************
// Update Raw JSON View
if (rawStructureContent) {
rawStructureContent.textContent = JSON.stringify(data, null, 4);
if (rawStructureContent.classList.contains('hljs')) { rawStructureContent.classList.remove('hljs'); }
hljs.highlightElement(rawStructureContent);
if (fallbackUsed && structureFallbackMessage) {
structureFallbackMessage.textContent = `Note: Displaying structure from fallback package: ${fallbackSource}`;
structureFallbackMessage.style.display = 'block';
}
} else { if (rawStructureContent) rawStructureContent.textContent = '(Raw content display element not found)';}
// Render the Tree Views
for (const view of views) {
console.log(`PopulateStructure V2: Rendering view '${view}' for ${resourceType}`);
console.log(`PopulateStructure V4: Rendering view '${view}' for ${resourceType}`);
const treeContainer = document.getElementById(`structure-tree-${view}`);
if (!treeContainer) { console.error(`Container missing for ${view}`); continue; }
// Use V11 buildTreeData (called within renderStructureTree)
await renderStructureTree(resourceType, view, JSON.parse(JSON.stringify(snapshotElementsCopy)), mustSupportPaths);
}
// Render Constraints & Bindings tables
renderConstraintsTable(data.elements);
renderBindingsTable(data.elements);
// Render Search Params table (passing potentially empty array)
renderSearchParametersTable(searchParams); // Pass the retrieved data
} catch (error) {
console.error("Error populating structure:", error);
if (structureTreeDifferential) { structureTreeDifferential.innerHTML = `<div class="alert alert-danger">Error loading structure: ${error.message}</div>`; }
// ... existing error handling ...
views.forEach(v => { const tc = document.getElementById(`structure-tree-${v}`); if(tc) tc.innerHTML = `<div class="alert alert-danger m-2">Error loading structure: ${error.message}</div>`; });
if (constraintsTableBody) constraintsTableBody.innerHTML = `<tr><td colspan="5" class="text-danger text-center">Error loading constraints: ${error.message}</td></tr>`;
if (bindingsTableBody) bindingsTableBody.innerHTML = `<tr><td colspan="4" class="text-danger text-center">Error loading bindings: ${error.message}</td></tr>`;
if (searchParamsTableBody) searchParamsTableBody.innerHTML = `<tr><td colspan="4" class="text-danger text-center">Error loading search params: ${error.message}</td></tr>`; // Show error here too
if (rawStructureContent) { rawStructureContent.textContent = `Error: ${error.message}`; }
} finally {
if (structureLoading) structureLoading.style.display = 'none';
if (rawStructureLoading) rawStructureLoading.style.display = 'none';
}
} // End populateStructure V2
} // End populateStructure V4
// --- Populate Example Selector Dropdown ---
@ -949,6 +1060,179 @@ function setupCopyButton(buttonElement, sourceElement, tooltipInstance, successT
buttonElement.removeEventListener('click', clickHandler); buttonElement.addEventListener('click', clickHandler); return tooltipInstance;
}
// --- Function to Render Constraints Table ---
function renderConstraintsTable(elements) {
const tableBody = document.getElementById('constraints-table')?.querySelector('tbody');
if (!tableBody) {
console.error('Constraints table body not found');
return;
}
tableBody.innerHTML = ''; // Clear existing rows
let constraintsFound = false;
if (!elements || !Array.isArray(elements)) {
console.warn("renderConstraintsTable called with invalid elements data:", elements);
elements = []; // Prevent errors later
}
elements.forEach(element => {
// Check if element exists and has a constraint property which is an array
if (element && element.constraint && Array.isArray(element.constraint) && element.constraint.length > 0) {
element.constraint.forEach(constraint => {
if (constraint) { // Ensure constraint object itself exists
constraintsFound = true;
const row = tableBody.insertRow();
row.insertCell().textContent = constraint.key || 'N/A';
row.insertCell().textContent = constraint.severity || 'N/A';
row.insertCell().textContent = element.path || 'N/A'; // Path of the element the constraint is on
row.insertCell().textContent = constraint.human || 'N/A';
const expressionCell = row.insertCell();
const codeElement = document.createElement('code');
codeElement.className = 'language-fhirpath'; // Use 'language-fhirpath' if you have a FHIRPath grammar for highlight.js
codeElement.textContent = constraint.expression || 'N/A';
expressionCell.appendChild(codeElement);
}
});
}
});
if (!constraintsFound) {
const row = tableBody.insertRow();
const cell = row.insertCell();
cell.colSpan = 5; // Match number of columns
cell.textContent = 'No constraints defined in this profile.';
cell.style.textAlign = 'center';
}
// Re-run highlight.js for the new code blocks
// Use setTimeout to allow the DOM to update before highlighting
setTimeout(() => {
document.querySelectorAll('#constraints-table code.language-fhirpath').forEach((block) => {
// Remove existing highlight classes if any, before re-highlighting
block.classList.remove('hljs');
hljs.highlightElement(block);
});
}, 0);
}
// --- Function to Render Terminology Bindings Table ---
function renderBindingsTable(elements) {
const tableBody = document.getElementById('bindings-table')?.querySelector('tbody');
if (!tableBody) {
console.error('Bindings table body not found');
return;
}
tableBody.innerHTML = ''; // Clear existing rows
let bindingsFound = false;
if (!elements || !Array.isArray(elements)) {
console.warn("renderBindingsTable called with invalid elements data:", elements);
elements = []; // Prevent errors later
}
elements.forEach(element => {
// Check if element exists and has a binding property which is an object
if (element && element.binding && typeof element.binding === 'object') {
bindingsFound = true;
const binding = element.binding;
const row = tableBody.insertRow();
row.insertCell().textContent = element.path || 'N/A';
row.insertCell().textContent = binding.strength || 'N/A';
const valueSetCell = row.insertCell();
if (binding.valueSet) {
const valueSetLink = document.createElement('a');
valueSetLink.href = binding.valueSet; // Ideally, resolve canonical to actual URL if possible
// Try to show last part of URL as name, or full URL if no '/'
const urlParts = binding.valueSet.split('/');
valueSetLink.textContent = urlParts.length > 1 ? urlParts[urlParts.length - 1] : binding.valueSet;
valueSetLink.target = '_blank'; // Open in new tab
valueSetLink.title = `Open ValueSet: ${binding.valueSet}`; // Add tooltip
valueSetLink.setAttribute('data-bs-toggle', 'tooltip'); // Enable tooltip
valueSetCell.appendChild(valueSetLink);
// Re-initialize tooltips for dynamically added links
const tooltip = new bootstrap.Tooltip(valueSetLink);
} else {
valueSetCell.textContent = 'N/A';
}
row.insertCell().textContent = binding.description || 'N/A';
}
});
if (!bindingsFound) {
const row = tableBody.insertRow();
const cell = row.insertCell();
cell.colSpan = 4; // Match number of columns
cell.textContent = 'No terminology bindings defined in this profile.';
cell.style.textAlign = 'center';
}
}
// --- Function to Render Search Parameters Table (Add Logging) ---
function renderSearchParametersTable(searchParamsData) {
const tableBody = document.getElementById('searchparams-table')?.querySelector('tbody');
if (!tableBody) {
console.error('Search Params table body not found');
return;
}
tableBody.innerHTML = ''; // Clear existing rows
// *** ADDED LOG: Check data received by rendering function ***
console.log('[renderSearchParametersTable] Received data:', searchParamsData);
// **********************************************************
if (!searchParamsData || !Array.isArray(searchParamsData) || searchParamsData.length === 0) {
console.log('[renderSearchParametersTable] No valid search parameter data found, displaying message.'); // Added Log
const row = tableBody.insertRow();
const cell = row.insertCell();
cell.colSpan = 4;
cell.textContent = 'No relevant search parameters found for this resource type.';
cell.style.textAlign = 'center';
cell.classList.add('text-muted', 'fst-italic');
return;
}
// Sort parameters alphabetically by code
searchParamsData.sort((a, b) => (a.code || '').localeCompare(b.code || ''));
searchParamsData.forEach((param, index) => {
// *** ADDED LOG: Log each parameter being processed ***
console.log(`[renderSearchParametersTable] Processing param ${index + 1}:`, param);
// ***************************************************
const row = tableBody.insertRow();
// Parameter Name (using 'code')
const nameCell = row.insertCell();
nameCell.innerHTML = `<code>${escapeHtmlAttr(param.code || 'N/A')}</code>`;
// Type
row.insertCell().textContent = param.type || 'N/A';
// Conformance (Placeholder)
const conformanceCell = row.insertCell();
conformanceCell.textContent = param.conformance || 'N/A'; // Show backend placeholder
// Add highlighting later if backend provides is_mandatory
// if (param.is_mandatory) { ... }
// Description
const descriptionCell = row.insertCell();
descriptionCell.innerHTML = `<small>${escapeHtmlAttr(param.description || 'No description available.')}</small>`;
if (param.expression) {
descriptionCell.title = `FHIRPath: ${param.expression}`;
descriptionCell.setAttribute('data-bs-toggle', 'tooltip');
descriptionCell.setAttribute('data-bs-placement', 'top');
// Ensure tooltip is initialized for dynamically added elements
const existingTooltip = bootstrap.Tooltip.getInstance(descriptionCell);
if (existingTooltip) {
existingTooltip.dispose(); // Remove old instance if any
}
new bootstrap.Tooltip(descriptionCell);
}
});
console.log('[renderSearchParametersTable] Finished rendering parameters.'); // Added Log
} // End renderSearchParametersTable
// --- DOMContentLoaded Listener ---
document.addEventListener('DOMContentLoaded', function() {

File diff suppressed because it is too large Load Diff