| Server IP : 185.208.173.17 / Your IP : 87.236.161.98 Web Server : Microsoft-IIS/10.0 System : Windows NT SRV8576125506 10.0 build 26100 (Windows Server 2016) AMD64 User : IUSR ( 0) PHP Version : 7.4.13 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : OFF | Perl : OFF | Python : OFF | Sudo : OFF | Pkexec : OFF Directory : /Program Files/MySQL/MySQL Workbench 8.0/swb/shell/lib/mysqlsh/plugins/mrs_plugin/lib/ |
Upload File : |
# Copyright (c) 2022, 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
from mrs_plugin.lib import (
core,
roles,
schemas,
content_sets,
auth_apps,
database,
script,
)
import re
import os
from zipfile import ZipFile, is_zipfile
import copy
import json
import shutil
from datetime import datetime
import mysqlsh
from typing import Optional
from tempfile import TemporaryDirectory
from urllib.parse import urlparse, ParseResult
import urllib.request
import ssl
DEFAULT_OPTIONS = {
"headers": {
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With, Origin, X-Auth-Token",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
},
"http": {"allowedOrigin": "auto"},
"logging": {
"exceptions": True,
"request": {"body": True, "headers": True},
"response": {"body": True, "headers": True},
},
"returnInternalErrorDetails": True,
"includeLinksInResults": False,
}
def prompt_for_url_context_root(default=None):
"""Prompts the user for the url_context_root
Returns:
The url_context_root as str
"""
return core.prompt(
"Please enter the context path for this service [/myService]: ",
{"defaultValue": default if default else "/myService"},
).strip()
def prompt_for_service_protocol(default=None):
"""Prompts the user for the supported service protocols
Returns:
The service protocols as str
"""
protocols = core.prompt_for_list_item(
item_list=[
"HTTP",
"HTTPS",
# "WEBSOCKET VIA HTTP", "WEBSOCKET VIA HTTPS"
],
prompt_caption=(
"Please select the protocol(s) the service should support "
f"[{default if default else 'HTTP,HTTPS'}]: "
),
prompt_default_value=default if default else "HTTP,HTTPS",
print_list=True,
allow_multi_select=True,
)
return ",".join(protocols)
def format_service_listing(services, print_header=False):
"""Formats the listing of MRS services
Args:
services (list): A list of services as dicts
print_header (bool): If set to true, a header is printed
Returns:
The formatted list of services
"""
if print_header:
output = (
f"{'ID':>3} {'PATH':25} {'ENABLED':8} {'PROTOCOL(s)':20} "
f"{'DEFAULT':9}\n"
)
else:
output = ""
for i, item in enumerate(services, start=1):
url = item.get("url_host_name") + item.get("url_context_root")
output += (
f"{i:>3} {url[:24]:25} "
f"{'Yes' if item['enabled'] else '-':8} "
f"{','.join(item['url_protocol'])[:19]:20} "
f"{'Yes' if item['is_current'] else '-':5}"
)
if i < len(services):
output += "\n"
return output
def format_metadata(host_ctx: Optional[str] = None, version: Optional[str] = None):
"""Formats the service metadata details
Args:
host_ctx (str): The url root context path
version (int): The version in the metadata audit log
Returns:
The metadata details in a tabular string
"""
table = f"{'ID':>3} {'ROOT PATH':25} {'VERSION':15}"
if host_ctx is None:
return table
return f"{table}\n{1:>3} {host_ctx[:24]:25} {version}"
def add_service(session, url_host_name, service):
if "options" in service:
service["options"] = core.convert_json(service["options"])
else:
service["options"] = DEFAULT_OPTIONS
path = service.get("url_context_root").lower()
if path == "/mrs":
raise Exception(
f"The REST service path `{path}` is reserved and cannot be used."
)
# If there is no id for the given host yet, create a host entry
if service.get("url_host_id") is None:
host = (
core.select(table="url_host", where=["name=?"])
.exec(session, [url_host_name if url_host_name else ""])
.first
)
if host:
service["url_host_id"] = host["id"]
else:
service["url_host_id"] = core.get_sequence_id(session)
core.insert(
table="url_host",
values={"id": service["url_host_id"], "name": url_host_name or ""},
).exec(session)
service["id"] = core.get_sequence_id(session)
# metadata column was only added in 3.0.0
current_version = core.get_mrs_schema_version(session)
if current_version[0] <= 2:
service.pop("metadata", None)
if not core.insert(table="service", values=service).exec(session).success:
raise Exception("Failed to add the new service.")
return service["id"]
def validate_service_path(session, path):
"""Ensures the given path is valid in any of the registered services.
Args:
session (object): The database session to use.
path (str): The path to validate.
Returns:
service, schema, content_set as dict.
"""
if not path:
return None, None, None
service = None
schema = None
content_set = None
# Match path against services and schemas
all_services = get_services(session)
for item in all_services:
host_ctx = item.get("host_ctx")
if host_ctx == path[: len(host_ctx)]:
service = item
if len(path) > len(host_ctx):
sub_path = path[len(host_ctx) :]
db_schemas = schemas.get_schemas(
service_id=service.get("id"), session=session
)
if db_schemas:
for item in db_schemas:
request_path = item.get("request_path")
if request_path == sub_path[: len(request_path)]:
schema = item
break
if not schema:
content_sets_local = content_sets.get_content_sets(
service_id=service.get("id"), session=session
)
if content_sets_local:
for item in content_sets_local:
request_path = item.get("request_path")
if request_path == sub_path[: len(request_path)]:
content_set = item
break
if not schema and not content_set:
raise ValueError(f"The given schema or content set was not found.")
break
if not service:
raise ValueError(f"The given MRS service was not found.")
return service, schema, content_set
def delete_service(session, service_id):
res = core.delete(table="service", where=["id=?"]).exec(
session, params=[service_id]
)
if not res.success:
raise Exception(f"The specified service with id {service_id} was not found.")
def delete_services(session, service_ids):
for service_id in service_ids:
delete_service(session, service_id)
def update_services(session, service_ids, value, merge_options=False):
"""Makes a given change to a MRS service
Args:
session: The database session to use
service_ids: The list of service ids to change
value: The value to be set as a dict for all values that will be changed
merge_options: If set to True, specified options will be merged rather than overwritten
Returns:
The result message as string
"""
# Update all given services
for service_id in service_ids:
service = get_service(session, service_id=service_id)
if service is None:
raise Exception(
f"The specified service with id {core.convert_id_to_string(service_id)} was not found."
)
if "url_host_name" in value:
host = (
core.select(table="url_host", where="name=?")
.exec(session, [value["url_host_name"]])
.first
)
if host:
host_id = host["id"]
else:
host_id = core.get_sequence_id(session)
core.insert(
table="url_host",
values={"id": host_id, "name": value["url_host_name"]},
).exec(session)
del value["url_host_name"]
value["url_host_id"] = host_id
# metadata column was only added in 3.0.0
current_version = core.get_mrs_schema_version(session)
if current_version[0] <= 2:
value.pop("metadata", None)
value.pop("published", None)
# Reset an empty in_development.developers list to None
in_development = value.get("in_development", None)
if in_development is not None:
developers = in_development.get("developers", None)
if developers is not None and len(developers) == 0:
value["in_development"] = None
# Prepare the merge of options, if requested
if merge_options:
options = value.get("options", None)
# Check if there are options set already, if so, merge the options
if options is not None:
row = (
core.MrsDbExec(
"""
SELECT options IS NULL AS options_is_null
FROM `mysql_rest_service_metadata`.`service`
WHERE id = ?""",
[service_id],
)
.exec(session)
.first
)
if row and row["options_is_null"] == 1:
merge_options = False
else:
value.pop("options")
if value:
core.update("service", sets=value, where=["id=?"]).exec(
session, [service_id]
)
# Merge options if requested
if merge_options and options is not None:
core.MrsDbExec(
"""
UPDATE `mysql_rest_service_metadata`.`service`
SET options = JSON_MERGE_PATCH(options, ?)
WHERE id = ?
""",
[options, service_id],
).exec(session)
def query_services(
session,
service_id: bytes = None,
url_context_root=None,
url_host_name="",
get_default=False,
developer_list=None,
auth_app_id=None,
):
"""Query MRS services
Query the existing services. Filters may be applied as the 'service_id' or
the 'url_context_root' with the 'url_host_name'.
In the case no service is found, the default service may be fetched if the
'get_default' is set to True.
To get the default service, don't set any other filters and set 'get_default'
to True.
Args:
session (object): The database session to use.
service_id: The id of the service
url_context_root (str): The context root for this service
get_default (bool): Whether to return the default service
Returns:
The list of found services.
"""
if url_context_root and not url_context_root.startswith("/"):
raise Exception("The url_context_root has to start with '/'.")
url_host_name = "" # no longer supported
current_service_id = get_current_service_id(session)
if not current_service_id:
current_service_id = "0x00000000000000000000000000000000"
wheres = []
params = [current_service_id]
current_version = core.get_mrs_schema_version(session)
if current_version[0] <= 2:
# Build SQL based on which input has been provided
sql = f"""
SELECT se.id, se.enabled, se.url_protocol, h.name AS url_host_name,
se.url_context_root, se.comments, se.options, se.url_host_id,
CONCAT(h.name, se.url_context_root) AS host_ctx,
CONCAT(h.name, se.url_context_root) AS full_service_path,
se.auth_path, se.auth_completed_url,
se.auth_completed_url_validation,
se.auth_completed_page_content,
se.id = ? as is_current,
NULL AS in_development,
NULL AS sorted_developers,
se.name
FROM `mysql_rest_service_metadata`.`service` se
LEFT JOIN `mysql_rest_service_metadata`.url_host h
ON se.url_host_id = h.id
"""
else:
sql = f"""
SELECT se.id, se.enabled, se.published, se.url_protocol, h.name AS url_host_name,
se.url_context_root, se.comments, se.options, se.url_host_id,
CONCAT(h.name, se.url_context_root) AS host_ctx,
(SELECT CONCAT(COALESCE(CONCAT(GROUP_CONCAT(IF(item REGEXP '^[A-Za-z0-9_]+$', item, QUOTE(item)) ORDER BY item), '@'), ''), h.name, se.url_context_root) FROM JSON_TABLE(
se.in_development->>'$.developers', '$[*]' COLUMNS (item text path '$')
) AS jt) AS full_service_path,
se.auth_path, se.auth_completed_url,
se.auth_completed_url_validation,
se.auth_completed_page_content,
se.metadata, se.parent_id,
se.id = ? as is_current,
se.in_development,
(SELECT GROUP_CONCAT(IF(item REGEXP '^[A-Za-z0-9_]+$', item, QUOTE(item)) ORDER BY item)
FROM JSON_TABLE(
se.in_development->>'$.developers', '$[*]' COLUMNS (item text path '$')
) AS jt) AS sorted_developers,
se.name,
(SELECT JSON_ARRAYAGG(aa.name) FROM `mysql_rest_service_metadata`.`service_has_auth_app` sa2
JOIN `mysql_rest_service_metadata`.`auth_app` AS aa ON
sa2.auth_app_id = aa.id
WHERE sa2.service_id = se.id) AS auth_apps
FROM `mysql_rest_service_metadata`.`service` se
LEFT JOIN `mysql_rest_service_metadata`.url_host h
ON se.url_host_id = h.id
"""
if auth_app_id is not None:
sql += """
JOIN `mysql_rest_service_metadata`.`service_has_auth_app` sa
ON se.id = sa.service_id AND sa.auth_app_id = ?
"""
params.append(auth_app_id)
# Make sure that each user only sees the services that are either public or the user is a developer of
# wheres.append("(in_development IS NULL OR "
# "SUBSTRING_INDEX(CURRENT_USER(),'@',1) MEMBER OF(in_development->>'$.developers'))")
if service_id:
wheres.append("se.id = ?")
params.append(service_id)
elif (
url_context_root is not None
and url_host_name is not None
and developer_list is None
):
wheres.append("h.name = ?")
wheres.append("url_context_root = ?")
params.append(url_host_name)
params.append(url_context_root)
wheres.append("se.in_development IS NULL")
elif get_default:
# if nothing else is supplied and get_default is True, then get the default service
wheres = ["se.id = ?"]
params = [current_service_id, current_service_id]
return (
core.MrsDbExec(sql + core._generate_where(wheres), params)
.exec(session)
.items
)
having = ""
if developer_list is not None:
def quote(s):
return f"'{s}'"
# Build the sorted_developer string that matches the selected column, use same quoting as MySQL
developer_list.sort()
sorted_developers = ",".join(
(
dev
if re.match("^[A-Za-z0-9_-]*$", dev)
else quote(re.sub(r"(['\\])", "\\\\\\1", dev, 0, re.MULTILINE))
)
for dev in developer_list
)
having = (
"\nHAVING h.name = ? AND url_context_root = ? AND sorted_developers = ?"
)
params.append(url_host_name)
params.append(url_context_root)
params.append(sorted_developers)
result = (
core.MrsDbExec(
sql
+ core._generate_where(wheres)
+ having
+ "\nORDER BY se.url_context_root, h.name, sorted_developers",
params,
)
.exec(session)
.items
)
if len(result) == 0 and get_default:
# No service was found s if we should get the default, then lets get it
wheres = ["se.id = ?"]
params = [current_service_id, current_service_id]
result = (
core.MrsDbExec(sql + core._generate_where(wheres), params)
.exec(session)
.items
)
return result
def get_service(
session,
service_id: bytes = None,
url_context_root=None,
url_host_name=None,
get_default=False,
developer_list=None,
):
"""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:
session (object): The database session to use.
service_id: The id of the service
url_context_root (str): The context root for this service
get_default (bool): Whether to return the default service
Returns:
The service as dict or None on error in interactive mode
"""
# url_host_name kept as a param for temporary backwards compat, but is no longer supported
result = query_services(
session,
service_id=service_id,
url_context_root=url_context_root,
url_host_name="",
get_default=get_default,
developer_list=developer_list,
)
return result[0] if len(result) == 1 else None
def get_services(session):
"""Get a list of MRS services
Args:
session (object): The database session to use.
Returns:
List of dicts representing the services
"""
return query_services(session)
def get_current_service(session):
service_id = get_current_service_id(session)
return get_service(session=session, service_id=service_id)
def get_current_service_id(session):
"""Returns the current service
Args:
session (object): The database session to use.
Returns:
The current or default service or None if no default is set
"""
if not session:
raise RuntimeError("A valid session is required.")
config = core.ConfigFile()
current_objects = config.settings.get("current_objects", [])
# Try to find the settings for the connection which the service resides on
connection_settings = list(
filter(
lambda item: item["connection"] == core.get_session_uri(session),
current_objects,
)
)
if not connection_settings:
return None
return connection_settings[0].get("current_service_id")
def set_current_service_id(session, service_id: bytes):
if not session:
raise RuntimeError("A valid session is required.")
config = core.ConfigFile()
current_objects = config.settings.get("current_objects", [])
# Try to find the settings for the connection which the service resides on
connection_settings = list(
filter(
lambda item: item["connection"] == core.get_session_uri(session),
current_objects,
)
)
if connection_settings:
# Found the settings for this host
connection_settings[0]["current_service_id"] = service_id
else:
# The settings for this host do not exist yet....create them.
current_objects.append(
{
"connection": core.get_session_uri(session),
"current_service_id": service_id,
}
)
config.settings["current_objects"] = current_objects
config.store()
def get_service_create_statement(
session,
service: dict,
include_database_endpoints: bool,
include_static_endpoints: bool,
include_dynamic_endpoints: bool,
) -> str:
output = []
result = []
service_linked_auth_apps = []
service_linked_auth_apps = auth_apps.get_auth_apps(session, service["id"])
# create the service
output.append(f'CREATE OR REPLACE REST SERVICE {service.get("host_ctx")}')
if service.get("enabled") != 1:
output.append(" DISABLED")
if service.get("comments"): # ignore either None or empty
output.append(f" COMMENT {core.squote_str(service.get("comments"))}")
if service.get("published", False):
output.append(f" PUBLISHED")
auth = []
if service.get("auth_path") != "/authentication":
auth.append(f' PATH {core.quote_auth_app(service.get("auth_path"))}')
if service.get("auth_completed_url"): # ignore either None or empty
auth.append(
f' REDIRECTION {core.quote_str(service.get("auth_completed_url"))}'
)
if service.get("auth_completed_url_validation"): # ignore either None or empty
auth.append(
f' VALIDATION {core.quote_str(service.get("auth_completed_url_validation"))}'
)
if service.get("auth_completed_page_content"): # ignore either None or empty
auth.append(
f' PAGE CONTENT {core.quote_str(service.get("auth_completed_page_content"))}'
)
if auth: # ignore either None or empty
auth.insert(0, f" AUTHENTICATION")
output.append("\n".join(auth))
if service.get("options"):
output.append(core.format_json_entry("OPTIONS", service.get("options")))
if service.get("metadata"):
output.append(core.format_json_entry("METADATA", service.get("metadata")))
for auth_app in service_linked_auth_apps:
output.append(
f" ADD AUTH APP {core.quote_auth_app(auth_app["name"])} IF EXISTS"
)
result.append("\n".join(output) + ";")
if include_database_endpoints:
for role in roles.get_roles(session, service["id"], include_global=False):
result.append(roles.get_role_create_statement(session, role))
result += [
schemas.get_schema_create_statement(session, schema, True)
for schema in schemas.get_schemas(session, service["id"])
if schema["schema_type"] != "SCRIPT_MODULE"
]
if include_static_endpoints or include_dynamic_endpoints:
result += [
content_sets.get_content_set_create_statement(
session, content_set, include_dynamic_endpoints
)
for content_set in content_sets.get_content_sets(session, service["id"])
]
return "\n\n".join(result)
def store_service_create_statement(
session,
service: dict,
file_path: str,
zip: bool,
include_database_endpoints: bool = False,
include_static_endpoints: bool = False,
include_dynamic_endpoints: bool = False,
):
file_content = get_service_create_statement(
session,
service,
include_database_endpoints,
include_static_endpoints,
include_dynamic_endpoints,
)
if zip and file_path.endswith(".zip"):
file_path = file_path[: -len(".zip")]
with open(file_path, "w") as f:
f.write(file_content)
if zip:
with ZipFile(f"{file_path}.zip", "w") as f:
f.write(file_path, arcname="service.mrs.sql")
os.remove(file_path)
def store_project_validations(
session,
destination: str,
services: list,
schemas: list,
project_settings: dict,
create_zip: bool,
):
core.validate_path_for_filesystem(destination)
for service_data in services:
service_name = service_data["name"]
service = get_service(session, url_context_root=service_name)
if service is None:
raise Exception(f"The service '{service_name}' was not found.")
if schemas:
for schema_request_path in schemas:
file_path = schema_request_path.get("file_path")
if file_path and not (
os.path.exists(file_path)
and (os.path.isfile(file_path) or os.path.isdir(file_path))
):
raise Exception(f"The given schema '{file_path}' was not found")
if project_settings["icon_path"]:
if not os.path.isfile(project_settings["icon_path"]):
raise Exception("The icon path is not valid.")
def auto_detect_project_dependencies(session, service_id):
"""This function will auto-detect the schemas that this service
depends on"""
result = []
for schema in schemas.get_schemas(session, service_id):
if schema["schema_type"] == "DATABASE_SCHEMA":
result.append({ "name": schema["name"], "file_path": None })
return result
def store_project(
session,
destination: str,
services: list,
schemas: list,
project_settings: dict,
create_zip: bool,
):
# expand the destination path
destination = os.path.expanduser(destination)
config = {
"name": project_settings["name"],
"version": project_settings["version"],
"restServices": [],
"schemas": [],
"creationDate": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
if project_settings["publisher"]:
config["publisher"] = project_settings["publisher"]
if project_settings["description"]:
config["description"] = project_settings["description"]
# remove the ".zip" from the destination to create the temp directory
temp_dir = (
destination[:-4] if create_zip and destination.endswith(".zip") else destination
)
os.makedirs(temp_dir, exist_ok=True)
# copy icon if set to do so
if project_settings["icon_path"]:
config["icon"] = f"appIcon{os.path.splitext(project_settings["icon_path"])[1]}"
icon_target_path = os.path.join(temp_dir, config["icon"])
shutil.copy(project_settings["icon_path"], icon_target_path)
for service_data in services:
service = get_service(session, url_context_root=service_data["name"])
# This is a special case for when the project dump is triggered by the
# frontend. The only thing that is asked from the user will be the
# destination path. A single service will be used (the one selected by
# the user). In this case, we'll try to auto-detect the project dependencies.
# This should be solved when we build a full dialog where the user can choose
# what to add to the project.
if len(services) == 1 and not schemas == 0:
schemas = auto_detect_project_dependencies(session, service["id"])
# create the path by removing the '/' in the service request path
target_file_name = f"{service_data["name"][1:]}.service.mrs.sql"
file_path = os.path.join(temp_dir, target_file_name)
store_service_create_statement(
session,
service,
file_path,
False,
service_data["include_database_endpoints"],
service_data["include_static_endpoints"],
service_data["include_dynamic_endpoints"],
)
config["restServices"].append(
{
"serviceName": service_data["name"],
"fileName": target_file_name,
}
)
for schema_data in schemas:
schema_target = os.path.join(temp_dir, schema_data["name"])
schema_format = None
schema_relative_path = core.make_string_valid_for_filesystem(
schema_data["name"]
)
if schema_data.get("file_path"):
# The schema dump already exists, so we just need to copy it
if os.path.isdir(schema_data["file_path"]):
schema_format = "folder"
shutil.copytree(schema_data["file_path"], schema_target)
else:
schema_target = f"{schema_target}.sql"
schema_relative_path = f"{schema_relative_path}.sql"
schema_format = "sqlFile"
shutil.copy(schema_data["file_path"], schema_target)
else:
# Create a schema dump into the target directory
schema_format = "dump"
# Set the mysqlsh session to the one that was given
if "shell.Object" in str(type(session)):
mysqlsh.globals.shell.set_session(session)
else:
mysqlsh.globals.shell.set_session(session.session)
mysqlsh.globals.util.dump_schemas(
[schema_data["name"]],
f"file://{schema_target}",
{
"skipUpgradeChecks": True,
"showProgress": False,
},
)
config["schemas"].append(
{
"schemaName": schema_data["name"],
"path": schema_relative_path,
"format": schema_format, # sqlFile or folder or dump
}
)
with open(os.path.join(temp_dir, "mrs.package.json"), "w") as f:
json.dump(config, f, indent=4)
if create_zip:
zf = ZipFile(destination, "w")
for dirname, subdirs, files in os.walk(temp_dir):
for filename in files:
zip_filename = os.path.join(dirname, filename)[
len(temp_dir) + 1 :
] # truncate the base directory
zf.write(os.path.join(dirname, filename), arcname=zip_filename)
zf.close()
shutil.rmtree(temp_dir)
def run_sql_script(session, sql_script, is_mrs: bool = False):
commands = mysqlsh.mysql.split_script(sql_script)
sql_mode = session.run_sql("select @@session.sql_mode").fetch_one()[0]
for command in commands:
command = command.strip()
if not command:
continue
if is_mrs:
script.run_mrs_script(
command,
**{"session": session, "sql_mode": sql_mode, "state_data": {}},
)
else:
session.run_sql(command)
def load_service(session, path: str):
with open(path, "r") as f:
sql_script = f.read()
run_sql_script(session, sql_script=sql_script, is_mrs=True)
def is_url(url) -> bool:
result: ParseResult = urlparse(url)
return all([result.scheme, result.netloc])
def is_github_shortcut(url) -> bool:
if "/" not in url:
return False
parts = url.split("/")
if parts[0] not in ["github.com", "github"]:
return False
if len(parts) != 3:
return False
return True
class LoadProjectFileContext:
def __init__(self, path: str) -> None:
self.path = path
self.download_dir = None
self.extract_dir = None
self.repo = None
# if it's a GitHub shortcut, resolve it to download the zip
# for the master sources
if is_github_shortcut(path):
branch = "main"
if "|" in path:
path, branch = path.split("|")
_, user, self.repo = path.split("/")
path = f"https://github.com/{user}/{self.repo}/archive/refs/heads/{branch}.zip"
# if it's a remote file, download it
if is_url(path):
self.download_dir = TemporaryDirectory(delete=False)
self.path = os.path.join(self.download_dir.name, "download.zip")
with urllib.request.urlopen(path, context=ssl._create_unverified_context()) as response:
with open(self.path, "w+b") as f:
f.write(response.read())
# if the file is a zip file, extract it to a directory
if is_zipfile(self.path):
self.extract_dir = TemporaryDirectory(delete=False)
zip_file = ZipFile(self.path)
zip_file.extractall(self.extract_dir.name)
self.path = self.extract_dir.name
sub_items = os.listdir(self.path)
if len(sub_items) == 1 and os.path.isdir(os.path.join(self.path, sub_items[0])):
self.path = os.path.join(self.path, sub_items[0])
def __enter__(self) -> str:
return self.path
def __exit__(self, exc_type, exc_value, exc_traceback):
if self.download_dir:
self.download_dir.cleanup()
self.download_dir = None
if self.extract_dir:
self.extract_dir.cleanup()
self.extract_dir = None
return False
def load_project(session, path: str):
with LoadProjectFileContext(path) as base_directory:
project_file = os.path.join(base_directory, "mrs.package.json")
project_config = None
with open(project_file, "r") as f:
project_config = json.load(f)
with core.MrsDbTransaction(session):
for schema in project_config.get("schemas", []):
if database.get_schema(session, schema) is not None:
raise ValueError(f"The schema '{schema}' already exists.")
schema_path = os.path.join(base_directory, schema["path"])
if schema["format"] == "sqlFile":
with open(schema_path) as f:
run_sql_script(session, f.read())
elif schema["format"] == "folder":
folder_path = schema_path
only_files = [
f
for f in os.listdir(folder_path)
if os.path.isfile(os.path.join(folder_path, f))
and f.endswith(".sql")
]
for file in only_files:
file = os.path.join(schema_path, file)
with open(file) as f:
run_sql_script(session, f.read())
elif schema["format"] == "dump":
if "shell.Object" in str(type(session)):
mysqlsh.globals.shell.set_session(session)
else:
mysqlsh.globals.shell.set_session(session.session)
with core.ServerLocalInFile(session, True):
mysqlsh.globals.util.load_dump(schema_path, ignoreExistingObjects=True)
else:
raise Exception("Invalid schema format.")
for service in project_config.get("restServices", []):
if get_service(session, url_context_root=service["serviceName"]) is not None:
raise ValueError(f"The service '{service["serviceName"]}' already exists.")
with open(os.path.join(base_directory, service["fileName"])) as f:
content = f.read()
run_sql_script(session, content)
def get_service_sdk_data(session, service_id, binary_formatter=None):
return database.get_sdk_service_data(
session, service_id, binary_formatter=binary_formatter
)
def clone_service(session, service, new_url_context_root, dev_list):
new_service = copy.deepcopy(service)
# Cleanup the existing data to properly insert it again
new_service.pop("id", None)
new_service.pop("url_host_name", None)
new_service.pop("host_ctx", None)
new_service.pop("full_service_path", None)
new_service.pop("is_current", None)
new_service.pop("sorted_developers", None)
new_service.pop("auth_apps", None)
new_service.pop("merge_options", None)
new_service["url_context_root"] = new_url_context_root
new_service["published"] = False
new_service["in_development"] = {"developers": dev_list}
# Add the service
new_service_id = add_service(
session=session, url_host_name=None, service=new_service
)
# Create links for the auth apps
for service_auth_app in auth_apps.get_auth_apps(session, service["id"]):
auth_apps.link_auth_app(session, service_auth_app["id"], new_service_id)
# Clone the schemas
for schema in schemas.get_schemas(session, service["id"]):
schemas.clone_schema(session, schema, new_service_id)
# Clone the content sets
for content_set in content_sets.get_content_sets(session, service["id"]):
content_sets.clone_content_set(session, content_set, new_service_id)
return