mrs_plugin/services.py (565 lines of code) (raw):
# Copyright (c) 2021, 2025, Oracle and/or its affiliates.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License, version 2.0,
# as published by the Free Software Foundation.
#
# This program is designed to work with certain software (including
# but not limited to OpenSSL) that is licensed under separate terms, as
# designated in a particular file or component or in included license
# documentation. The authors of MySQL hereby grant you an additional
# permission to link the program and your derivative works with the
# separately licensed software that they have either included with
# the program or referenced in the documentation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
# the GNU General Public License, version 2.0, for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
"""Sub-Module for managing MRS services"""
# cSpell:ignore mysqlsh, mrs
from mysqlsh.plugin_manager import plugin_function
import mrs_plugin.lib as lib
from .interactive import resolve_service, resolve_options, resolve_file_path, resolve_overwrite_file, service_query_selection
from pathlib import Path
import os
import shutil
import json
import datetime
import base64
def verify_value_keys(**kwargs):
for key in kwargs["value"].keys():
if key not in ["url_host_id", "url_context_root", "url_protocol", "url_host_name",
"enabled", "comments", "options",
"auth_path", "auth_completed_url", "auth_completed_url_validation",
"auth_completed_page_content", "auth_apps", "metadata",
"in_development", "published", "name"] and key != "delete":
raise Exception(f"Attempting to change an invalid service value.")
def resolve_service_ids(**kwargs):
value = kwargs.get("value")
session = kwargs.get("session")
service_id = kwargs.pop("service_id", None)
url_context_root = kwargs.pop("url_context_root", None)
url_host_name = kwargs.pop("url_host_name", None)
interactive = lib.core.get_interactive_default()
allow_multi_select = kwargs.pop("allow_multi_select", False)
kwargs.pop("url_protocol", None)
kwargs["service_ids"] = []
if service_id is not None:
kwargs["service_ids"] = [service_id]
else:
# Get the right service_id(s) if service_id is not given
if not url_context_root:
# Check if there already is at least one service
rows = lib.core.select(table="service",
cols=["COUNT(*) AS service_count",
"MAX(id) AS id"]
).exec(session).items
if len(rows) == 0 or rows[0]["service_count"] == 0:
Exception("No service available.")
# If there are more services, let the user select one or all
if interactive:
if allow_multi_select:
caption = ("Please select a service index, type "
"'hostname/root_context' or type '*' "
"to select all: ")
else:
caption = ("Please select a service index or type "
"'hostname/root_context'")
services = lib.services.get_services(session=session)
selection = lib.core.prompt_for_list_item(
item_list=services,
prompt_caption=caption,
item_name_property="host_ctx",
given_value=None,
print_list=True,
allow_multi_select=allow_multi_select)
if not selection or selection == "":
raise ValueError("Operation cancelled.")
if allow_multi_select:
kwargs["service_ids"] = [item["id"] for item in selection]
else:
kwargs["service_ids"].append(selection["id"])
else:
# Lookup the service id
res = session.run_sql(
"""
SELECT se.id FROM `mysql_rest_service_metadata`.`service` se
LEFT JOIN `mysql_rest_service_metadata`.url_host h
ON se.url_host_id = h.id
WHERE h.name = ? AND se.url_context_root = ?
""",
[url_host_name if url_host_name else "", url_context_root])
row = res.fetch_one()
if row:
kwargs["service_ids"].append(row.get_field("id"))
if len(kwargs["service_ids"]) == 0:
raise ValueError("The specified service was not found.")
for service_id in kwargs["service_ids"]:
service = lib.services.get_service(
service_id=service_id, session=session)
# Determine changes in the url_context_root for this service
if value is not None and "url_context_root" in value:
url_ctx_root = value["url_context_root"]
if interactive and not url_ctx_root:
url_ctx_root = lib.services.prompt_for_url_context_root(
default=service.get('url_context_root'))
# If the context root has changed, check if the new one is valid
if service["url_context_root"] != url_ctx_root:
if (not url_ctx_root or not url_ctx_root.startswith('/')):
raise ValueError(
"The url_context_root has to start with '/'.")
return kwargs
def resolve_url_context_root(required=False, **kwargs):
url_context_root = kwargs.get("url_context_root")
if url_context_root is None and lib.core.get_interactive_default():
url_context_root = kwargs["url_context_root"] = lib.services.prompt_for_url_context_root(
)
if required and url_context_root is None:
raise Exception("No context path given. Operation cancelled.")
if url_context_root is not None and not url_context_root.startswith('/'):
raise Exception(
f"The url_context_root [{url_context_root}] has to start with '/'.")
return kwargs
def resolve_url_host_name(required=False, **kwargs):
url_host_name = kwargs.get("url_host_name")
if lib.core.get_interactive_default():
if url_host_name is None:
url_host_name = lib.core.prompt(
"Please enter the host name for this service (e.g. "
"None or localhost) [None]: ",
{'defaultValue': 'None'}).strip()
if url_host_name and url_host_name.lower() == 'none':
url_host_name = None
kwargs["url_host_name"] = url_host_name
return kwargs
def resolve_url_protocol(**kwargs):
if kwargs.get("url_protocol") is None:
if lib.core.get_interactive_default():
kwargs["url_protocol"] = lib.services.prompt_for_service_protocol()
else:
kwargs["url_protocol"] = ["HTTP", "HTTPS"]
return kwargs
def resolve_comments(**kwargs):
if lib.core.get_interactive_default():
if kwargs.get("comments") is None:
kwargs["comments"] = lib.core.prompt_for_comments()
return kwargs
def call_update_service(op_text, **kwargs):
with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
kwargs["session"] = session
kwargs = resolve_service_ids(**kwargs)
with lib.core.MrsDbTransaction(session):
lib.services.update_services(**kwargs)
if lib.core.get_interactive_result():
if len(kwargs['service_ids']) == 1:
return f"The service has been {op_text}."
return f"The services have been {op_text}."
return True
return False
def file_name_using_language_convention(name, sdk_language):
if sdk_language == "Python":
return lib.core.convert_to_snake_case(name)
return name
def default_copyright_header(sdk_language):
header = "Copyright (c) 2023, 2025, Oracle and/or its affiliates."
if sdk_language == "TypeScript":
return f"// {header}"
if sdk_language == "Python":
return f"# {header}"
def generate_create_statement(**kwargs):
lib.core.convert_ids_to_binary(["service_id"], kwargs)
lib.core.try_convert_ids_to_binary(["service"], kwargs)
include_database_endpoints = kwargs.get("include_database_endpoints", False)
include_static_endpoints = kwargs.get("include_static_endpoints", False)
include_dynamic_endpoints = kwargs.get("include_dynamic_endpoints", False)
service_query = service_query_selection(**kwargs)
with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
service = resolve_service(session, service_query=service_query)
if service is None:
raise ValueError("The specified service was not found.")
return lib.services.get_service_create_statement(
session, service,
include_database_endpoints, include_static_endpoints, include_dynamic_endpoints)
def store_create_statement(**kwargs):
lib.core.convert_ids_to_binary(["service_id"], kwargs)
lib.core.try_convert_ids_to_binary(["service"], kwargs)
include_database_endpoints = kwargs.get("include_database_endpoints", False)
include_static_endpoints = kwargs.get("include_static_endpoints", False)
include_dynamic_endpoints = kwargs.get("include_dynamic_endpoints", False)
service_query = service_query_selection(**kwargs)
file_path = kwargs.get("file_path")
zip = kwargs.get("zip", False)
with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
service = resolve_service(session, service_query=service_query)
if service is None:
raise ValueError("The specified service was not found.")
lib.services.store_service_create_statement(session, service,
file_path, zip,
include_database_endpoints, include_static_endpoints, include_dynamic_endpoints)
@plugin_function('mrs.add.service', shell=True, cli=True, web=True)
def add_service(**kwargs):
"""Adds a new MRS service
Args:
**kwargs: Additional options
Keyword Args:
url_context_root (str): The context root for this service
url_host_name (str): The host name for this service
enabled (bool): Whether the new service should be enabled
url_protocol (list): The protocols supported by this service
comments (str): Comments about the service
options (dict): Options for the service
auth_path (str): The authentication path
auth_completed_url (str): The redirection URL called after authentication
auth_completed_url_validation (str): The regular expression that validates the
app redirection URL specified by the /login?onCompletionRedirect parameter
auth_completed_page_content (str): The custom page content to use of the
authentication completed page
metadata (dict): Metadata of the service
published (bool): Whether the new service should be published immediately
name (str): The name of the service
session (object): The database session to use.
Returns:
Text confirming the service creation with its id or a dict holding the new service id otherwise
"""
if "options" in kwargs:
kwargs["options"] = lib.core.convert_json(kwargs["options"])
if "metadata" in kwargs:
kwargs["metadata"] = lib.core.convert_json(kwargs["metadata"])
with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
options = kwargs.get("options")
kwargs["session"] = session
# Get url_context_root
kwargs = resolve_url_context_root(required=True, **kwargs)
url_context_root = kwargs["url_context_root"]
# Get url_host_name
kwargs = resolve_url_host_name(required=False, **kwargs)
url_host_name = kwargs["url_host_name"]
if url_host_name is None:
url_host_name = ""
# Get url_protocol
kwargs = resolve_url_protocol(**kwargs)
kwargs = resolve_comments(**kwargs)
defaultOptions = {
"headers": {
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS"
},
"http": {
"allowedOrigin": "auto"
},
"logging": {
"request": {
"headers": True,
"body": True
},
"response": {
"headers": True,
"body": True
},
"exceptions": True
},
"returnInternalErrorDetails": True,
"includeLinksInResults": False
}
options = resolve_options(options, defaultOptions)
with lib.core.MrsDbTransaction(session):
service_id = lib.services.add_service(session, url_host_name, {
"url_context_root": url_context_root,
"url_protocol": kwargs.get("url_protocol"),
"enabled": int(kwargs.get("enabled", True)),
"comments": kwargs.get("comments"),
"options": options,
"auth_path": kwargs.get("auth_path", '/authentication'),
"auth_completed_url": kwargs.get("auth_completed_url"),
"auth_completed_url_validation": kwargs.get("auth_completed_url_validation"),
"auth_completed_page_content": kwargs.get("auth_completed_page_content"),
"metadata": kwargs.get("metadata"),
"published": int(kwargs.get("published", False)),
"name": kwargs.get("name"),
})
service = lib.services.get_service(session, service_id)
if lib.core.get_interactive_result():
return f"\nService '{service['host_ctx']}' created successfully."
else:
return service
@plugin_function('mrs.get.service', shell=True, cli=True, web=True)
def get_service(**kwargs):
"""Gets a specific MRS service
If no service is specified, the service that is set as current service is
returned if it was defined before
Args:
**kwargs: Additional options
Keyword Args:
service_id (str): The id of the service
url_context_root (str): The context root for this service
url_host_name (str): The host name for this service
get_default (bool): Whether to return the default service
auto_select_single (bool): If there is a single service only, use that
session (object): The database session to use.
Returns:
The service as dict or None on error in interactive mode
"""
lib.core.convert_ids_to_binary(["service_id"], kwargs)
url_context_root = kwargs.get("url_context_root")
url_host_name = kwargs.get("url_host_name")
service_id = kwargs.get("service_id")
get_default = kwargs.get("get_default", False)
auto_select_single = kwargs.get("auto_select_single", False)
with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
# If there are no selective parameters given and interactive mode
if (not url_context_root and not service_id and not get_default
and lib.core.get_interactive_default()):
# See if there is a current service, if so, return that one
service = lib.services.get_current_service(session=session)
if service:
return lib.services.format_service_listing([service], True)
# Check if there already is at least one service
row = lib.core.select(table="service",
cols="COUNT(*) as service_count, MIN(id) AS id"
).exec(session).first
service_count = row.get("service_count", 0) if row else 0
if service_count == 0:
raise ValueError("No services available. Use "
"mrs.add.`service`() to add a new service.")
if auto_select_single and service_count == 1:
service_id = row["id"]
# If there are more services, let the user select one or all
if not service_id:
services = lib.services.get_services(session)
print("MRS Service Listing")
item = lib.core.prompt_for_list_item(
item_list=services,
prompt_caption=("Please select a service index or type "
"'hostname/root_context': "),
item_name_property="host_ctx",
given_value=None,
print_list=True)
if not item:
raise ValueError("Operation cancelled.")
else:
return lib.services.format_service_listing([item], True)
service = lib.services.get_service(url_context_root=url_context_root, url_host_name=url_host_name,
service_id=service_id, get_default=get_default, session=session)
if lib.core.get_interactive_result():
# in interactive mode, if there is no service, we should display an empty listing
if service:
return lib.services.format_service_listing([service], True)
else:
return "The specified service was not found."
else:
return service
@plugin_function('mrs.list.services', shell=True, cli=True, web=True)
def get_services(**kwargs):
"""Get a list of MRS services
Args:
**kwargs: Additional options
Keyword Args:
session (object): The database session to use.
Returns:
Either a string listing the services when interactive is set or list
of dicts representing the services
"""
with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
services = lib.services.get_services(session)
if lib.core.get_interactive_result():
return lib.services.format_service_listing(services, True)
else:
return services
@plugin_function('mrs.enable.service', shell=True, cli=True, web=True)
def enable_service(**kwargs):
"""Enables a MRS service
If there is no service yet, a service with default values will be
created and set as default.
Args:
**kwargs: Additional options
Keyword Args:
service_id (str): The id of the service
url_context_root (str): The context root for this service
url_host_name (str): The host name for this service
session (object): The database session to use.
Returns:
The result message as string
"""
lib.core.convert_ids_to_binary(["service_id"], kwargs)
kwargs["value"] = {"enabled": True}
kwargs["allow_multi_select"] = True
return call_update_service("enabled", **kwargs)
@plugin_function('mrs.disable.service', shell=True, cli=True, web=True)
def disable_service(**kwargs):
"""Disables a MRS service
Args:
**kwargs: Additional options
Keyword Args:
service_id (str): The id of the service
url_context_root (str): The context root for this service
url_host_name (str): The host name for this service
session (object): The database session to use.
Returns:
The result message as string
"""
lib.core.convert_ids_to_binary(["service_id"], kwargs)
kwargs["value"] = {"enabled": False}
kwargs["allow_multi_select"] = True
return call_update_service("disabled", **kwargs)
@plugin_function('mrs.delete.service', shell=True, cli=True, web=True)
def delete_service(**kwargs):
"""Deletes a MRS service
Args:
**kwargs: Additional options
Keyword Args:
service_id (str): The id of the service
url_context_root (str): The context root for this service
url_host_name (str): The host name for this service
session (object): The database session to use.
Returns:
The result message as string
"""
with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
lib.core.convert_ids_to_binary(["service_id"], kwargs)
kwargs["session"] = session
kwargs["allow_multi_select"] = True
kwargs = resolve_service_ids(**kwargs)
with lib.core.MrsDbTransaction(session):
lib.services.delete_services(session, kwargs["service_ids"])
if lib.core.get_interactive_result():
if len(kwargs['service_ids']) == 1:
return f"The service has been deleted."
return f"The services have been deleted."
return True
@plugin_function('mrs.set.service.contextPath', shell=True, cli=True, web=True)
def set_url_context_root(**kwargs):
"""Sets the url_context_root of a MRS service
Args:
**kwargs: Additional options
Keyword Args:
service_id (str): The id of the service
url_context_root (str): The context root for this service
url_host_name (str): The host name for this service
value (str): The context_path
session (object): The database session to use.
Returns:
The result message as string
"""
lib.core.convert_ids_to_binary(["service_id"], kwargs)
kwargs["value"] = {"url_context_root": kwargs["value"]}
if "service_id" not in kwargs:
kwargs = resolve_url_context_root(required=False, **kwargs)
kwargs = resolve_url_host_name(required=False, **kwargs)
kwargs.pop("url_context_root", None)
return call_update_service("updated", **kwargs)
@plugin_function('mrs.set.service.protocol', shell=True, cli=True, web=True)
def set_protocol(**kwargs):
"""Sets the protocol of a MRS service
Args:
**kwargs: Additional options
Keyword Args:
service_id (str): The id of the service
url_context_root (str): The context root for this service
url_host_name (str): The host name for this service
value (str): The protocol either 'HTTP', 'HTTPS' or 'HTTP,HTTPS'
session (object): The database session to use.
Returns:
The result message as string
"""
lib.core.convert_ids_to_binary(["service_id"], kwargs)
kwargs["value"] = {"url_protocol": kwargs["value"]}
if "service_id" not in kwargs:
kwargs = resolve_url_context_root(required=False, **kwargs)
kwargs = resolve_url_host_name(required=False, **kwargs)
kwargs.pop("url_protocol", None)
return call_update_service("updated", **kwargs)
@plugin_function('mrs.set.service.comments', shell=True, cli=True, web=True)
def set_comments(**kwargs):
"""Sets the comments of a MRS service
Args:
**kwargs: Additional options
Keyword Args:
service_id (str): The id of the service
url_context_root (str): The context root for this service
url_host_name (str): The host name for this service
value (str): The comments
session (object): The database session to use.
Returns:
The result message as string
"""
lib.core.convert_ids_to_binary(["service_id"], kwargs)
kwargs["value"] = {"comments": kwargs["value"]}
if "service_id" not in kwargs:
kwargs = resolve_url_context_root(required=False, **kwargs)
kwargs = resolve_url_host_name(required=False, **kwargs)
kwargs.pop("comments", None)
return call_update_service("updated", **kwargs)
@plugin_function('mrs.set.service.options', shell=True, cli=True, web=True)
def set_options(**kwargs):
"""Sets the options of a MRS service
Args:
**kwargs: Additional options
Keyword Args:
url_context_root (str): The context root for this service
url_host_name (str): The host name for this service
value (str): The comments
service_id (str): The id of the service
session (object): The database session to use.
Returns:
The result message as string
"""
lib.core.convert_ids_to_binary(["service_id"], kwargs)
kwargs["value"] = {"options": kwargs["value"]}
if "service_id" not in kwargs:
kwargs = resolve_url_context_root(required=False, **kwargs)
kwargs = resolve_url_host_name(required=False, **kwargs)
kwargs.pop("options", None)
return call_update_service("updated", **kwargs)
@plugin_function('mrs.update.service', shell=True, cli=True, web=True)
def update_service(**kwargs):
"""Sets all properties of a MRS service
Args:
**kwargs: Additional options
Keyword Args:
service_id (str): The id of the service
url_context_root (str): The context root for this service
url_host_name (str): The host name for this service
value (dict): The values as dict
session (object): The database session to use.
Allowed options for value:
url_context_root (str): The context root for this service
url_protocol (list): The protocol either 'HTTP', 'HTTPS' or 'HTTP,HTTPS'
url_host_name (str): The host name for this service
enabled (bool): Whether the service should be enabled
comments (str): Comments about the service
options (dict): Options of the service
auth_path (str): The authentication path
auth_completed_url (str): The redirection URL called after authentication
auth_completed_url_validation (str): The regular expression that validates the
app redirection URL specified by the /login?onCompletionRedirect parameter
auth_completed_page_content (str): The custom page content to use of the
authentication completed page
metadata (dict): The metadata of the service
in_development (dict): The development settings
published (bool): Whether the service is published
name (str): The name of the service
Returns:
The result message as string
"""
if kwargs.get("value") is not None:
# create a copy so that the dict won't change for the caller...and convert to dict
kwargs["value"] = lib.core.convert_json(kwargs["value"])
lib.core.convert_ids_to_binary(["service_id"], kwargs)
verify_value_keys(**kwargs)
return call_update_service("updated", **kwargs)
@plugin_function('mrs.get.serviceRequestPathAvailability', shell=True, cli=True, web=True)
def get_service_request_path_availability(**kwargs):
"""Checks the availability of a given request path for the given service
Args:
**kwargs: Additional options
Keyword Args:
service_id (str): The id of the service
request_path (str): The request path to check
session (object): The database session to use.
Returns:
True or False
"""
lib.core.convert_ids_to_binary(["service_id"], kwargs)
service_id = kwargs.get("service_id")
request_path = kwargs.get("request_path")
with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
service = resolve_service(session, service_id, True)
# Get request_path
if not request_path and lib.core.get_interactive_default():
request_path = lib.core.prompt(
"Please enter the request path for this content set ["
f"/content]: ",
{'defaultValue': '/content'}).strip()
if not request_path.startswith('/'):
raise Exception("The request_path has to start with '/'.")
try:
in_development = service.get("in_development", None)
if in_development is None:
in_development = {}
lib.core.check_request_path(
session,
in_development.get("developers", "")
+ service["host_ctx"] + request_path)
except:
return False
return True
@plugin_function('mrs.get.currentServiceMetadata', shell=True, cli=True, web=True)
def get_current_service_metadata(**kwargs):
"""Gets information about the current service
This function returns the id of the current MRS service as well as the last id of the metadata audit_log
related to this MRS service as metadata_version. If there are no entries for the service in the audit_log, the
string noChange is returned instead.
Args:
**kwargs: Additional options
Keyword Args:
session (object): The database session to use.
Returns:
{id: string, host_ctx: string, metadata_version: string}
"""
session = kwargs.get("session")
if session.database_type != "MySQL":
return {}
with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
status = lib.general.get_status(session)
if status.get("service_configured", False) == False:
return {}
service_id = lib.services.get_current_service_id(session)
service = lib.services.get_service(
session=session, service_id=service_id)
# Lookup the last entry in the audit_log table that affects the service and use that as the
# version int
res = session.run_sql(
"""
SELECT max(id) AS version FROM `mysql_rest_service_metadata`.`audit_log`
""")
row = res.fetch_one()
metadata_version = row.get_field("version") if row is not None else "0"
if metadata_version is None:
metadata_version = "0"
if service is None:
lib.services.set_current_service_id(
session=session, service_id=None)
return {
"metadata_version": metadata_version
}
metadata = {
"id": lib.core.convert_id_to_string(service.get("id")),
"host_ctx": service.get("host_ctx"),
"metadata_version": metadata_version
}
if not lib.core.get_interactive_result():
return metadata
else:
return lib.services.format_metadata(metadata["host_ctx"], metadata["metadata_version"])
@plugin_function('mrs.set.currentService', shell=True, cli=True, web=True)
def set_current_service(**kwargs):
"""Sets the default MRS service
Args:
**kwargs: Additional options
Keyword Args:
service_id (str): The id of the service
url_context_root (str): The context root for this service
url_host_name (str): The host name for this service
session (object): The database session to use.
Returns:
The result message as string
"""
lib.core.convert_ids_to_binary(["service_id"], kwargs)
service_id = kwargs.get("service_id")
with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
if service_id is None:
kwargs["session"] = session
kwargs = resolve_url_context_root(required=False, **kwargs)
kwargs = resolve_url_host_name(required=False, **kwargs)
kwargs = resolve_service_ids(**kwargs)
if kwargs["service_ids"]:
service_id = kwargs["service_ids"][0]
if service_id is None:
if lib.core.get_interactive_result():
return "The specified service was not found."
return False
lib.services.set_current_service_id(session, service_id)
if lib.core.get_interactive_result():
return "The service has been made the default."
return True
@plugin_function('mrs.get.sdkBaseClasses', shell=True, cli=True, web=True)
def get_sdk_base_classes(**kwargs):
"""Returns the SDK base classes source for the given language
Args:
**kwargs: Options to determine what should be generated.
Keyword Args:
sdk_language (str): The SDK language to generate
prepare_for_runtime (bool): Prepare code to be used in Monaco at runtime
session (object): The database session to use.
Returns:
The SDK base classes source
"""
sdk_language = kwargs.get("sdk_language", "TypeScript")
prepare_for_runtime = kwargs.get("prepare_for_runtime", False)
return lib.sdk.get_base_classes(sdk_language=sdk_language, prepare_for_runtime=prepare_for_runtime)
@plugin_function('mrs.get.sdkServiceClasses', shell=True, cli=True, web=True)
def get_sdk_service_classes(**kwargs):
"""Returns the SDK service classes source for the given language
Args:
**kwargs: Options to determine what should be generated.
Keyword Args:
service_id (str): The id of the service
service_url (str): The url of the service
sdk_language (str): The SDK language to generate
prepare_for_runtime (bool): Prepare code to be used in Monaco at runtime
session (object): The database session to use.
Returns:
The SDK base classes source
"""
lib.core.convert_ids_to_binary(["service_id"], kwargs)
service_id = kwargs.get("service_id")
sdk_language = kwargs.get("sdk_language", "TypeScript")
prepare_for_runtime = kwargs.get("prepare_for_runtime", False)
service_url = kwargs.get("service_url")
with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
service = resolve_service(
session=session, service_query=service_id, required=False, auto_select_single=True)
return lib.sdk.generate_service_sdk(
service=service, sdk_language=sdk_language, session=session, prepare_for_runtime=prepare_for_runtime,
service_url=service_url)
@plugin_function('mrs.dump.sdkServiceFiles', shell=True, cli=True, web=True)
def dump_sdk_service_files(**kwargs):
"""Dumps the SDK service files for a REST Service
Args:
**kwargs: Options to determine what should be generated.
Keyword Args:
directory (str): The directory to store the .mrs.sdk folder with the files
options (dict): Several options how the SDK should be created
session (object): The database session to use.
Allowed options for options:
service_id (str): The ID of the service the SDK should be generated for. If not specified, the default service
is used.
db_connection_uri (str): The dbConnectionUri that was used to export the SDK files
sdk_language (str): The SDK language to generate
add_app_base_class (str): The additional AppBaseClass file name
service_url (str): The url of the service
version (integer): The version of the generated files
generationDate (str): The generation date of the SDK files
header (str): The header to use for the SDK files
Returns:
True on success
"""
directory = kwargs.get("directory")
options = kwargs.get("options", {})
if not directory:
if lib.core.get_interactive_default():
directory = lib.core.prompt(
"Please enter the directory the folder with the SDK files should be placed:")
if not directory:
print("Cancelled.")
return False
else:
raise Exception("No directory given.")
# Ensure the directory path exists
Path(directory).mkdir(parents=True, exist_ok=True)
# Try to read the mrs_config from the directory
mrs_config = get_stored_sdk_options(directory=directory)
if mrs_config is None and options is None:
raise Exception(
f"No SDK options given and no existing SDK config found in the directory {directory}")
if mrs_config is None:
mrs_config = {}
mrs_config["serviceId"] = options.get("service_id", mrs_config.get("serviceId"))
mrs_config["sdkLanguage"] = options.get("sdk_language", mrs_config.get("sdkLanguage", "TypeScript"))
mrs_config["serviceUrl"] = options.get("service_url", mrs_config.get("serviceUrl"))
mrs_config["addAppBaseClass"] = options.get("add_app_base_class", mrs_config.get("addAppBaseClass"))
mrs_config["dbConnectionUri"] = options.get("db_connection_uri", mrs_config.get("dbConnectionUri"))
mrs_config["generationDate"] = datetime.datetime.now(
datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
if mrs_config.get("serviceUrl") is None:
raise Exception("The service URL is required.")
mrs_config["header"] = options.get(
"header", default_copyright_header(mrs_config["sdkLanguage"]))
sdk_language = mrs_config["sdkLanguage"]
with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
if mrs_config.get("serviceId") is None:
mrs_config["serviceId"] = lib.core.convert_id_to_base64_string(
lib.services.get_current_service_id(session))
serviceId = lib.core.id_to_binary(
mrs_config.get("serviceId"), "mrs.config.json", True)
if serviceId is None:
raise Exception(
"No serviceId defined in mrs.config.json. Please export the MRS SDK again.")
service = resolve_service(session, serviceId, True, True)
service_name = lib.core.convert_path_to_camel_case(
service.get("url_context_root"))
if sdk_language == "TypeScript":
file_type = "ts"
base_classes_file = os.path.join(directory, "MrsBaseClasses.ts")
elif sdk_language == "Python":
file_type = "py"
base_classes_file = os.path.join(directory, "mrs_base_classes.py")
base_classes = get_sdk_base_classes(
sdk_language=sdk_language, session=session)
with open(base_classes_file, 'w') as f:
f.write(base_classes)
file_name = file_name_using_language_convention(
service_name, sdk_language)
service_classes = get_sdk_service_classes(
service_id=serviceId, service_url=mrs_config["serviceUrl"],
sdk_language=sdk_language, session=session)
with open(os.path.join(directory, f"{file_name}.{file_type}"), 'w') as f:
f.write(service_classes)
add_app_base_class = mrs_config.get("addAppBaseClass")
if add_app_base_class is not None and isinstance(add_app_base_class, str) and add_app_base_class != '':
path = os.path.abspath(__file__)
file_path = Path(os.path.dirname(path), "sdk", sdk_language.lower(), add_app_base_class)
shutil.copy(file_path, os.path.join(directory, add_app_base_class))
# cspell:ignore timespec
conf_file = Path(directory, "mrs.config.json")
with open(conf_file, 'w') as f:
f.write(json.dumps(mrs_config, indent=4))
# TODO: this should be in a separate function (maybe context-aware for each language)
if sdk_language == "Python":
# In Python, we should create a "__init__.py" file to be able to import the directory as a regular package
package_file = Path(directory, "__init__.py")
with open(package_file, "w") as f:
copyright_header = mrs_config["header"]
f.write(copyright_header)
return True
@plugin_function('mrs.get.sdkOptions', shell=True, cli=True, web=True)
def get_stored_sdk_options(directory):
"""Reads the SDK service option file located in a given directory
Args:
directory (str): The directory where the mrs.config.json file is stored
Returns:
The SDK options stored in that directory otherwise None
"""
# Try to read the mrs_config from the directory
mrs_config = {}
conf_file = Path(directory, "mrs.config.json")
if conf_file.is_file():
try:
with open(conf_file) as f:
mrs_config = json.load(f)
except:
pass
if "addAppBaseClass" in mrs_config:
if isinstance(mrs_config["addAppBaseClass"], int):
del mrs_config["addAppBaseClass"]
return mrs_config
@plugin_function('mrs.get.runtimeManagementCode', shell=True, cli=True, web=True)
def get_runtime_management_code(**kwargs):
"""Returns the SDK service classes source for the given language
Args:
**kwargs: Options to determine what should be generated.
Keyword Args:
session (object): The database session to use.
Returns:
The SDK base classes source
"""
with lib.core.MrsDbSession(exception_handler=lib.core.print_exception, **kwargs) as session:
return lib.sdk.get_mrs_runtime_management_code(session=session)
@plugin_function('mrs.get.serviceCreateStatement', shell=True, cli=True, web=True)
def get_service_create_statement(**kwargs):
"""Returns the corresponding CREATE REST SERVICE SQL statement of the given MRS service object.
When using the 'service' parameter, you can choose either of these formats:
- '0x11EF8496143CFDEC969C7413EA499D96' - Hexadecimal string ID
- 'Ee+ElhQ8/eyWnHQT6kmdlg==' - Base64 string ID
- 'localhost/myService' - Human readable string ID
The service search parameters will be prioritized in the following order:
- service_id
- service
- url_host_name + url_context_root
Args:
**kwargs: Options to determine what should be generated.
Keyword Args:
service_id (str): The ID of the service to generate.
service (str): The identifier of the service.
url_context_root (str): The context root for this service
url_host_name (str): The host name for this service
include_database_endpoints (bool): Include database objects that belong to the service.
session (object): The database session to use.
Returns:
The SQL that represents the create statement for the MRS service
"""
return generate_create_statement(**kwargs)
@plugin_function('mrs.dump.serviceSqlScript', shell=True, cli=True, web=True)
def dump_service_create_statement(**kwargs):
"""Dump a REST Service into a REST SQL file. The database and the dynamic endpoints will be included.
When using the 'service' parameter, you can choose either of these formats:
- '0x11EF8496143CFDEC969C7413EA499D96' - Hexadecimal string ID
- 'Ee+ElhQ8/eyWnHQT6kmdlg==' - Base64 string ID
- 'localhost/myService' - Human readable string ID
The service search parameters will be prioritized in the following order:
- service_id
- service
- url_host_name + url_context_root
Args:
**kwargs: Options to determine what should be generated.
Keyword Args:
service (str): The identifier of the service.
endpoints (str): The endpoints to be included in the script
file_path (str): The path where to store the file.
overwrite (bool): Overwrite the file, if already exists.
zip (bool): The final file is to be zipped.
session (object): The database session to use.
Returns:
True if the file was saved.
"""
file_path = kwargs.get("file_path")
overwrite = kwargs.get("overwrite")
match kwargs.get("endpoints", ""):
case "all" | "dynamic":
kwargs["include_database_endpoints"] = True
kwargs["include_static_endpoints"] = True
kwargs["include_dynamic_endpoints"] = True
case "static":
kwargs["include_database_endpoints"] = True
kwargs["include_static_endpoints"] = True
case "database":
kwargs["include_database_endpoints"] = True
file_path = resolve_file_path(file_path)
resolve_overwrite_file(file_path, overwrite)
kwargs["file_path"] = file_path
store_create_statement(**kwargs)
if lib.core.get_interactive_result():
return f"File created in {file_path}."
return True