mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-06-14 12:10:03 +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
|
import shutil
|
||||||
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, Response, current_app, session, send_file, make_response
|
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_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_migrate import Migrate
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from flask_wtf.csrf import CSRFProtect
|
from flask_wtf.csrf import CSRFProtect
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
@ -83,6 +84,7 @@ except Exception as e:
|
|||||||
|
|
||||||
db = SQLAlchemy(app)
|
db = SQLAlchemy(app)
|
||||||
csrf = CSRFProtect(app)
|
csrf = CSRFProtect(app)
|
||||||
|
migrate = Migrate(app, db)
|
||||||
|
|
||||||
# Register Blueprint
|
# Register Blueprint
|
||||||
app.register_blueprint(services_bp, url_prefix='/api')
|
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)
|
complies_with_profiles = db.Column(db.JSON, nullable=True)
|
||||||
imposed_profiles = db.Column(db.JSON, nullable=True)
|
imposed_profiles = db.Column(db.JSON, nullable=True)
|
||||||
optional_usage_elements = 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'),)
|
__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():
|
def check_api_key():
|
||||||
api_key = request.headers.get('X-API-Key')
|
api_key = request.headers.get('X-API-Key')
|
||||||
if not api_key and request.is_json:
|
if not api_key and request.is_json:
|
||||||
@ -256,9 +266,10 @@ def push_igs():
|
|||||||
|
|
||||||
@app.route('/process-igs', methods=['POST'])
|
@app.route('/process-igs', methods=['POST'])
|
||||||
def process_ig():
|
def process_ig():
|
||||||
form = FlaskForm()
|
form = FlaskForm() # Assuming a basic FlaskForm for CSRF protection
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
filename = request.form.get('filename')
|
filename = request.form.get('filename')
|
||||||
|
# --- Keep existing filename and path validation ---
|
||||||
if not filename or not filename.endswith('.tgz'):
|
if not filename or not filename.endswith('.tgz'):
|
||||||
flash("Invalid package file selected.", "error")
|
flash("Invalid package file selected.", "error")
|
||||||
return redirect(url_for('view_igs'))
|
return redirect(url_for('view_igs'))
|
||||||
@ -266,57 +277,68 @@ def process_ig():
|
|||||||
if not os.path.exists(tgz_path):
|
if not os.path.exists(tgz_path):
|
||||||
flash(f"Package file not found: {filename}", "error")
|
flash(f"Package file not found: {filename}", "error")
|
||||||
return redirect(url_for('view_igs'))
|
return redirect(url_for('view_igs'))
|
||||||
|
|
||||||
name, version = services.parse_package_filename(filename)
|
name, version = services.parse_package_filename(filename)
|
||||||
if not name:
|
if not name: # Add fallback naming if parse fails
|
||||||
name = filename[:-4].replace('_', '.')
|
name = filename[:-4].replace('_', '.') # Basic guess
|
||||||
version = 'unknown'
|
version = 'unknown'
|
||||||
logger.warning(f"Using fallback naming for {filename} -> {name}#{version}")
|
logger.warning(f"Using fallback naming for {filename} -> {name}#{version}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info(f"Starting processing for {name}#{version} from file {filename}")
|
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)
|
package_info = services.process_package_file(tgz_path)
|
||||||
|
|
||||||
if package_info.get('errors'):
|
if package_info.get('errors'):
|
||||||
flash(f"Processing completed with errors for {name}#{version}: {', '.join(package_info['errors'])}", "warning")
|
flash(f"Processing completed with errors for {name}#{version}: {', '.join(package_info['errors'])}", "warning")
|
||||||
|
|
||||||
|
# (Keep existing optional_usage_dict logic)
|
||||||
optional_usage_dict = {
|
optional_usage_dict = {
|
||||||
info['name']: True
|
info['name']: True
|
||||||
for info in package_info.get('resource_types_info', [])
|
for info in package_info.get('resource_types_info', [])
|
||||||
if info.get('optional_usage')
|
if info.get('optional_usage')
|
||||||
}
|
}
|
||||||
logger.debug(f"Optional usage elements identified: {optional_usage_dict}")
|
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()
|
existing_ig = ProcessedIg.query.filter_by(package_name=name, version=version).first()
|
||||||
|
|
||||||
if existing_ig:
|
if existing_ig:
|
||||||
logger.info(f"Updating existing processed record for {name}#{version}")
|
logger.info(f"Updating existing processed record for {name}#{version}")
|
||||||
existing_ig.processed_date = datetime.datetime.now(tz=datetime.timezone.utc)
|
processed_ig = existing_ig
|
||||||
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
|
|
||||||
else:
|
else:
|
||||||
logger.info(f"Creating new processed record for {name}#{version}")
|
logger.info(f"Creating new processed record for {name}#{version}")
|
||||||
processed_ig = ProcessedIg(
|
processed_ig = ProcessedIg(package_name=name, version=version)
|
||||||
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
|
|
||||||
)
|
|
||||||
db.session.add(processed_ig)
|
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")
|
flash(f"Successfully processed {name}#{version}!", "success")
|
||||||
|
|
||||||
except Exception as e:
|
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)
|
logger.error(f"Error processing IG {filename}: {str(e)}", exc_info=True)
|
||||||
flash(f"Error processing IG '{filename}': {str(e)}", "error")
|
flash(f"Error processing IG '{filename}': {str(e)}", "error")
|
||||||
else:
|
else:
|
||||||
|
# Handle CSRF or other form validation errors
|
||||||
logger.warning(f"Form validation failed for process-igs: {form.errors}")
|
logger.warning(f"Form validation failed for process-igs: {form.errors}")
|
||||||
flash("CSRF token missing or invalid, or other form error.", "error")
|
flash("CSRF token missing or invalid, or other form error.", "error")
|
||||||
|
|
||||||
return redirect(url_for('view_igs'))
|
return redirect(url_for('view_igs'))
|
||||||
|
|
||||||
|
# --- End of /process-igs Function ---
|
||||||
|
|
||||||
@app.route('/delete-ig', methods=['POST'])
|
@app.route('/delete-ig', methods=['POST'])
|
||||||
def delete_ig():
|
def delete_ig():
|
||||||
form = FlaskForm()
|
form = FlaskForm()
|
||||||
@ -422,13 +444,12 @@ def view_ig(processed_ig_id):
|
|||||||
config=current_app.config)
|
config=current_app.config)
|
||||||
|
|
||||||
#---------------------------------------------------------------------------------------OLD backup-----------------------------------
|
#---------------------------------------------------------------------------------------OLD backup-----------------------------------
|
||||||
# @app.route('/get-structure')
|
#@app.route('/get-structure')
|
||||||
# def get_structure():
|
# def get_structure():
|
||||||
# package_name = request.args.get('package_name')
|
# package_name = request.args.get('package_name')
|
||||||
# package_version = request.args.get('package_version')
|
# package_version = request.args.get('package_version')
|
||||||
# resource_type = request.args.get('resource_type')
|
# resource_type = request.args.get('resource_type') # This is the StructureDefinition ID/Name
|
||||||
# # Keep view parameter for potential future use or caching, though not used directly in this revised logic
|
# view = request.args.get('view', 'snapshot') # Keep for potential future use
|
||||||
# view = request.args.get('view', 'snapshot') # Default to snapshot view processing
|
|
||||||
|
|
||||||
# if not all([package_name, package_version, resource_type]):
|
# 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)
|
# 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.")
|
# logger.error("FHIR_PACKAGES_DIR not configured.")
|
||||||
# return jsonify({"error": "Server configuration error: Package directory not set."}), 500
|
# 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_filename = services.construct_tgz_filename(package_name, package_version)
|
||||||
# tgz_path = os.path.join(packages_dir, tgz_filename)
|
# 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
|
# sd_data = None
|
||||||
|
# search_params_data = [] # Initialize search params list
|
||||||
# fallback_used = False
|
# fallback_used = False
|
||||||
# source_package_id = f"{package_name}#{package_version}"
|
# 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}")
|
# logger.debug(f"Attempting to find SD for '{resource_type}' in {tgz_filename}")
|
||||||
|
|
||||||
# # --- Fetch SD Data (Keep existing logic including fallback) ---
|
# # --- Fetch SD Data (Primary Package) ---
|
||||||
# if os.path.exists(tgz_path):
|
# primary_package_exists = os.path.exists(tgz_path)
|
||||||
|
# core_package_exists = os.path.exists(core_tgz_path)
|
||||||
|
|
||||||
|
# if primary_package_exists:
|
||||||
# try:
|
# try:
|
||||||
# sd_data, _ = services.find_and_extract_sd(tgz_path, resource_type)
|
# 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:
|
# except Exception as e:
|
||||||
# logger.error(f"Unexpected error extracting SD '{resource_type}' from {tgz_path}: {e}", exc_info=True)
|
# logger.error(f"Unexpected error extracting SD '{resource_type}' from primary package {tgz_path}: {e}", exc_info=True)
|
||||||
# return jsonify({"error": f"Unexpected error reading StructureDefinition: {str(e)}"}), 500
|
# sd_data = None # Ensure sd_data is None if extraction failed
|
||||||
# else:
|
|
||||||
# logger.warning(f"Package file not found: {tgz_path}")
|
# # --- Fallback SD Check (if primary failed or file didn't exist) ---
|
||||||
# # Try fallback... (keep existing fallback logic)
|
|
||||||
# if sd_data is None:
|
# 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}.")
|
# logger.info(f"SD for '{resource_type}' not found or failed to load from {source_package_id}. Attempting fallback to {services.CANONICAL_PACKAGE_ID}.")
|
||||||
# core_package_name, core_package_version = services.CANONICAL_PACKAGE
|
# if not core_package_exists:
|
||||||
# core_tgz_filename = services.construct_tgz_filename(core_package_name, core_package_version)
|
# logger.error(f"Core package {services.CANONICAL_PACKAGE_ID} not found locally at {core_tgz_path}.")
|
||||||
# core_tgz_path = os.path.join(packages_dir, core_tgz_filename)
|
# 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."
|
||||||
# if not os.path.exists(core_tgz_path):
|
# return jsonify({"error": error_message}), 500 if primary_package_exists else 404
|
||||||
# # 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:
|
# try:
|
||||||
# sd_data, _ = services.find_and_extract_sd(core_tgz_path, resource_type)
|
# sd_data, _ = services.find_and_extract_sd(core_tgz_path, resource_type)
|
||||||
# if sd_data is not None:
|
# if sd_data is not None:
|
||||||
# fallback_used = True
|
# fallback_used = True
|
||||||
# source_package_id = services.CANONICAL_PACKAGE_ID
|
# source_package_id = services.CANONICAL_PACKAGE_ID
|
||||||
# logger.info(f"Found SD for '{resource_type}' in fallback package {source_package_id}.")
|
# base_resource_type_for_sp = sd_data.get('type') # Store base type from fallback SD
|
||||||
# # Add error handling as before...
|
# 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:
|
# except Exception as e:
|
||||||
# logger.error(f"Unexpected error extracting SD '{resource_type}' from fallback {core_tgz_path}: {e}", exc_info=True)
|
# 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
|
# 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:
|
# 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
|
# return jsonify({"error": f"StructureDefinition for '{resource_type}' not found."}), 404
|
||||||
|
|
||||||
# # --- *** START Backend Modification *** ---
|
# # --- Fetch Search Parameters (Primary Package First) ---
|
||||||
# # Extract snapshot and differential elements
|
# 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', [])
|
# snapshot_elements = sd_data.get('snapshot', {}).get('element', [])
|
||||||
# differential_elements = sd_data.get('differential', {}).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')}
|
# 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.")
|
# logger.debug(f"Found {len(differential_ids)} unique IDs in differential.")
|
||||||
|
|
||||||
# enriched_elements = []
|
# enriched_elements = []
|
||||||
# if snapshot_elements:
|
# if snapshot_elements:
|
||||||
# logger.debug(f"Processing {len(snapshot_elements)} snapshot elements to add isInDifferential flag.")
|
# logger.debug(f"Processing {len(snapshot_elements)} snapshot elements to add isInDifferential flag.")
|
||||||
# for element in snapshot_elements:
|
# for element in snapshot_elements:
|
||||||
# element_id = element.get('id')
|
# element_id = element.get('id')
|
||||||
# # Add the isInDifferential flag
|
|
||||||
# element['isInDifferential'] = bool(element_id and element_id in differential_ids)
|
# element['isInDifferential'] = bool(element_id and element_id in differential_ids)
|
||||||
# enriched_elements.append(element)
|
# 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]
|
# enriched_elements = [services.remove_narrative(el) for el in enriched_elements]
|
||||||
# else:
|
# 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.")
|
# 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 (Existing Logic - slightly refined key lookup) ---
|
||||||
|
|
||||||
# # Retrieve must_support_paths from DB (keep existing logic)
|
|
||||||
# must_support_paths = []
|
# must_support_paths = []
|
||||||
# processed_ig = ProcessedIg.query.filter_by(package_name=package_name, version=package_version).first()
|
# processed_ig = ProcessedIg.query.filter_by(package_name=package_name, version=package_version).first()
|
||||||
# if processed_ig and processed_ig.must_support_elements:
|
# if processed_ig and processed_ig.must_support_elements:
|
||||||
# # Use the profile ID (which is likely the resource_type for profiles) as the key
|
# ms_elements_dict = processed_ig.must_support_elements
|
||||||
# must_support_paths = processed_ig.must_support_elements.get(resource_type, [])
|
# if resource_type in ms_elements_dict:
|
||||||
# logger.debug(f"Retrieved {len(must_support_paths)} Must Support paths for '{resource_type}' from processed IG DB record.")
|
# 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:
|
# 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 final response ---
|
||||||
# # Construct the response
|
|
||||||
# response_data = {
|
# response_data = {
|
||||||
# 'elements': enriched_elements, # Return the processed list
|
# 'elements': enriched_elements,
|
||||||
# 'must_support_paths': must_support_paths,
|
# 'must_support_paths': must_support_paths,
|
||||||
|
# 'search_parameters': search_params_data, # Include potentially populated list
|
||||||
# 'fallback_used': fallback_used,
|
# 'fallback_used': fallback_used,
|
||||||
# 'source_package': source_package_id
|
# 'source_package': source_package_id
|
||||||
# # Removed raw structure_definition to reduce payload size, unless needed elsewhere
|
|
||||||
# }
|
# }
|
||||||
|
|
||||||
# # Use Response object for consistent JSON formatting
|
# # Use Response object for consistent JSON formatting and smaller payload
|
||||||
# return Response(json.dumps(response_data, indent=2), mimetype='application/json') # Use indent=2 for readability if debugging
|
# return Response(json.dumps(response_data, indent=None, separators=(',', ':')), mimetype='application/json')
|
||||||
#-----------------------------------------------------------------------------------------------------------------------------------
|
#-----------------------------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# --- Full /get-structure Function ---
|
||||||
@app.route('/get-structure')
|
@app.route('/get-structure')
|
||||||
def get_structure():
|
def get_structure():
|
||||||
package_name = request.args.get('package_name')
|
package_name = request.args.get('package_name')
|
||||||
package_version = request.args.get('package_version')
|
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
|
view = request.args.get('view', 'snapshot') # Keep for potential future use
|
||||||
|
|
||||||
|
# --- Parameter Validation ---
|
||||||
if not all([package_name, package_version, resource_type]):
|
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)
|
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
|
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')
|
packages_dir = current_app.config.get('FHIR_PACKAGES_DIR')
|
||||||
if not packages_dir:
|
if not packages_dir:
|
||||||
logger.error("FHIR_PACKAGES_DIR not configured.")
|
logger.error("FHIR_PACKAGES_DIR not configured.")
|
||||||
return jsonify({"error": "Server configuration error: Package directory not set."}), 500
|
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_filename = services.construct_tgz_filename(package_name, package_version)
|
||||||
tgz_path = os.path.join(packages_dir, tgz_filename)
|
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_package_name, core_package_version = services.CANONICAL_PACKAGE
|
||||||
core_tgz_filename = services.construct_tgz_filename(core_package_name, core_package_version)
|
core_tgz_filename = services.construct_tgz_filename(core_package_name, core_package_version)
|
||||||
core_tgz_path = os.path.join(packages_dir, core_tgz_filename)
|
core_tgz_path = os.path.join(packages_dir, core_tgz_filename)
|
||||||
@ -572,8 +629,10 @@ def get_structure():
|
|||||||
|
|
||||||
if primary_package_exists:
|
if primary_package_exists:
|
||||||
try:
|
try:
|
||||||
|
# Assuming find_and_extract_sd handles narrative removal
|
||||||
sd_data, _ = services.find_and_extract_sd(tgz_path, resource_type)
|
sd_data, _ = services.find_and_extract_sd(tgz_path, resource_type)
|
||||||
if sd_data:
|
if sd_data:
|
||||||
|
# Determine the base resource type from the fetched SD
|
||||||
base_resource_type_for_sp = sd_data.get('type')
|
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}'")
|
logger.debug(f"Determined base resource type '{base_resource_type_for_sp}' from primary SD '{resource_type}'")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -601,11 +660,11 @@ def get_structure():
|
|||||||
|
|
||||||
# --- Check if SD data was ultimately found ---
|
# --- Check if SD data was ultimately found ---
|
||||||
if not sd_data:
|
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.")
|
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
|
return jsonify({"error": f"StructureDefinition for '{resource_type}' not found."}), 404
|
||||||
|
|
||||||
# --- Fetch Search Parameters (Primary Package First) ---
|
# --- 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:
|
if base_resource_type_for_sp and primary_package_exists:
|
||||||
try:
|
try:
|
||||||
logger.info(f"Fetching SearchParameters for base type '{base_resource_type_for_sp}' from primary package {tgz_path}")
|
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:
|
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.")
|
logger.warning(f"Core package {core_tgz_path} not found, cannot perform fallback search for SearchParameters.")
|
||||||
|
|
||||||
|
# --- Prepare Snapshot/Differential Elements ---
|
||||||
# --- Prepare Snapshot/Differential Elements (Existing Logic) ---
|
|
||||||
snapshot_elements = sd_data.get('snapshot', {}).get('element', [])
|
snapshot_elements = sd_data.get('snapshot', {}).get('element', [])
|
||||||
differential_elements = sd_data.get('differential', {}).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')}
|
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.")
|
logger.debug(f"Found {len(differential_ids)} unique IDs in differential.")
|
||||||
|
|
||||||
enriched_elements = []
|
enriched_elements = []
|
||||||
if snapshot_elements:
|
if snapshot_elements:
|
||||||
logger.debug(f"Processing {len(snapshot_elements)} snapshot elements to add isInDifferential flag.")
|
logger.debug(f"Processing {len(snapshot_elements)} snapshot elements to add isInDifferential flag.")
|
||||||
for element in snapshot_elements:
|
for element in snapshot_elements:
|
||||||
element_id = element.get('id')
|
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)
|
element['isInDifferential'] = bool(element_id and element_id in differential_ids)
|
||||||
enriched_elements.append(element)
|
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]
|
enriched_elements = [services.remove_narrative(el) for el in enriched_elements]
|
||||||
else:
|
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.")
|
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 = []
|
must_support_paths = []
|
||||||
processed_ig = ProcessedIg.query.filter_by(package_name=package_name, version=package_version).first()
|
# Query DB once for the ProcessedIg record
|
||||||
if processed_ig and processed_ig.must_support_elements:
|
processed_ig_record = ProcessedIg.query.filter_by(package_name=package_name, version=package_version).first()
|
||||||
ms_elements_dict = processed_ig.must_support_elements
|
if processed_ig_record and processed_ig_record.must_support_elements:
|
||||||
if resource_type in ms_elements_dict:
|
ms_elements_dict = processed_ig_record.must_support_elements
|
||||||
must_support_paths = ms_elements_dict[resource_type]
|
# Try getting MS paths using the profile ID/name first, fallback to base type
|
||||||
logger.debug(f"Retrieved {len(must_support_paths)} Must Support paths using profile key '{resource_type}' from processed IG DB record.")
|
must_support_paths = ms_elements_dict.get(resource_type, [])
|
||||||
elif base_resource_type_for_sp and base_resource_type_for_sp in ms_elements_dict:
|
if not must_support_paths and base_resource_type_for_sp:
|
||||||
must_support_paths = ms_elements_dict[base_resource_type_for_sp]
|
must_support_paths = ms_elements_dict.get(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.")
|
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:
|
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:
|
else:
|
||||||
logger.debug(f"No processed IG record or no must_support_elements found in DB for {package_name}#{package_version}")
|
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 ---
|
# --- Construct the final response ---
|
||||||
response_data = {
|
response_data = {
|
||||||
'elements': enriched_elements,
|
'elements': enriched_elements,
|
||||||
'must_support_paths': must_support_paths,
|
'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,
|
'fallback_used': fallback_used,
|
||||||
'source_package': source_package_id
|
'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
|
# 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')
|
return Response(json.dumps(response_data, indent=None, separators=(',', ':')), mimetype='application/json')
|
||||||
|
|
||||||
|
# --- End of /get-structure Function ---
|
||||||
|
|
||||||
@app.route('/get-example')
|
@app.route('/get-example')
|
||||||
def get_example():
|
def get_example():
|
||||||
package_name = request.args.get('package_name')
|
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
|
#FileLock
|
||||||
#Thu Apr 24 13:26:14 UTC 2025
|
#Fri Apr 25 13:11:59 UTC 2025
|
||||||
server=172.19.0.2\:33797
|
server=172.19.0.2\:35493
|
||||||
hostName=3e6e009eccb3
|
hostName=499bb2429005
|
||||||
method=file
|
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
|
Flask-WTF==1.2.1
|
||||||
WTForms==3.1.2
|
WTForms==3.1.2
|
||||||
Pytest
|
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) {
|
function renderSearchParametersTable(searchParamsData) {
|
||||||
|
// Find the table body element using its ID
|
||||||
const tableBody = document.getElementById('searchparams-table')?.querySelector('tbody');
|
const tableBody = document.getElementById('searchparams-table')?.querySelector('tbody');
|
||||||
|
|
||||||
|
// Check if the table body element exists
|
||||||
if (!tableBody) {
|
if (!tableBody) {
|
||||||
console.error('Search Params table body not found');
|
console.error('Search Params table body element (#searchparams-table > tbody) not found');
|
||||||
return;
|
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);
|
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) {
|
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 row = tableBody.insertRow();
|
||||||
const cell = row.insertCell();
|
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.textContent = 'No relevant search parameters found for this resource type.';
|
||||||
cell.style.textAlign = 'center';
|
cell.style.textAlign = 'center';
|
||||||
cell.classList.add('text-muted', 'fst-italic');
|
cell.classList.add('text-muted', 'fst-italic'); // Style the message
|
||||||
return;
|
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 || ''));
|
searchParamsData.sort((a, b) => (a.code || '').localeCompare(b.code || ''));
|
||||||
|
|
||||||
|
// Iterate through each search parameter object in the sorted array
|
||||||
searchParamsData.forEach((param, index) => {
|
searchParamsData.forEach((param, index) => {
|
||||||
// *** ADDED LOG: Log each parameter being processed ***
|
console.log(`[renderSearchParametersTable] Processing param ${index + 1}:`, param); // Log each parameter
|
||||||
console.log(`[renderSearchParametersTable] Processing param ${index + 1}:`, param);
|
|
||||||
// ***************************************************
|
|
||||||
|
|
||||||
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();
|
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>`;
|
nameCell.innerHTML = `<code>${escapeHtmlAttr(param.code || 'N/A')}</code>`;
|
||||||
|
|
||||||
// Type
|
// --- Column 2: Parameter Type ---
|
||||||
row.insertCell().textContent = param.type || 'N/A';
|
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();
|
const conformanceCell = row.insertCell();
|
||||||
conformanceCell.textContent = param.conformance || 'N/A'; // Show backend placeholder
|
// Default to 'Optional' if conformance isn't specified in the data
|
||||||
// Add highlighting later if backend provides is_mandatory
|
const conformanceValue = param.conformance || 'Optional';
|
||||||
// if (param.is_mandatory) { ... }
|
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();
|
const descriptionCell = row.insertCell();
|
||||||
|
// Use escapeHtmlAttr for the description text
|
||||||
descriptionCell.innerHTML = `<small>${escapeHtmlAttr(param.description || 'No description available.')}</small>`;
|
descriptionCell.innerHTML = `<small>${escapeHtmlAttr(param.description || 'No description available.')}</small>`;
|
||||||
|
// If a FHIRPath expression exists, add it as a tooltip
|
||||||
if (param.expression) {
|
if (param.expression) {
|
||||||
descriptionCell.title = `FHIRPath: ${param.expression}`;
|
descriptionCell.setAttribute('title', `FHIRPath: ${param.expression}`);
|
||||||
descriptionCell.setAttribute('data-bs-toggle', 'tooltip');
|
descriptionCell.setAttribute('data-bs-toggle', 'tooltip');
|
||||||
descriptionCell.setAttribute('data-bs-placement', 'top');
|
descriptionCell.setAttribute('data-bs-placement', 'top');
|
||||||
// Ensure tooltip is initialized for dynamically added elements
|
// Initialize or re-initialize the tooltip
|
||||||
const existingTooltip = bootstrap.Tooltip.getInstance(descriptionCell);
|
const existingDescTooltip = bootstrap.Tooltip.getInstance(descriptionCell);
|
||||||
if (existingTooltip) {
|
if (existingDescTooltip) {
|
||||||
existingTooltip.dispose(); // Remove old instance if any
|
existingDescTooltip.dispose();
|
||||||
}
|
}
|
||||||
new bootstrap.Tooltip(descriptionCell);
|
new bootstrap.Tooltip(descriptionCell);
|
||||||
}
|
}
|
||||||
});
|
}); // End forEach loop over searchParamsData
|
||||||
console.log('[renderSearchParametersTable] Finished rendering parameters.'); // Added Log
|
|
||||||
} // End renderSearchParametersTable
|
console.log('[renderSearchParametersTable] Finished rendering parameters.');
|
||||||
|
} // End renderSearchParametersTable function
|
||||||
|
|
||||||
|
|
||||||
// --- DOMContentLoaded Listener ---
|
// --- DOMContentLoaded Listener ---
|
||||||
|
@ -88,13 +88,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
console.log("DOMContentLoaded event fired."); // Log when the listener starts
|
console.log("DOMContentLoaded event fired.");
|
||||||
|
|
||||||
// --- Get DOM Elements & Check Existence ---
|
// --- Get DOM Elements & Check Existence ---
|
||||||
const form = document.getElementById('fhirRequestForm');
|
const form = document.getElementById('fhirRequestForm');
|
||||||
const sendButton = document.getElementById('sendRequest');
|
const sendButton = document.getElementById('sendRequest');
|
||||||
const fhirPathInput = document.getElementById('fhirPath'); // Renamed for clarity
|
const fhirPathInput = document.getElementById('fhirPath');
|
||||||
const requestBodyInput = document.getElementById('requestBody'); // Renamed for clarity
|
const requestBodyInput = document.getElementById('requestBody');
|
||||||
const requestBodyGroup = document.getElementById('requestBodyGroup');
|
const requestBodyGroup = document.getElementById('requestBodyGroup');
|
||||||
const jsonError = document.getElementById('jsonError');
|
const jsonError = document.getElementById('jsonError');
|
||||||
const responseCard = document.getElementById('responseCard');
|
const responseCard = document.getElementById('responseCard');
|
||||||
@ -104,303 +104,298 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const methodRadios = document.getElementsByName('method');
|
const methodRadios = document.getElementsByName('method');
|
||||||
const toggleServerButton = document.getElementById('toggleServer');
|
const toggleServerButton = document.getElementById('toggleServer');
|
||||||
const toggleLabel = document.getElementById('toggleLabel');
|
const toggleLabel = document.getElementById('toggleLabel');
|
||||||
const fhirServerUrlInput = document.getElementById('fhirServerUrl'); // Renamed for clarity
|
const fhirServerUrlInput = document.getElementById('fhirServerUrl');
|
||||||
const copyRequestBodyButton = document.getElementById('copyRequestBody');
|
const copyRequestBodyButton = document.getElementById('copyRequestBody');
|
||||||
const copyResponseHeadersButton = document.getElementById('copyResponseHeaders');
|
const copyResponseHeadersButton = document.getElementById('copyResponseHeaders');
|
||||||
const copyResponseBodyButton = document.getElementById('copyResponseBody');
|
const copyResponseBodyButton = document.getElementById('copyResponseBody');
|
||||||
|
|
||||||
// --- Element Existence Check ---
|
// Basic check for critical elements
|
||||||
let elementsReady = true;
|
if (!form || !sendButton || !fhirPathInput || !responseCard || !toggleServerButton || !fhirServerUrlInput || !responseStatus || !responseHeaders || !responseBody || !toggleLabel) {
|
||||||
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) {
|
|
||||||
console.error("One or more critical UI elements could not be found. Script execution halted.");
|
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
|
return; // Stop script execution
|
||||||
}
|
}
|
||||||
console.log("All critical elements checked/found.");
|
console.log("All critical elements checked/found.");
|
||||||
|
|
||||||
// --- State Variable ---
|
// --- State Variable ---
|
||||||
|
// Default assumes standalone, will be forced otherwise by appMode check below
|
||||||
let useLocalHapi = true;
|
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() {
|
// --- DEFINE HELPER FUNCTIONS ---
|
||||||
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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validates request body, returns null on error, otherwise returns body string or empty string
|
// Validates request body, returns null on error, otherwise returns body string or empty string
|
||||||
function validateRequestBody(method, path) {
|
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();
|
const bodyValue = requestBodyInput.value.trim();
|
||||||
if (!bodyValue) {
|
requestBodyInput.classList.remove('is-invalid'); // Reset validation
|
||||||
requestBodyInput.classList.remove('is-invalid');
|
jsonError.style.display = 'none';
|
||||||
jsonError.style.display = 'none';
|
|
||||||
return ''; // Empty body is valid for POST/PUT
|
if (!bodyValue) return ''; // Empty body is valid for POST/PUT
|
||||||
}
|
|
||||||
const isSearch = path && path.endsWith('_search');
|
const isSearch = path && path.endsWith('_search');
|
||||||
const isJson = bodyValue.startsWith('{') || bodyValue.startsWith('[');
|
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
|
if (method === 'POST' && isSearch && isForm) { // POST Search with form params
|
||||||
requestBodyInput.classList.remove('is-invalid');
|
|
||||||
jsonError.style.display = 'none';
|
|
||||||
return bodyValue;
|
return bodyValue;
|
||||||
} else if (method === 'POST' || method === 'PUT') { // Other POST/PUT expect JSON
|
} else if (method === 'POST' || method === 'PUT') { // Other POST/PUT expect JSON/XML
|
||||||
try {
|
if (isJson) {
|
||||||
JSON.parse(bodyValue);
|
try { JSON.parse(bodyValue); return bodyValue; }
|
||||||
requestBodyInput.classList.remove('is-invalid');
|
catch (e) { jsonError.textContent = `Invalid JSON: ${e.message}`; }
|
||||||
jsonError.style.display = 'none';
|
} else if (isXml) {
|
||||||
return bodyValue; // Return the valid JSON string
|
// Basic XML check is difficult in JS, accept it for now
|
||||||
} catch (e) {
|
// Backend or target server will validate fully
|
||||||
requestBodyInput.classList.add('is-invalid');
|
return bodyValue;
|
||||||
jsonError.textContent = `Invalid JSON: ${e.message}`;
|
} else { // Neither JSON nor XML, and not a POST search form
|
||||||
jsonError.style.display = 'block';
|
jsonError.textContent = 'Request body must be valid JSON or XML for PUT/POST (unless using POST _search with form parameters).';
|
||||||
return null; // Indicate validation error
|
|
||||||
}
|
}
|
||||||
|
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
|
return undefined; // Indicate no body should be sent for GET/DELETE
|
||||||
requestBodyInput.classList.remove('is-invalid');
|
|
||||||
jsonError.style.display = 'none';
|
|
||||||
return undefined; // Indicate no body should be sent
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleans path for proxying
|
// Cleans path for proxying
|
||||||
function cleanFhirPath(path) {
|
function cleanFhirPath(path) {
|
||||||
if (!path) return '';
|
if (!path) return '';
|
||||||
// Remove optional leading 'r4/' or 'fhir/', then trim slashes
|
// Remove optional leading 'r4/' or 'fhir/', then trim slashes
|
||||||
const cleaned = path.replace(/^(r4\/|fhir\/)*/i, '').replace(/^\/+|\/+$/g, '');
|
return path.replace(/^(r4\/|fhir\/)*/i, '').replace(/^\/+|\/+$/g, '');
|
||||||
return cleaned;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copies text to clipboard
|
// Copies text to clipboard
|
||||||
async function copyToClipboard(text, button) {
|
async function copyToClipboard(text, button) {
|
||||||
if (text === null || text === undefined || !button) return;
|
if (text === null || text === undefined || !button || !navigator.clipboard) return;
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(String(text)); // Ensure text is string
|
await navigator.clipboard.writeText(String(text));
|
||||||
const originalIcon = button.innerHTML;
|
const originalIcon = button.innerHTML;
|
||||||
button.innerHTML = '<i class="bi bi-check-lg"></i>';
|
button.innerHTML = '<i class="bi bi-check-lg"></i> Copied!';
|
||||||
setTimeout(() => { if(button.isConnected) button.innerHTML = originalIcon; }, 2000);
|
setTimeout(() => { if(button.isConnected) button.innerHTML = originalIcon; }, 1500);
|
||||||
} catch (err) { console.error('Copy failed:', err); alert('Failed to copy to clipboard'); }
|
} 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 ---
|
// --- INITIAL SETUP & MODE CHECK ---
|
||||||
const appMode = '{{ app_mode | default("standalone") | lower }}';
|
updateServerToggleUI(); // Set initial UI based on detected mode and default state
|
||||||
console.log('App Mode:', appMode);
|
updateRequestBodyVisibility(); // Set initial visibility based on default method (GET)
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
// --- ATTACH EVENT LISTENERS ---
|
// --- 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 (copyRequestBodyButton && requestBodyInput) { copyRequestBodyButton.addEventListener('click', () => copyToClipboard(requestBodyInput.value, copyRequestBodyButton)); }
|
||||||
if (copyResponseHeadersButton && responseHeaders) { copyResponseHeadersButton.addEventListener('click', () => copyToClipboard(responseHeaders.textContent, copyResponseHeadersButton)); }
|
if (copyResponseHeadersButton && responseHeaders) { copyResponseHeadersButton.addEventListener('click', () => copyToClipboard(responseHeaders.textContent, copyResponseHeadersButton)); }
|
||||||
if (copyResponseBodyButton && responseBody) { copyResponseBodyButton.addEventListener('click', () => copyToClipboard(responseBody.textContent, copyResponseBodyButton)); }
|
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) ---
|
// --- Send Request Button Listener ---
|
||||||
if (sendButton && fhirPathInput && form) {
|
sendButton.addEventListener('click', async function() {
|
||||||
sendButton.addEventListener('click', async function() {
|
console.log("Send Request button clicked.");
|
||||||
console.log("Send Request button clicked.");
|
// --- UI Reset ---
|
||||||
sendButton.disabled = true; sendButton.textContent = 'Sending...';
|
sendButton.disabled = true; sendButton.textContent = 'Sending...';
|
||||||
responseCard.style.display = 'none';
|
responseCard.style.display = 'none';
|
||||||
responseStatus.textContent = ''; responseHeaders.textContent = ''; responseBody.textContent = '';
|
responseStatus.textContent = ''; responseHeaders.textContent = ''; responseBody.textContent = '';
|
||||||
responseStatus.className = 'badge'; // Reset badge class
|
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
|
// --- Get Values ---
|
||||||
const path = fhirPathInput.value.trim();
|
const path = fhirPathInput.value.trim();
|
||||||
const method = document.querySelector('input[name="method"]:checked')?.value;
|
const method = document.querySelector('input[name="method"]:checked')?.value;
|
||||||
let body = undefined; // Default to no body
|
const customUrl = fhirServerUrlInput.value.trim();
|
||||||
|
let body = undefined;
|
||||||
|
|
||||||
if (!path) { alert('Please enter a FHIR Path.'); sendButton.disabled = false; sendButton.textContent = 'Send Request'; return; }
|
// --- Basic Input Validation ---
|
||||||
if (!method) { alert('Please select a Request Type.'); sendButton.disabled = false; sendButton.textContent = 'Send Request'; return; }
|
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)
|
// --- Validate & Get Body (if needed) ---
|
||||||
if (method === 'POST' || method === 'PUT') {
|
if (method === 'POST' || method === 'PUT') {
|
||||||
body = validateRequestBody(method, path);
|
body = validateRequestBody(method, path);
|
||||||
if (body === null) { // null indicates validation error
|
if (body === null) { // null indicates validation error
|
||||||
alert('Request body contains invalid JSON.'); sendButton.disabled = false; sendButton.textContent = 'Send Request'; return;
|
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 is empty string, ensure it's treated as such for fetch
|
||||||
if (body === '') body = '';
|
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
|
responseBody.textContent = displayBody;
|
||||||
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
|
|
||||||
|
|
||||||
if (!useLocalHapi) {
|
// Highlight response body if Prism.js is available
|
||||||
const customUrl = fhirServerUrlInput.value.trim();
|
if (typeof Prism !== 'undefined') {
|
||||||
if (!customUrl) {
|
responseBody.className = 'border p-2 bg-light'; // Reset base classes
|
||||||
alert('Please enter a custom FHIR Server URL when not using Local HAPI.');
|
if (responseContentType.includes('json')) {
|
||||||
fhirServerUrlInput.classList.add('is-invalid');
|
responseBody.classList.add('language-json');
|
||||||
sendButton.disabled = false; sendButton.textContent = 'Send Request';
|
} else if (responseContentType.includes('xml')) {
|
||||||
return;
|
responseBody.classList.add('language-xml');
|
||||||
}
|
} // Add other languages if needed
|
||||||
try {
|
Prism.highlightElement(responseBody);
|
||||||
// Validate custom URL format, though actual reachability is checked by backend proxy
|
}
|
||||||
new URL(customUrl);
|
|
||||||
fhirServerUrlInput.classList.remove('is-invalid');
|
} catch (error) {
|
||||||
// The proxy needs the *full* path including the custom base, it doesn't know it otherwise
|
console.error('Fetch error:', error);
|
||||||
// We modify the path being sent TO THE PROXY
|
responseCard.style.display = 'block';
|
||||||
// Let backend handle the full external URL based on path sent to it
|
responseStatus.textContent = `Network Error`;
|
||||||
// This assumes the proxy handles the full path correctly.
|
responseStatus.className = 'badge bg-danger';
|
||||||
// *Correction*: Proxy expects just the relative path. Send custom URL in header?
|
responseHeaders.textContent = 'N/A';
|
||||||
// Let's stick to the original proxy design: frontend sends relative path to /fhir/,
|
// Provide a more informative error message
|
||||||
// backend needs to know if it's proxying to local or custom.
|
let errorDetail = `Error: ${error.message}\n\n`;
|
||||||
// WE NEED TO TELL THE BACKEND WHICH URL TO PROXY TO!
|
if (useLocalHapi) {
|
||||||
// This requires backend changes. For now, assume proxy ALWAYS goes to localhost:8080.
|
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.`;
|
||||||
// To make custom URL work, the backend proxy needs modification.
|
} else {
|
||||||
// Sticking with current backend: Custom URL cannot be proxied via '/fhir/'.
|
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.`;
|
||||||
// --> 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
responseBody.textContent = errorDetail;
|
||||||
console.log(`Final Fetch URL: ${finalFetchUrl}`);
|
} finally {
|
||||||
|
sendButton.disabled = false; sendButton.textContent = 'Send Request';
|
||||||
// 4. Prepare Headers
|
}
|
||||||
const headers = { 'Accept': 'application/fhir+json, application/fhir+xml;q=0.9' };
|
}); // End sendButton listener
|
||||||
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.");
|
|
||||||
}
|
|
||||||
|
|
||||||
}); // End DOMContentLoaded Listener
|
}); // End DOMContentLoaded Listener
|
||||||
</script>
|
</script>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user