mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-06-13 23:40:00 +00:00
Incremental, Bugfix
This commit is contained in:
parent
8f2c90087d
commit
02d58facbb
301
app.py
301
app.py
@ -5,6 +5,7 @@ import datetime
|
||||
import shutil
|
||||
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, Response, current_app, session, send_file, make_response
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_migrate import Migrate
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
from werkzeug.utils import secure_filename
|
||||
@ -83,6 +84,7 @@ except Exception as e:
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
csrf = CSRFProtect(app)
|
||||
migrate = Migrate(app, db)
|
||||
|
||||
# Register Blueprint
|
||||
app.register_blueprint(services_bp, url_prefix='/api')
|
||||
@ -98,8 +100,16 @@ class ProcessedIg(db.Model):
|
||||
complies_with_profiles = db.Column(db.JSON, nullable=True)
|
||||
imposed_profiles = db.Column(db.JSON, nullable=True)
|
||||
optional_usage_elements = db.Column(db.JSON, nullable=True)
|
||||
# --- ADD THIS LINE ---
|
||||
search_param_conformance = db.Column(db.JSON, nullable=True) # Stores the extracted conformance map
|
||||
# --- END ADD ---
|
||||
__table_args__ = (db.UniqueConstraint('package_name', 'version', name='uq_package_version'),)
|
||||
|
||||
# --- Make sure to handle database migration if you use Flask-Migrate ---
|
||||
# (e.g., flask db migrate -m "Add search_param_conformance to ProcessedIg", flask db upgrade)
|
||||
# If not using migrations, you might need to drop and recreate the table (losing existing processed data)
|
||||
# or manually alter the table using SQLite tools.
|
||||
|
||||
def check_api_key():
|
||||
api_key = request.headers.get('X-API-Key')
|
||||
if not api_key and request.is_json:
|
||||
@ -256,9 +266,10 @@ def push_igs():
|
||||
|
||||
@app.route('/process-igs', methods=['POST'])
|
||||
def process_ig():
|
||||
form = FlaskForm()
|
||||
form = FlaskForm() # Assuming a basic FlaskForm for CSRF protection
|
||||
if form.validate_on_submit():
|
||||
filename = request.form.get('filename')
|
||||
# --- Keep existing filename and path validation ---
|
||||
if not filename or not filename.endswith('.tgz'):
|
||||
flash("Invalid package file selected.", "error")
|
||||
return redirect(url_for('view_igs'))
|
||||
@ -266,57 +277,68 @@ def process_ig():
|
||||
if not os.path.exists(tgz_path):
|
||||
flash(f"Package file not found: {filename}", "error")
|
||||
return redirect(url_for('view_igs'))
|
||||
|
||||
name, version = services.parse_package_filename(filename)
|
||||
if not name:
|
||||
name = filename[:-4].replace('_', '.')
|
||||
version = 'unknown'
|
||||
logger.warning(f"Using fallback naming for {filename} -> {name}#{version}")
|
||||
if not name: # Add fallback naming if parse fails
|
||||
name = filename[:-4].replace('_', '.') # Basic guess
|
||||
version = 'unknown'
|
||||
logger.warning(f"Using fallback naming for {filename} -> {name}#{version}")
|
||||
|
||||
try:
|
||||
logger.info(f"Starting processing for {name}#{version} from file {filename}")
|
||||
# This now returns the conformance map too
|
||||
package_info = services.process_package_file(tgz_path)
|
||||
|
||||
if package_info.get('errors'):
|
||||
flash(f"Processing completed with errors for {name}#{version}: {', '.join(package_info['errors'])}", "warning")
|
||||
|
||||
# (Keep existing optional_usage_dict logic)
|
||||
optional_usage_dict = {
|
||||
info['name']: True
|
||||
for info in package_info.get('resource_types_info', [])
|
||||
if info.get('optional_usage')
|
||||
}
|
||||
logger.debug(f"Optional usage elements identified: {optional_usage_dict}")
|
||||
|
||||
# Find existing or create new DB record
|
||||
existing_ig = ProcessedIg.query.filter_by(package_name=name, version=version).first()
|
||||
|
||||
if existing_ig:
|
||||
logger.info(f"Updating existing processed record for {name}#{version}")
|
||||
existing_ig.processed_date = datetime.datetime.now(tz=datetime.timezone.utc)
|
||||
existing_ig.resource_types_info = package_info.get('resource_types_info', [])
|
||||
existing_ig.must_support_elements = package_info.get('must_support_elements')
|
||||
existing_ig.examples = package_info.get('examples')
|
||||
existing_ig.complies_with_profiles = package_info.get('complies_with_profiles', [])
|
||||
existing_ig.imposed_profiles = package_info.get('imposed_profiles', [])
|
||||
existing_ig.optional_usage_elements = optional_usage_dict
|
||||
processed_ig = existing_ig
|
||||
else:
|
||||
logger.info(f"Creating new processed record for {name}#{version}")
|
||||
processed_ig = ProcessedIg(
|
||||
package_name=name,
|
||||
version=version,
|
||||
processed_date=datetime.datetime.now(tz=datetime.timezone.utc),
|
||||
resource_types_info=package_info.get('resource_types_info', []),
|
||||
must_support_elements=package_info.get('must_support_elements'),
|
||||
examples=package_info.get('examples'),
|
||||
complies_with_profiles=package_info.get('complies_with_profiles', []),
|
||||
imposed_profiles=package_info.get('imposed_profiles', []),
|
||||
optional_usage_elements=optional_usage_dict
|
||||
)
|
||||
processed_ig = ProcessedIg(package_name=name, version=version)
|
||||
db.session.add(processed_ig)
|
||||
db.session.commit()
|
||||
|
||||
# Update all fields
|
||||
processed_ig.processed_date = datetime.datetime.now(tz=datetime.timezone.utc)
|
||||
processed_ig.resource_types_info = package_info.get('resource_types_info', [])
|
||||
processed_ig.must_support_elements = package_info.get('must_support_elements')
|
||||
processed_ig.examples = package_info.get('examples')
|
||||
processed_ig.complies_with_profiles = package_info.get('complies_with_profiles', [])
|
||||
processed_ig.imposed_profiles = package_info.get('imposed_profiles', [])
|
||||
processed_ig.optional_usage_elements = optional_usage_dict
|
||||
# --- ADD THIS LINE: Save the extracted conformance map ---
|
||||
processed_ig.search_param_conformance = package_info.get('search_param_conformance') # Get map from results
|
||||
# --- END ADD ---
|
||||
|
||||
db.session.commit() # Commit all changes
|
||||
flash(f"Successfully processed {name}#{version}!", "success")
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
db.session.rollback() # Rollback on error
|
||||
logger.error(f"Error processing IG {filename}: {str(e)}", exc_info=True)
|
||||
flash(f"Error processing IG '{filename}': {str(e)}", "error")
|
||||
else:
|
||||
# Handle CSRF or other form validation errors
|
||||
logger.warning(f"Form validation failed for process-igs: {form.errors}")
|
||||
flash("CSRF token missing or invalid, or other form error.", "error")
|
||||
|
||||
return redirect(url_for('view_igs'))
|
||||
|
||||
# --- End of /process-igs Function ---
|
||||
|
||||
@app.route('/delete-ig', methods=['POST'])
|
||||
def delete_ig():
|
||||
form = FlaskForm()
|
||||
@ -422,13 +444,12 @@ def view_ig(processed_ig_id):
|
||||
config=current_app.config)
|
||||
|
||||
#---------------------------------------------------------------------------------------OLD backup-----------------------------------
|
||||
# @app.route('/get-structure')
|
||||
#@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)
|
||||
@ -439,121 +460,157 @@ def view_ig(processed_ig_id):
|
||||
# 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')
|
||||
#-----------------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
# --- Full /get-structure Function ---
|
||||
@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') # This is the StructureDefinition ID/Name
|
||||
# This is the StructureDefinition ID/Name or base ResourceType
|
||||
resource_type = request.args.get('resource_type')
|
||||
view = request.args.get('view', 'snapshot') # Keep for potential future use
|
||||
|
||||
# --- Parameter Validation ---
|
||||
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
|
||||
|
||||
# --- Package Directory Setup ---
|
||||
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
|
||||
|
||||
# Paths for primary and core packages
|
||||
# --- Paths setup ---
|
||||
tgz_filename = services.construct_tgz_filename(package_name, package_version)
|
||||
tgz_path = os.path.join(packages_dir, tgz_filename)
|
||||
# Assuming CANONICAL_PACKAGE is defined in services (e.g., ('hl7.fhir.r4.core', '4.0.1'))
|
||||
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)
|
||||
@ -572,8 +629,10 @@ def get_structure():
|
||||
|
||||
if primary_package_exists:
|
||||
try:
|
||||
# Assuming find_and_extract_sd handles narrative removal
|
||||
sd_data, _ = services.find_and_extract_sd(tgz_path, resource_type)
|
||||
if sd_data:
|
||||
# Determine the base resource type from the fetched SD
|
||||
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:
|
||||
@ -601,11 +660,11 @@ def get_structure():
|
||||
|
||||
# --- Check if SD data was ultimately found ---
|
||||
if not sd_data:
|
||||
# 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
|
||||
|
||||
# --- Fetch Search Parameters (Primary Package First) ---
|
||||
# find_and_extract_search_params returns a list of dicts with basic SP info
|
||||
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}")
|
||||
@ -631,52 +690,108 @@ def get_structure():
|
||||
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) ---
|
||||
# --- Prepare Snapshot/Differential Elements ---
|
||||
snapshot_elements = sd_data.get('snapshot', {}).get('element', [])
|
||||
differential_elements = sd_data.get('differential', {}).get('element', [])
|
||||
# Create set of IDs from differential elements for efficient lookup
|
||||
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 based on presence in differential_ids set
|
||||
element['isInDifferential'] = bool(element_id and element_id in differential_ids)
|
||||
enriched_elements.append(element)
|
||||
# remove_narrative should ideally be handled within find_and_extract_sd,
|
||||
# but applying it again here ensures it's done if the service function missed it.
|
||||
enriched_elements = [services.remove_narrative(el) for el in enriched_elements]
|
||||
else:
|
||||
# If no snapshot, log warning. Front-end might need adjustment if only differential is sent.
|
||||
logger.warning(f"No snapshot found for {resource_type} in {source_package_id}. Returning empty element list.")
|
||||
enriched_elements = []
|
||||
enriched_elements = [] # Or consider returning differential and handle in JS
|
||||
|
||||
# --- Retrieve Must Support Paths from DB (Existing Logic - slightly refined key lookup) ---
|
||||
# --- Retrieve Must Support Paths from DB ---
|
||||
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:
|
||||
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.")
|
||||
# Query DB once for the ProcessedIg record
|
||||
processed_ig_record = ProcessedIg.query.filter_by(package_name=package_name, version=package_version).first()
|
||||
if processed_ig_record and processed_ig_record.must_support_elements:
|
||||
ms_elements_dict = processed_ig_record.must_support_elements
|
||||
# Try getting MS paths using the profile ID/name first, fallback to base type
|
||||
must_support_paths = ms_elements_dict.get(resource_type, [])
|
||||
if not must_support_paths and base_resource_type_for_sp:
|
||||
must_support_paths = ms_elements_dict.get(base_resource_type_for_sp, [])
|
||||
if must_support_paths:
|
||||
logger.debug(f"Retrieved {len(must_support_paths)} MS paths using base type key '{base_resource_type_for_sp}' from DB.")
|
||||
elif must_support_paths:
|
||||
logger.debug(f"Retrieved {len(must_support_paths)} MS paths using profile key '{resource_type}' from DB.")
|
||||
else:
|
||||
logger.debug(f"No specific Must Support paths found for keys '{resource_type}' or '{base_resource_type_for_sp}' in processed IG DB.")
|
||||
logger.debug(f"No specific MS paths found for keys '{resource_type}' or '{base_resource_type_for_sp}' in DB.")
|
||||
else:
|
||||
logger.debug(f"No processed IG record or no must_support_elements found in DB for {package_name}#{package_version}")
|
||||
|
||||
# --- Fetch and Merge Conformance Data ---
|
||||
search_param_conformance_rules = {}
|
||||
if base_resource_type_for_sp: # Only proceed if we identified the base type
|
||||
# Reuse the DB record queried for Must Support if available
|
||||
if processed_ig_record:
|
||||
# Check if the record has the conformance data attribute and it's not None/empty
|
||||
# **IMPORTANT**: This assumes 'search_param_conformance' column was added to the model
|
||||
if hasattr(processed_ig_record, 'search_param_conformance') and processed_ig_record.search_param_conformance:
|
||||
all_conformance_data = processed_ig_record.search_param_conformance
|
||||
# Get the specific rules map for the current base resource type
|
||||
search_param_conformance_rules = all_conformance_data.get(base_resource_type_for_sp, {})
|
||||
logger.debug(f"Retrieved conformance rules for {base_resource_type_for_sp} from DB: {search_param_conformance_rules}")
|
||||
else:
|
||||
logger.warning(f"ProcessedIg record found, but 'search_param_conformance' attribute/data is missing or empty for {package_name}#{package_version}.")
|
||||
else:
|
||||
# This case should be rare if MS check already happened, but handles it
|
||||
logger.warning(f"No ProcessedIg record found for {package_name}#{package_version} to get conformance rules.")
|
||||
|
||||
# Merge the retrieved conformance rules into the search_params_data list
|
||||
if search_params_data:
|
||||
logger.debug(f"Merging conformance data into {len(search_params_data)} search parameters.")
|
||||
for param in search_params_data:
|
||||
param_code = param.get('code')
|
||||
if param_code:
|
||||
# Lookup the code in the rules; default to 'Optional' if not found
|
||||
conformance_level = search_param_conformance_rules.get(param_code, 'Optional')
|
||||
param['conformance'] = conformance_level # Update the dictionary
|
||||
else:
|
||||
# Handle cases where SearchParameter might lack a 'code' (should be rare)
|
||||
param['conformance'] = 'Unknown'
|
||||
logger.debug("Finished merging conformance data.")
|
||||
else:
|
||||
logger.debug(f"No search parameters found for {base_resource_type_for_sp} to merge conformance data into.")
|
||||
else:
|
||||
logger.warning(f"Cannot fetch conformance data because base resource type (e.g., Patient) for '{resource_type}' could not be determined.")
|
||||
# Ensure existing search params still have a default conformance
|
||||
for param in search_params_data:
|
||||
if 'conformance' not in param or param['conformance'] == 'N/A':
|
||||
param['conformance'] = 'Optional'
|
||||
|
||||
|
||||
# --- Construct the final response ---
|
||||
response_data = {
|
||||
'elements': enriched_elements,
|
||||
'must_support_paths': must_support_paths,
|
||||
'search_parameters': search_params_data, # Include potentially populated list
|
||||
# This list now includes the 'conformance' field with actual values (or 'Optional'/'Unknown')
|
||||
'search_parameters': search_params_data,
|
||||
'fallback_used': fallback_used,
|
||||
'source_package': source_package_id
|
||||
# Consider explicitly including the raw sd_data['differential'] if needed by JS,
|
||||
# otherwise keep it excluded to reduce payload size.
|
||||
# 'differential_elements': differential_elements
|
||||
}
|
||||
|
||||
# Use Response object for consistent JSON formatting and smaller payload
|
||||
# indent=None, separators=(',', ':') creates the most compact JSON
|
||||
return Response(json.dumps(response_data, indent=None, separators=(',', ':')), mimetype='application/json')
|
||||
|
||||
# --- End of /get-structure Function ---
|
||||
|
||||
@app.route('/get-example')
|
||||
def get_example():
|
||||
package_name = request.args.get('package_name')
|
||||
|
@ -1 +0,0 @@
|
||||
|
@ -1 +0,0 @@
|
||||
|
Binary file not shown.
Binary file not shown.
@ -1,6 +1,6 @@
|
||||
#FileLock
|
||||
#Thu Apr 24 13:26:14 UTC 2025
|
||||
server=172.19.0.2\:33797
|
||||
hostName=3e6e009eccb3
|
||||
#Fri Apr 25 13:11:59 UTC 2025
|
||||
server=172.19.0.2\:35493
|
||||
hostName=499bb2429005
|
||||
method=file
|
||||
id=19667fa3b1585036c013c3404aaa964d49518f08071
|
||||
id=1966d138790b0be6a4873c639ee5ac2e23787fd766d
|
||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
1
migrations/README
Normal file
1
migrations/README
Normal file
@ -0,0 +1 @@
|
||||
Single-database configuration for Flask.
|
BIN
migrations/__pycache__/env.cpython-312.pyc
Normal file
BIN
migrations/__pycache__/env.cpython-312.pyc
Normal file
Binary file not shown.
50
migrations/alembic.ini
Normal file
50
migrations/alembic.ini
Normal file
@ -0,0 +1,50 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic,flask_migrate
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[logger_flask_migrate]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = flask_migrate
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
113
migrations/env.py
Normal file
113
migrations/env.py
Normal file
@ -0,0 +1,113 @@
|
||||
import logging
|
||||
from logging.config import fileConfig
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from alembic import context
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
logger = logging.getLogger('alembic.env')
|
||||
|
||||
|
||||
def get_engine():
|
||||
try:
|
||||
# this works with Flask-SQLAlchemy<3 and Alchemical
|
||||
return current_app.extensions['migrate'].db.get_engine()
|
||||
except (TypeError, AttributeError):
|
||||
# this works with Flask-SQLAlchemy>=3
|
||||
return current_app.extensions['migrate'].db.engine
|
||||
|
||||
|
||||
def get_engine_url():
|
||||
try:
|
||||
return get_engine().url.render_as_string(hide_password=False).replace(
|
||||
'%', '%%')
|
||||
except AttributeError:
|
||||
return str(get_engine().url).replace('%', '%%')
|
||||
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
config.set_main_option('sqlalchemy.url', get_engine_url())
|
||||
target_db = current_app.extensions['migrate'].db
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def get_metadata():
|
||||
if hasattr(target_db, 'metadatas'):
|
||||
return target_db.metadatas[None]
|
||||
return target_db.metadata
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url, target_metadata=get_metadata(), literal_binds=True
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
# this callback is used to prevent an auto-migration from being generated
|
||||
# when there are no changes to the schema
|
||||
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||
def process_revision_directives(context, revision, directives):
|
||||
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||
script = directives[0]
|
||||
if script.upgrade_ops.is_empty():
|
||||
directives[:] = []
|
||||
logger.info('No changes in schema detected.')
|
||||
|
||||
conf_args = current_app.extensions['migrate'].configure_args
|
||||
if conf_args.get("process_revision_directives") is None:
|
||||
conf_args["process_revision_directives"] = process_revision_directives
|
||||
|
||||
connectable = get_engine()
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=get_metadata(),
|
||||
**conf_args
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
24
migrations/script.py.mako
Normal file
24
migrations/script.py.mako
Normal file
@ -0,0 +1,24 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
@ -5,3 +5,5 @@ requests==2.31.0
|
||||
Flask-WTF==1.2.1
|
||||
WTForms==3.1.2
|
||||
Pytest
|
||||
fhir.resources==8.0.0
|
||||
Flask-Migrate==4.1.0
|
1020
services.py
1020
services.py
File diff suppressed because it is too large
Load Diff
@ -1168,70 +1168,111 @@ function renderBindingsTable(elements) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Function to Render Search Parameters Table (Add Logging) ---
|
||||
// --- Function to Render Search Parameters Table (Includes Conformance Badges & Tooltips) ---
|
||||
function renderSearchParametersTable(searchParamsData) {
|
||||
// Find the table body element using its ID
|
||||
const tableBody = document.getElementById('searchparams-table')?.querySelector('tbody');
|
||||
|
||||
// Check if the table body element exists
|
||||
if (!tableBody) {
|
||||
console.error('Search Params table body not found');
|
||||
return;
|
||||
console.error('Search Params table body element (#searchparams-table > tbody) not found');
|
||||
return; // Exit if the table body isn't found
|
||||
}
|
||||
tableBody.innerHTML = ''; // Clear existing rows
|
||||
|
||||
// *** ADDED LOG: Check data received by rendering function ***
|
||||
tableBody.innerHTML = ''; // Clear any existing rows (like loading indicators)
|
||||
|
||||
// Log the data received for debugging
|
||||
console.log('[renderSearchParametersTable] Received data:', searchParamsData);
|
||||
// **********************************************************
|
||||
|
||||
// Check if the received data is valid and is an array with items
|
||||
if (!searchParamsData || !Array.isArray(searchParamsData) || searchParamsData.length === 0) {
|
||||
console.log('[renderSearchParametersTable] No valid search parameter data found, displaying message.'); // Added Log
|
||||
console.log('[renderSearchParametersTable] No valid search parameter data found, displaying message.');
|
||||
// Display a message indicating no parameters were found
|
||||
const row = tableBody.insertRow();
|
||||
const cell = row.insertCell();
|
||||
cell.colSpan = 4;
|
||||
cell.colSpan = 4; // Span across all columns
|
||||
cell.textContent = 'No relevant search parameters found for this resource type.';
|
||||
cell.style.textAlign = 'center';
|
||||
cell.classList.add('text-muted', 'fst-italic');
|
||||
return;
|
||||
cell.classList.add('text-muted', 'fst-italic'); // Style the message
|
||||
return; // Exit the function
|
||||
}
|
||||
|
||||
// Sort parameters alphabetically by code
|
||||
// Sort parameters alphabetically by their 'code' before rendering
|
||||
searchParamsData.sort((a, b) => (a.code || '').localeCompare(b.code || ''));
|
||||
|
||||
// Iterate through each search parameter object in the sorted array
|
||||
searchParamsData.forEach((param, index) => {
|
||||
// *** ADDED LOG: Log each parameter being processed ***
|
||||
console.log(`[renderSearchParametersTable] Processing param ${index + 1}:`, param);
|
||||
// ***************************************************
|
||||
console.log(`[renderSearchParametersTable] Processing param ${index + 1}:`, param); // Log each parameter
|
||||
|
||||
const row = tableBody.insertRow();
|
||||
const row = tableBody.insertRow(); // Create a new table row
|
||||
|
||||
// Parameter Name (using 'code')
|
||||
// --- Column 1: Parameter Code ---
|
||||
const nameCell = row.insertCell();
|
||||
// Use escapeHtmlAttr (ensure this helper function exists in your scope)
|
||||
// Display the parameter code within a <code> tag
|
||||
nameCell.innerHTML = `<code>${escapeHtmlAttr(param.code || 'N/A')}</code>`;
|
||||
|
||||
// Type
|
||||
row.insertCell().textContent = param.type || 'N/A';
|
||||
// --- Column 2: Parameter Type ---
|
||||
row.insertCell().textContent = param.type || 'N/A'; // Display the parameter type
|
||||
|
||||
// Conformance (Placeholder)
|
||||
// --- Column 3: Conformance Level (with Badges and Tooltip) ---
|
||||
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) { ... }
|
||||
// Default to 'Optional' if conformance isn't specified in the data
|
||||
const conformanceValue = param.conformance || 'Optional';
|
||||
let badgeClass = 'bg-secondary'; // Default badge style
|
||||
let titleText = 'Conformance level not specified in CapabilityStatement.'; // Default tooltip text
|
||||
|
||||
// Description
|
||||
// Determine badge style and tooltip text based on the conformance value
|
||||
if (conformanceValue === 'SHALL') {
|
||||
badgeClass = 'bg-danger'; // Red for SHALL (Mandatory)
|
||||
titleText = 'Server SHALL support this parameter.';
|
||||
} else if (conformanceValue === 'SHOULD') {
|
||||
badgeClass = 'bg-warning text-dark'; // Yellow for SHOULD (Recommended)
|
||||
titleText = 'Server SHOULD support this parameter.';
|
||||
} else if (conformanceValue === 'MAY') {
|
||||
badgeClass = 'bg-info text-dark'; // Blue for MAY (Optional but defined)
|
||||
titleText = 'Server MAY support this parameter.';
|
||||
} else if (conformanceValue === 'Optional') {
|
||||
badgeClass = 'bg-light text-dark border'; // Light grey for Optional (Not explicitly mentioned in Caps)
|
||||
titleText = 'Server support for this parameter is optional.';
|
||||
}
|
||||
// Add handling for other potential values like 'SHALL-NOT', 'SHOULD-NOT' if needed
|
||||
|
||||
// Set the tooltip attributes
|
||||
conformanceCell.setAttribute('title', titleText);
|
||||
conformanceCell.setAttribute('data-bs-toggle', 'tooltip');
|
||||
conformanceCell.setAttribute('data-bs-placement', 'top');
|
||||
// Set the cell content with the styled badge
|
||||
conformanceCell.innerHTML = `<span class="badge ${badgeClass}">${escapeHtmlAttr(conformanceValue)}</span>`;
|
||||
|
||||
// Initialize or re-initialize the Bootstrap tooltip for this specific cell
|
||||
// This is important because the table rows are added dynamically
|
||||
const existingTooltip = bootstrap.Tooltip.getInstance(conformanceCell);
|
||||
if (existingTooltip) {
|
||||
existingTooltip.dispose(); // Remove any previous tooltip instance to avoid conflicts
|
||||
}
|
||||
new bootstrap.Tooltip(conformanceCell); // Create and initialize the new tooltip
|
||||
|
||||
// --- Column 4: Description (with FHIRPath Tooltip if available) ---
|
||||
const descriptionCell = row.insertCell();
|
||||
// Use escapeHtmlAttr for the description text
|
||||
descriptionCell.innerHTML = `<small>${escapeHtmlAttr(param.description || 'No description available.')}</small>`;
|
||||
// If a FHIRPath expression exists, add it as a tooltip
|
||||
if (param.expression) {
|
||||
descriptionCell.title = `FHIRPath: ${param.expression}`;
|
||||
descriptionCell.setAttribute('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
|
||||
// Initialize or re-initialize the tooltip
|
||||
const existingDescTooltip = bootstrap.Tooltip.getInstance(descriptionCell);
|
||||
if (existingDescTooltip) {
|
||||
existingDescTooltip.dispose();
|
||||
}
|
||||
new bootstrap.Tooltip(descriptionCell);
|
||||
}
|
||||
});
|
||||
console.log('[renderSearchParametersTable] Finished rendering parameters.'); // Added Log
|
||||
} // End renderSearchParametersTable
|
||||
}); // End forEach loop over searchParamsData
|
||||
|
||||
console.log('[renderSearchParametersTable] Finished rendering parameters.');
|
||||
} // End renderSearchParametersTable function
|
||||
|
||||
|
||||
// --- DOMContentLoaded Listener ---
|
||||
|
@ -88,13 +88,13 @@
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log("DOMContentLoaded event fired."); // Log when the listener starts
|
||||
console.log("DOMContentLoaded event fired.");
|
||||
|
||||
// --- Get DOM Elements & Check Existence ---
|
||||
const form = document.getElementById('fhirRequestForm');
|
||||
const sendButton = document.getElementById('sendRequest');
|
||||
const fhirPathInput = document.getElementById('fhirPath'); // Renamed for clarity
|
||||
const requestBodyInput = document.getElementById('requestBody'); // Renamed for clarity
|
||||
const fhirPathInput = document.getElementById('fhirPath');
|
||||
const requestBodyInput = document.getElementById('requestBody');
|
||||
const requestBodyGroup = document.getElementById('requestBodyGroup');
|
||||
const jsonError = document.getElementById('jsonError');
|
||||
const responseCard = document.getElementById('responseCard');
|
||||
@ -104,303 +104,298 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const methodRadios = document.getElementsByName('method');
|
||||
const toggleServerButton = document.getElementById('toggleServer');
|
||||
const toggleLabel = document.getElementById('toggleLabel');
|
||||
const fhirServerUrlInput = document.getElementById('fhirServerUrl'); // Renamed for clarity
|
||||
const fhirServerUrlInput = document.getElementById('fhirServerUrl');
|
||||
const copyRequestBodyButton = document.getElementById('copyRequestBody');
|
||||
const copyResponseHeadersButton = document.getElementById('copyResponseHeaders');
|
||||
const copyResponseBodyButton = document.getElementById('copyResponseBody');
|
||||
|
||||
// --- Element Existence Check ---
|
||||
let elementsReady = true;
|
||||
if (!form) { console.error("Element not found: #fhirRequestForm"); elementsReady = false; }
|
||||
if (!sendButton) { console.error("Element not found: #sendRequest"); elementsReady = false; }
|
||||
if (!fhirPathInput) { console.error("Element not found: #fhirPath"); elementsReady = false; }
|
||||
if (!requestBodyInput) { console.warn("Element not found: #requestBody"); } // Warn only
|
||||
if (!requestBodyGroup) { console.warn("Element not found: #requestBodyGroup"); } // Warn only
|
||||
if (!jsonError) { console.warn("Element not found: #jsonError"); } // Warn only
|
||||
if (!responseCard) { console.error("Element not found: #responseCard"); elementsReady = false; }
|
||||
if (!responseStatus) { console.error("Element not found: #responseStatus"); elementsReady = false; }
|
||||
if (!responseHeaders) { console.error("Element not found: #responseHeaders"); elementsReady = false; }
|
||||
if (!responseBody) { console.error("Element not found: #responseBody"); elementsReady = false; }
|
||||
if (!toggleServerButton) { console.error("Element not found: #toggleServer"); elementsReady = false; }
|
||||
if (!toggleLabel) { console.error("Element not found: #toggleLabel"); elementsReady = false; }
|
||||
if (!fhirServerUrlInput) { console.error("Element not found: #fhirServerUrl"); elementsReady = false; }
|
||||
// Add checks for copy buttons if needed
|
||||
|
||||
if (!elementsReady) {
|
||||
// Basic check for critical elements
|
||||
if (!form || !sendButton || !fhirPathInput || !responseCard || !toggleServerButton || !fhirServerUrlInput || !responseStatus || !responseHeaders || !responseBody || !toggleLabel) {
|
||||
console.error("One or more critical UI elements could not be found. Script execution halted.");
|
||||
alert("Error initializing UI components. Please check the console.");
|
||||
return; // Stop script execution
|
||||
}
|
||||
console.log("All critical elements checked/found.");
|
||||
|
||||
// --- State Variable ---
|
||||
// Default assumes standalone, will be forced otherwise by appMode check below
|
||||
let useLocalHapi = true;
|
||||
|
||||
// --- DEFINE FUNCTIONS ---
|
||||
// --- Get App Mode from Flask Context ---
|
||||
// Ensure this variable is correctly passed from Flask using the context_processor
|
||||
const appMode = '{{ app_mode | default("standalone") | lower }}';
|
||||
console.log('App Mode Detected:', appMode);
|
||||
|
||||
function updateServerToggleUI() {
|
||||
if (!toggleLabel || !fhirServerUrlInput) { console.error("updateServerToggleUI: Toggle UI elements missing!"); return; }
|
||||
toggleLabel.textContent = useLocalHapi ? 'Use Local HAPI' : 'Use Custom URL';
|
||||
fhirServerUrlInput.style.display = useLocalHapi ? 'none' : 'block';
|
||||
fhirServerUrlInput.classList.remove('is-invalid');
|
||||
}
|
||||
|
||||
function toggleServer() {
|
||||
if (appMode === 'lite') return; // Ignore clicks in Lite mode
|
||||
useLocalHapi = !useLocalHapi;
|
||||
updateServerToggleUI();
|
||||
if (useLocalHapi && fhirServerUrlInput) {
|
||||
fhirServerUrlInput.value = '';
|
||||
}
|
||||
console.log(`Server toggled: ${useLocalHapi ? 'Local HAPI' : 'Custom URL'}`);
|
||||
}
|
||||
|
||||
function updateRequestBodyVisibility() {
|
||||
const selectedMethod = document.querySelector('input[name="method"]:checked')?.value;
|
||||
if (!requestBodyGroup || !selectedMethod) return;
|
||||
requestBodyGroup.style.display = (selectedMethod === 'POST' || selectedMethod === 'PUT') ? 'block' : 'none';
|
||||
if (selectedMethod !== 'POST' && selectedMethod !== 'PUT') {
|
||||
if (requestBodyInput) requestBodyInput.value = '';
|
||||
if (jsonError) jsonError.style.display = 'none';
|
||||
}
|
||||
}
|
||||
// --- DEFINE HELPER FUNCTIONS ---
|
||||
|
||||
// Validates request body, returns null on error, otherwise returns body string or empty string
|
||||
function validateRequestBody(method, path) {
|
||||
if (!requestBodyInput || !jsonError) return (method === 'POST' || method === 'PUT') ? '' : undefined; // Return empty string for modifying methods if field missing, else undefined
|
||||
if (!requestBodyInput || !jsonError) return (method === 'POST' || method === 'PUT') ? '' : undefined;
|
||||
const bodyValue = requestBodyInput.value.trim();
|
||||
if (!bodyValue) {
|
||||
requestBodyInput.classList.remove('is-invalid');
|
||||
jsonError.style.display = 'none';
|
||||
return ''; // Empty body is valid for POST/PUT
|
||||
}
|
||||
requestBodyInput.classList.remove('is-invalid'); // Reset validation
|
||||
jsonError.style.display = 'none';
|
||||
|
||||
if (!bodyValue) return ''; // Empty body is valid for POST/PUT
|
||||
|
||||
const isSearch = path && path.endsWith('_search');
|
||||
const isJson = bodyValue.startsWith('{') || bodyValue.startsWith('[');
|
||||
const isForm = !isJson; // Assume form urlencoded if not JSON
|
||||
const isXml = bodyValue.startsWith('<');
|
||||
const isForm = !isJson && !isXml;
|
||||
|
||||
if (method === 'POST' && isSearch && isForm) { // POST Search with form params
|
||||
requestBodyInput.classList.remove('is-invalid');
|
||||
jsonError.style.display = 'none';
|
||||
return bodyValue;
|
||||
} else if (method === 'POST' || method === 'PUT') { // Other POST/PUT expect JSON
|
||||
try {
|
||||
JSON.parse(bodyValue);
|
||||
requestBodyInput.classList.remove('is-invalid');
|
||||
jsonError.style.display = 'none';
|
||||
return bodyValue; // Return the valid JSON string
|
||||
} catch (e) {
|
||||
requestBodyInput.classList.add('is-invalid');
|
||||
jsonError.textContent = `Invalid JSON: ${e.message}`;
|
||||
jsonError.style.display = 'block';
|
||||
return null; // Indicate validation error
|
||||
} else if (method === 'POST' || method === 'PUT') { // Other POST/PUT expect JSON/XML
|
||||
if (isJson) {
|
||||
try { JSON.parse(bodyValue); return bodyValue; }
|
||||
catch (e) { jsonError.textContent = `Invalid JSON: ${e.message}`; }
|
||||
} else if (isXml) {
|
||||
// Basic XML check is difficult in JS, accept it for now
|
||||
// Backend or target server will validate fully
|
||||
return bodyValue;
|
||||
} else { // Neither JSON nor XML, and not a POST search form
|
||||
jsonError.textContent = 'Request body must be valid JSON or XML for PUT/POST (unless using POST _search with form parameters).';
|
||||
}
|
||||
requestBodyInput.classList.add('is-invalid');
|
||||
jsonError.style.display = 'block';
|
||||
return null; // Indicate validation error
|
||||
}
|
||||
// For GET/DELETE, body should not be sent, but we don't error here
|
||||
requestBodyInput.classList.remove('is-invalid');
|
||||
jsonError.style.display = 'none';
|
||||
return undefined; // Indicate no body should be sent
|
||||
return undefined; // Indicate no body should be sent for GET/DELETE
|
||||
}
|
||||
|
||||
// Cleans path for proxying
|
||||
function cleanFhirPath(path) {
|
||||
if (!path) return '';
|
||||
// Remove optional leading 'r4/' or 'fhir/', then trim slashes
|
||||
const cleaned = path.replace(/^(r4\/|fhir\/)*/i, '').replace(/^\/+|\/+$/g, '');
|
||||
return cleaned;
|
||||
return path.replace(/^(r4\/|fhir\/)*/i, '').replace(/^\/+|\/+$/g, '');
|
||||
}
|
||||
|
||||
// Copies text to clipboard
|
||||
async function copyToClipboard(text, button) {
|
||||
if (text === null || text === undefined || !button) return;
|
||||
if (text === null || text === undefined || !button || !navigator.clipboard) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(String(text)); // Ensure text is string
|
||||
await navigator.clipboard.writeText(String(text));
|
||||
const originalIcon = button.innerHTML;
|
||||
button.innerHTML = '<i class="bi bi-check-lg"></i>';
|
||||
setTimeout(() => { if(button.isConnected) button.innerHTML = originalIcon; }, 2000);
|
||||
} catch (err) { console.error('Copy failed:', err); alert('Failed to copy to clipboard'); }
|
||||
button.innerHTML = '<i class="bi bi-check-lg"></i> Copied!';
|
||||
setTimeout(() => { if(button.isConnected) button.innerHTML = originalIcon; }, 1500);
|
||||
} catch (err) { console.error('Copy failed:', err); /* Optionally alert user */ }
|
||||
}
|
||||
|
||||
// Updates the UI elements related to the server toggle button/input
|
||||
function updateServerToggleUI() {
|
||||
if (appMode === 'lite') {
|
||||
// LITE MODE: Force Custom URL, disable toggle
|
||||
useLocalHapi = false; // Force state
|
||||
toggleServerButton.disabled = true;
|
||||
toggleServerButton.classList.add('disabled');
|
||||
toggleServerButton.style.pointerEvents = 'none'; // Make unclickable
|
||||
toggleServerButton.setAttribute('aria-disabled', 'true');
|
||||
toggleServerButton.title = "Local HAPI server is unavailable in Lite mode";
|
||||
toggleLabel.textContent = 'Use Custom URL'; // Always show this label
|
||||
fhirServerUrlInput.style.display = 'block'; // Always show input
|
||||
fhirServerUrlInput.placeholder = "Enter FHIR Base URL (Local HAPI unavailable)";
|
||||
fhirServerUrlInput.required = true; // Make required in lite mode
|
||||
} else {
|
||||
// STANDALONE MODE: Allow toggle
|
||||
toggleServerButton.disabled = false;
|
||||
toggleServerButton.classList.remove('disabled');
|
||||
toggleServerButton.style.pointerEvents = 'auto';
|
||||
toggleServerButton.removeAttribute('aria-disabled');
|
||||
toggleServerButton.title = "Toggle between Local HAPI and Custom URL";
|
||||
toggleLabel.textContent = useLocalHapi ? 'Using Local HAPI' : 'Using Custom URL';
|
||||
fhirServerUrlInput.style.display = useLocalHapi ? 'none' : 'block';
|
||||
fhirServerUrlInput.placeholder = "e.g., https://hapi.fhir.org/baseR4";
|
||||
fhirServerUrlInput.required = !useLocalHapi; // Required only if custom is selected
|
||||
}
|
||||
fhirServerUrlInput.classList.remove('is-invalid'); // Clear validation state on toggle
|
||||
console.log(`UI Updated: useLocalHapi=${useLocalHapi}, Button Disabled=${toggleServerButton.disabled}, Input Visible=${fhirServerUrlInput.style.display !== 'none'}, Input Required=${fhirServerUrlInput.required}`);
|
||||
}
|
||||
|
||||
// Toggles the server selection state (only effective in Standalone mode)
|
||||
function toggleServer() {
|
||||
if (appMode === 'lite') {
|
||||
console.log("Toggle ignored: Lite mode active.");
|
||||
return; // Do nothing in lite mode
|
||||
}
|
||||
useLocalHapi = !useLocalHapi;
|
||||
if (useLocalHapi && fhirServerUrlInput) {
|
||||
fhirServerUrlInput.value = ''; // Clear custom URL when switching to local
|
||||
}
|
||||
updateServerToggleUI(); // Update UI based on new state
|
||||
console.log(`Server toggled: Now using ${useLocalHapi ? 'Local HAPI' : 'Custom URL'}`);
|
||||
}
|
||||
|
||||
// Updates visibility of the Request Body textarea based on selected HTTP method
|
||||
function updateRequestBodyVisibility() {
|
||||
const selectedMethod = document.querySelector('input[name="method"]:checked')?.value;
|
||||
if (!requestBodyGroup || !selectedMethod) return;
|
||||
const showBody = (selectedMethod === 'POST' || selectedMethod === 'PUT');
|
||||
requestBodyGroup.style.display = showBody ? 'block' : 'none';
|
||||
if (!showBody) { // Clear body and errors if body not needed
|
||||
if (requestBodyInput) requestBodyInput.value = '';
|
||||
if (jsonError) jsonError.style.display = 'none';
|
||||
if (requestBodyInput) requestBodyInput.classList.remove('is-invalid');
|
||||
}
|
||||
}
|
||||
|
||||
// --- INITIAL SETUP & MODE CHECK ---
|
||||
const appMode = '{{ app_mode | default("standalone") | lower }}';
|
||||
console.log('App Mode:', appMode);
|
||||
|
||||
if (appMode === 'lite') {
|
||||
console.log('Lite mode detected: Disabling local HAPI toggle.');
|
||||
if (toggleServerButton) {
|
||||
toggleServerButton.disabled = true;
|
||||
toggleServerButton.title = "Local HAPI is not available in Lite mode";
|
||||
toggleServerButton.classList.add('disabled');
|
||||
useLocalHapi = false;
|
||||
if (fhirServerUrlInput) { fhirServerUrlInput.placeholder = "Enter FHIR Base URL (Local HAPI unavailable)"; }
|
||||
} else { console.error("Lite mode: Could not find toggle server button."); }
|
||||
} else {
|
||||
console.log('Standalone mode detected.');
|
||||
if (toggleServerButton) { toggleServerButton.disabled = false; toggleServerButton.title = ""; toggleServerButton.classList.remove('disabled'); }
|
||||
else { console.error("Standalone mode: Could not find toggle server button."); }
|
||||
}
|
||||
|
||||
// --- SET INITIAL UI STATE ---
|
||||
updateServerToggleUI();
|
||||
updateRequestBodyVisibility();
|
||||
updateServerToggleUI(); // Set initial UI based on detected mode and default state
|
||||
updateRequestBodyVisibility(); // Set initial visibility based on default method (GET)
|
||||
|
||||
// --- ATTACH EVENT LISTENERS ---
|
||||
if (toggleServerButton) { toggleServerButton.addEventListener('click', toggleServer); }
|
||||
toggleServerButton.addEventListener('click', toggleServer);
|
||||
if (methodRadios) { methodRadios.forEach(radio => { radio.addEventListener('change', updateRequestBodyVisibility); }); }
|
||||
if (requestBodyInput && fhirPathInput) { requestBodyInput.addEventListener('input', () => validateRequestBody( document.querySelector('input[name="method"]:checked')?.value, fhirPathInput.value )); }
|
||||
if (copyRequestBodyButton && requestBodyInput) { copyRequestBodyButton.addEventListener('click', () => copyToClipboard(requestBodyInput.value, copyRequestBodyButton)); }
|
||||
if (copyResponseHeadersButton && responseHeaders) { copyResponseHeadersButton.addEventListener('click', () => copyToClipboard(responseHeaders.textContent, copyResponseHeadersButton)); }
|
||||
if (copyResponseBodyButton && responseBody) { copyResponseBodyButton.addEventListener('click', () => copyToClipboard(responseBody.textContent, copyResponseBodyButton)); }
|
||||
if (methodRadios) { methodRadios.forEach(radio => { radio.addEventListener('change', updateRequestBodyVisibility); }); }
|
||||
if (requestBodyInput && fhirPathInput) { requestBodyInput.addEventListener('input', () => validateRequestBody( document.querySelector('input[name="method"]:checked')?.value, fhirPathInput.value )); }
|
||||
|
||||
// --- Send Request Button Listener (CORRECTED) ---
|
||||
if (sendButton && fhirPathInput && form) {
|
||||
sendButton.addEventListener('click', async function() {
|
||||
console.log("Send Request button clicked.");
|
||||
sendButton.disabled = true; sendButton.textContent = 'Sending...';
|
||||
responseCard.style.display = 'none';
|
||||
responseStatus.textContent = ''; responseHeaders.textContent = ''; responseBody.textContent = '';
|
||||
responseStatus.className = 'badge'; // Reset badge class
|
||||
// --- Send Request Button Listener ---
|
||||
sendButton.addEventListener('click', async function() {
|
||||
console.log("Send Request button clicked.");
|
||||
// --- UI Reset ---
|
||||
sendButton.disabled = true; sendButton.textContent = 'Sending...';
|
||||
responseCard.style.display = 'none';
|
||||
responseStatus.textContent = ''; responseHeaders.textContent = ''; responseBody.textContent = '';
|
||||
responseStatus.className = 'badge'; // Reset badge class
|
||||
fhirServerUrlInput.classList.remove('is-invalid'); // Reset validation
|
||||
if(requestBodyInput) requestBodyInput.classList.remove('is-invalid');
|
||||
if(jsonError) jsonError.style.display = 'none';
|
||||
|
||||
// 1. Get Values
|
||||
const path = fhirPathInput.value.trim();
|
||||
const method = document.querySelector('input[name="method"]:checked')?.value;
|
||||
let body = undefined; // Default to no body
|
||||
// --- Get Values ---
|
||||
const path = fhirPathInput.value.trim();
|
||||
const method = document.querySelector('input[name="method"]:checked')?.value;
|
||||
const customUrl = fhirServerUrlInput.value.trim();
|
||||
let body = undefined;
|
||||
|
||||
if (!path) { alert('Please enter a FHIR Path.'); sendButton.disabled = false; sendButton.textContent = 'Send Request'; return; }
|
||||
if (!method) { alert('Please select a Request Type.'); sendButton.disabled = false; sendButton.textContent = 'Send Request'; return; }
|
||||
// --- Basic Input Validation ---
|
||||
if (!path) { alert('Please enter a FHIR Path.'); sendButton.disabled = false; sendButton.textContent = 'Send Request'; return; }
|
||||
if (!method) { alert('Please select a Request Type.'); sendButton.disabled = false; sendButton.textContent = 'Send Request'; return; }
|
||||
if (!useLocalHapi && !customUrl) { // Custom URL mode needs a URL
|
||||
alert('Please enter a custom FHIR Server URL.'); fhirServerUrlInput.classList.add('is-invalid'); sendButton.disabled = false; sendButton.textContent = 'Send Request'; return;
|
||||
}
|
||||
if (!useLocalHapi && customUrl) { // Validate custom URL format
|
||||
try { new URL(customUrl); }
|
||||
catch (_) { alert('Invalid custom FHIR Server URL format.'); fhirServerUrlInput.classList.add('is-invalid'); sendButton.disabled = false; sendButton.textContent = 'Send Request'; return; }
|
||||
}
|
||||
|
||||
// 2. Validate & Get Body (if needed)
|
||||
if (method === 'POST' || method === 'PUT') {
|
||||
body = validateRequestBody(method, path);
|
||||
if (body === null) { // null indicates validation error
|
||||
alert('Request body contains invalid JSON.'); sendButton.disabled = false; sendButton.textContent = 'Send Request'; return;
|
||||
}
|
||||
// If body is empty string, ensure it's treated as such for fetch
|
||||
if (body === '') body = '';
|
||||
// --- Validate & Get Body (if needed) ---
|
||||
if (method === 'POST' || method === 'PUT') {
|
||||
body = validateRequestBody(method, path);
|
||||
if (body === null) { // null indicates validation error
|
||||
alert('Request body contains invalid JSON/Format.'); sendButton.disabled = false; sendButton.textContent = 'Send Request'; return;
|
||||
}
|
||||
// If body is empty string, ensure it's treated as such for fetch
|
||||
if (body === '') body = '';
|
||||
}
|
||||
|
||||
// --- Determine Fetch URL and Headers ---
|
||||
const cleanedPath = cleanFhirPath(path);
|
||||
const finalFetchUrl = '/fhir/' + cleanedPath; // Always send to the backend proxy endpoint
|
||||
const headers = { 'Accept': 'application/fhir+json, application/fhir+xml;q=0.9, */*;q=0.8' };
|
||||
|
||||
// Determine Content-Type if body exists
|
||||
if (body !== undefined) {
|
||||
if (body.trim().startsWith('{')) { headers['Content-Type'] = 'application/fhir+json'; }
|
||||
else if (body.trim().startsWith('<')) { headers['Content-Type'] = 'application/fhir+xml'; }
|
||||
else if (method === 'POST' && path.endsWith('_search') && body && !body.trim().startsWith('{') && !body.trim().startsWith('<')) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; }
|
||||
else if (body) { headers['Content-Type'] = 'application/fhir+json'; } // Default if unknown but present
|
||||
}
|
||||
|
||||
// Add Custom Target Header if needed
|
||||
if (!useLocalHapi && customUrl) {
|
||||
headers['X-Target-FHIR-Server'] = customUrl.replace(/\/+$/, ''); // Send custom URL without trailing slash
|
||||
console.log("Adding header X-Target-FHIR-Server:", headers['X-Target-FHIR-Server']);
|
||||
}
|
||||
|
||||
// Add CSRF token ONLY if sending to local proxy (for modifying methods)
|
||||
const csrfTokenInput = form.querySelector('input[name="csrf_token"]');
|
||||
const csrfToken = csrfTokenInput ? csrfTokenInput.value : null;
|
||||
// Include DELETE method for CSRF check
|
||||
if (useLocalHapi && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(method) && csrfToken) {
|
||||
headers['X-CSRFToken'] = csrfToken;
|
||||
console.log("CSRF Token added for local request.");
|
||||
} else if (useLocalHapi && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(method) && !csrfToken) {
|
||||
console.warn("CSRF token input not found for local modifying request.");
|
||||
}
|
||||
|
||||
console.log(`Executing Fetch: Method=${method}, URL=${finalFetchUrl}, LocalHAPI=${useLocalHapi}`);
|
||||
console.log("Request Headers:", headers);
|
||||
if (body !== undefined) console.log("Request Body (first 300 chars):", (body || '').substring(0, 300) + ((body?.length ?? 0) > 300 ? "..." : ""));
|
||||
|
||||
// --- Make the Fetch Request ---
|
||||
try {
|
||||
const response = await fetch(finalFetchUrl, {
|
||||
method: method,
|
||||
headers: headers,
|
||||
body: body // Will be undefined for GET/DELETE
|
||||
});
|
||||
|
||||
// --- Process Response ---
|
||||
responseCard.style.display = 'block';
|
||||
responseStatus.textContent = `${response.status} ${response.statusText}`;
|
||||
responseStatus.className = `badge ${response.ok ? 'bg-success' : 'bg-danger'}`;
|
||||
|
||||
let headerText = '';
|
||||
response.headers.forEach((value, key) => { headerText += `${key}: ${value}\n`; });
|
||||
responseHeaders.textContent = headerText.trim();
|
||||
|
||||
const responseContentType = response.headers.get('content-type') || '';
|
||||
let responseBodyText = await response.text();
|
||||
let displayBody = responseBodyText;
|
||||
|
||||
// Attempt to pretty-print JSON
|
||||
if (responseContentType.includes('json') && responseBodyText.trim()) {
|
||||
try { displayBody = JSON.stringify(JSON.parse(responseBodyText), null, 2); }
|
||||
catch (e) { console.warn("Failed to pretty-print JSON response:", e); /* Show raw text */ }
|
||||
}
|
||||
// Attempt to pretty-print XML (basic indentation)
|
||||
else if (responseContentType.includes('xml') && responseBodyText.trim()) {
|
||||
try {
|
||||
let formattedXml = '';
|
||||
let indent = 0;
|
||||
const xmlLines = responseBodyText.replace(/>\s*</g, '><').split(/(<[^>]+>)/);
|
||||
for (let i = 0; i < xmlLines.length; i++) {
|
||||
const node = xmlLines[i];
|
||||
if (!node || node.trim().length === 0) continue;
|
||||
if (node.match(/^<\/\w/)) indent--; // Closing tag
|
||||
formattedXml += ' '.repeat(Math.max(0, indent)) + node.trim() + '\n';
|
||||
if (node.match(/^<\w/) && !node.match(/\/>$/) && !node.match(/^<\?/)) indent++; // Opening tag
|
||||
}
|
||||
displayBody = formattedXml.trim();
|
||||
} catch(e) { console.warn("Basic XML formatting failed:", e); /* Show raw text */ }
|
||||
}
|
||||
|
||||
// 3. Determine Target URL
|
||||
const cleanedPath = cleanFhirPath(path);
|
||||
const proxyBase = '/fhir'; // Always use the toolkit's proxy endpoint
|
||||
let targetUrl = useLocalHapi ? `${proxyBase}/${cleanedPath}` : fhirServerUrlInput.value.trim().replace(/\/+$/, '') + `/${cleanedPath}`;
|
||||
let finalFetchUrl = proxyBase + '/' + cleanedPath; // URL to send the fetch request TO
|
||||
responseBody.textContent = displayBody;
|
||||
|
||||
if (!useLocalHapi) {
|
||||
const customUrl = fhirServerUrlInput.value.trim();
|
||||
if (!customUrl) {
|
||||
alert('Please enter a custom FHIR Server URL when not using Local HAPI.');
|
||||
fhirServerUrlInput.classList.add('is-invalid');
|
||||
sendButton.disabled = false; sendButton.textContent = 'Send Request';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Validate custom URL format, though actual reachability is checked by backend proxy
|
||||
new URL(customUrl);
|
||||
fhirServerUrlInput.classList.remove('is-invalid');
|
||||
// The proxy needs the *full* path including the custom base, it doesn't know it otherwise
|
||||
// We modify the path being sent TO THE PROXY
|
||||
// Let backend handle the full external URL based on path sent to it
|
||||
// This assumes the proxy handles the full path correctly.
|
||||
// *Correction*: Proxy expects just the relative path. Send custom URL in header?
|
||||
// Let's stick to the original proxy design: frontend sends relative path to /fhir/,
|
||||
// backend needs to know if it's proxying to local or custom.
|
||||
// WE NEED TO TELL THE BACKEND WHICH URL TO PROXY TO!
|
||||
// This requires backend changes. For now, assume proxy ALWAYS goes to localhost:8080.
|
||||
// To make custom URL work, the backend proxy needs modification.
|
||||
// Sticking with current backend: Custom URL cannot be proxied via '/fhir/'.
|
||||
// --> Reverting to only allowing local proxy for now.
|
||||
if (!useLocalHapi) {
|
||||
alert("Current implementation only supports proxying to the local HAPI server via '/fhir'. Custom URL feature requires backend changes to the proxy route.");
|
||||
sendButton.disabled = false; sendButton.textContent = 'Send Request';
|
||||
return;
|
||||
}
|
||||
// If backend were changed, we'd likely send the custom URL in a header or modify the request path.
|
||||
} catch (_) {
|
||||
alert('Invalid custom FHIR Server URL format.');
|
||||
fhirServerUrlInput.classList.add('is-invalid');
|
||||
sendButton.disabled = false; sendButton.textContent = 'Send Request';
|
||||
return;
|
||||
}
|
||||
// Highlight response body if Prism.js is available
|
||||
if (typeof Prism !== 'undefined') {
|
||||
responseBody.className = 'border p-2 bg-light'; // Reset base classes
|
||||
if (responseContentType.includes('json')) {
|
||||
responseBody.classList.add('language-json');
|
||||
} else if (responseContentType.includes('xml')) {
|
||||
responseBody.classList.add('language-xml');
|
||||
} // Add other languages if needed
|
||||
Prism.highlightElement(responseBody);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
responseCard.style.display = 'block';
|
||||
responseStatus.textContent = `Network Error`;
|
||||
responseStatus.className = 'badge bg-danger';
|
||||
responseHeaders.textContent = 'N/A';
|
||||
// Provide a more informative error message
|
||||
let errorDetail = `Error: ${error.message}\n\n`;
|
||||
if (useLocalHapi) {
|
||||
errorDetail += `Could not connect to the FHIRFLARE proxy at ${finalFetchUrl}. Ensure the toolkit server and the local HAPI FHIR server (at http://localhost:8080/fhir) are running.`;
|
||||
} else {
|
||||
errorDetail += `Could not connect to the FHIRFLARE proxy at ${finalFetchUrl} or the proxy could not connect to the target server (${customUrl}). Ensure the toolkit server is running and the target server is accessible.`;
|
||||
}
|
||||
|
||||
console.log(`Final Fetch URL: ${finalFetchUrl}`);
|
||||
|
||||
// 4. Prepare Headers
|
||||
const headers = { 'Accept': 'application/fhir+json, application/fhir+xml;q=0.9' };
|
||||
const isSearch = path.endsWith('_search');
|
||||
if (method === 'POST' || method === 'PUT') {
|
||||
// Determine Content-Type based on body content if possible
|
||||
if (body && body.trim().startsWith('{')) { headers['Content-Type'] = 'application/fhir+json'; }
|
||||
else if (body && body.trim().startsWith('<')) { headers['Content-Type'] = 'application/fhir+xml'; }
|
||||
else if (method === 'POST' && isSearch && body && !body.trim().startsWith('{') && !body.trim().startsWith('<')) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; }
|
||||
else if (body) { headers['Content-Type'] = 'application/fhir+json'; } // Default if body exists but type unknown
|
||||
}
|
||||
|
||||
// Add CSRF token if method requires it AND using local proxy
|
||||
const csrfTokenInput = form.querySelector('input[name="csrf_token"]');
|
||||
const csrfToken = csrfTokenInput ? csrfTokenInput.value : null;
|
||||
if (useLocalHapi && ['POST', 'PUT', 'DELETE'].includes(method) && csrfToken) { // Include DELETE
|
||||
headers['X-CSRFToken'] = csrfToken;
|
||||
console.log("CSRF Token added for local modifying request.");
|
||||
} else if (useLocalHapi && ['POST', 'PUT', 'DELETE'].includes(method) && !csrfToken) {
|
||||
console.warn("CSRF token input not found for local modifying request.");
|
||||
// Potentially alert user or add specific handling
|
||||
}
|
||||
|
||||
// 5. Make the Fetch Request
|
||||
try {
|
||||
const response = await fetch(finalFetchUrl, { method, headers, body }); // Body is undefined for GET/DELETE
|
||||
|
||||
// 6. Process Response
|
||||
responseCard.style.display = 'block';
|
||||
responseStatus.textContent = `${response.status} ${response.statusText}`;
|
||||
responseStatus.className = `badge ${response.ok ? 'bg-success' : 'bg-danger'}`;
|
||||
|
||||
let headerText = '';
|
||||
response.headers.forEach((value, key) => { headerText += `${key}: ${value}\n`; });
|
||||
responseHeaders.textContent = headerText.trim();
|
||||
|
||||
const responseContentType = response.headers.get('content-type') || '';
|
||||
let responseBodyText = await response.text();
|
||||
let displayBody = responseBodyText;
|
||||
|
||||
// Attempt to pretty-print JSON
|
||||
if (responseContentType.includes('json') && responseBodyText.trim()) {
|
||||
try {
|
||||
displayBody = JSON.stringify(JSON.parse(responseBodyText), null, 2);
|
||||
} catch (e) {
|
||||
console.warn("Failed to pretty-print JSON response:", e);
|
||||
displayBody = responseBodyText; // Show raw text if parsing fails
|
||||
}
|
||||
}
|
||||
// Add XML formatting if needed later
|
||||
|
||||
responseBody.textContent = displayBody;
|
||||
|
||||
// Highlight response body if Prism.js is available
|
||||
if (typeof Prism !== 'undefined') {
|
||||
responseBody.className = ''; // Clear previous classes
|
||||
if (responseContentType.includes('json')) {
|
||||
responseBody.classList.add('language-json');
|
||||
} else if (responseContentType.includes('xml')) {
|
||||
responseBody.classList.add('language-xml');
|
||||
} // Add other languages if needed
|
||||
Prism.highlightElement(responseBody);
|
||||
}
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fetch error:', error);
|
||||
responseCard.style.display = 'block';
|
||||
responseStatus.textContent = `Network Error`;
|
||||
responseStatus.className = 'badge bg-danger';
|
||||
responseHeaders.textContent = 'N/A';
|
||||
responseBody.textContent = `Error: ${error.message}\n\nCould not connect to the FHIRFLARE proxy at ${finalFetchUrl}. Ensure the toolkit server is running.`;
|
||||
} finally {
|
||||
sendButton.disabled = false; sendButton.textContent = 'Send Request';
|
||||
}
|
||||
}); // End sendButton listener
|
||||
} else {
|
||||
console.error("Cannot attach listener: Send Request Button, FHIR Path input, or Form element not found.");
|
||||
}
|
||||
responseBody.textContent = errorDetail;
|
||||
} finally {
|
||||
sendButton.disabled = false; sendButton.textContent = 'Send Request';
|
||||
}
|
||||
}); // End sendButton listener
|
||||
|
||||
}); // End DOMContentLoaded Listener
|
||||
</script>
|
||||
|
Loading…
x
Reference in New Issue
Block a user