diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 114dd8df44ab..b1219bd72fb5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,14 +6,14 @@ repos: name: isort (python) - repo: https://github.com/psf/black - rev: 24.4.2 + rev: 24.8.0 hooks: - id: black language_version: python3 exclude: migrations - repo: https://github.com/pycqa/flake8 - rev: 7.1.0 + rev: 7.1.1 hooks: - id: flake8 name: flake8 @@ -31,6 +31,8 @@ repos: hooks: - id: prettier exclude: ^(frontend/|CHANGELOG.md|.github/docker_build_comment_template.md) + additional_dependencies: + - prettier@3.3.3 # SEE: https://github.com/pre-commit/pre-commit/issues/3133 - repo: https://github.com/python-poetry/poetry rev: 1.8.0 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index b900f8a162a6..04bca1732ea1 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.134.0" + ".": "2.136.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index affdbff3ed21..3669c6f0b6c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,56 @@ # Changelog +## [2.136.0](https://github.com/Flagsmith/flagsmith/compare/v2.135.1...v2.136.0) (2024-08-13) + + +### Features + +* Add automatic tagging for github integration ([#4028](https://github.com/Flagsmith/flagsmith/issues/4028)) ([7920e8e](https://github.com/Flagsmith/flagsmith/commit/7920e8e22e15fc2f91dbd56582679b1f3064e4a9)) +* Add tags for GitHub integration FE ([#4035](https://github.com/Flagsmith/flagsmith/issues/4035)) ([3c46a31](https://github.com/Flagsmith/flagsmith/commit/3c46a31f6060f7a12206fa27409073da946130d8)) +* Support Aptible deployments ([#4340](https://github.com/Flagsmith/flagsmith/issues/4340)) ([3b47ae0](https://github.com/Flagsmith/flagsmith/commit/3b47ae07848c1330210d18bd8bb4194fa5d9262e)) +* Use environment feature state instead of fetching feature states ([#4188](https://github.com/Flagsmith/flagsmith/issues/4188)) ([b1d49a6](https://github.com/Flagsmith/flagsmith/commit/b1d49a63b5d7c1fe319185586f486829626840cd)) + + +### Bug Fixes + +* ensure that usage notification logic is independent of other organisations notifications ([#4480](https://github.com/Flagsmith/flagsmith/issues/4480)) ([6660af5](https://github.com/Flagsmith/flagsmith/commit/6660af56047e8d7083486b2dab20df44d0fc9303)) +* Remove warning about non-unique health namespace ([#4479](https://github.com/Flagsmith/flagsmith/issues/4479)) ([6ef7a74](https://github.com/Flagsmith/flagsmith/commit/6ef7a742f0f56aef2335da380770dc7f307d53c5)) + + +### Infrastructure (Flagsmith SaaS Only) + +* reduce task retention days to 7 ([#4484](https://github.com/Flagsmith/flagsmith/issues/4484)) ([4349149](https://github.com/Flagsmith/flagsmith/commit/4349149700ad92e824c115c93f1912c7009c9aa2)) + +## [2.135.1](https://github.com/Flagsmith/flagsmith/compare/v2.135.0...v2.135.1) (2024-08-12) + + +### Infrastructure (Flagsmith SaaS Only) + +* bump feature evaluation cache to 300 ([#4471](https://github.com/Flagsmith/flagsmith/issues/4471)) ([abbf24b](https://github.com/Flagsmith/flagsmith/commit/abbf24bf987e8f74cb2ecf3ec3d82456d9892654)) + +## [2.135.0](https://github.com/Flagsmith/flagsmith/compare/v2.134.1...v2.135.0) (2024-08-09) + + +### Features + +* **app_analytics:** Add cache for feature evaluation ([#4418](https://github.com/Flagsmith/flagsmith/issues/4418)) ([2dfbf99](https://github.com/Flagsmith/flagsmith/commit/2dfbf99cdc8d8529aa487a4e471df46c0dbc6878)) +* Support blank identifiers, assume transient ([#4449](https://github.com/Flagsmith/flagsmith/issues/4449)) ([0014a5b](https://github.com/Flagsmith/flagsmith/commit/0014a5b4312d1ee7d7dd7b914434f26408ee18b7)) + + +### Bug Fixes + +* Identity overrides are not deleted when deleting Edge identities ([#4460](https://github.com/Flagsmith/flagsmith/issues/4460)) ([2ab73ed](https://github.com/Flagsmith/flagsmith/commit/2ab73edc7352bec8324eb808ba70d6508fe5eed6)) +* show correct SAML Frontend URL on edit ([#4462](https://github.com/Flagsmith/flagsmith/issues/4462)) ([13ad7ef](https://github.com/Flagsmith/flagsmith/commit/13ad7ef7e6613bdd640cdfca7ce99a892b3893be)) + +## [2.134.1](https://github.com/Flagsmith/flagsmith/compare/v2.134.0...v2.134.1) (2024-08-07) + + +### Bug Fixes + +* don't allow bypassing `ALLOW_REGISTRATION_WITHOUT_INVITE` behaviour ([#4454](https://github.com/Flagsmith/flagsmith/issues/4454)) ([0e6deec](https://github.com/Flagsmith/flagsmith/commit/0e6deec6404c3e78edf5f36b36ea0f2dcef3dd06)) +* protect get environment document endpoint ([#4459](https://github.com/Flagsmith/flagsmith/issues/4459)) ([bee01c7](https://github.com/Flagsmith/flagsmith/commit/bee01c7f21cae19e7665ede3284f96989d33940f)) +* Set grace period to a singular event ([#4455](https://github.com/Flagsmith/flagsmith/issues/4455)) ([3225c47](https://github.com/Flagsmith/flagsmith/commit/3225c47043f9647a7426b7f05890bde29b681acc)) + ## [2.134.0](https://github.com/Flagsmith/flagsmith/compare/v2.133.1...v2.134.0) (2024-08-02) diff --git a/api/app/settings/common.py b/api/app/settings/common.py index b5705b60770a..c3fdc55384c7 100644 --- a/api/app/settings/common.py +++ b/api/app/settings/common.py @@ -330,6 +330,11 @@ USE_POSTGRES_FOR_ANALYTICS = env.bool("USE_POSTGRES_FOR_ANALYTICS", default=False) USE_CACHE_FOR_USAGE_DATA = env.bool("USE_CACHE_FOR_USAGE_DATA", default=False) +PG_API_USAGE_CACHE_SECONDS = env.int("PG_API_USAGE_CACHE_SECONDS", default=60) + +FEATURE_EVALUATION_CACHE_SECONDS = env.int( + "FEATURE_EVALUATION_CACHE_SECONDS", default=60 +) ENABLE_API_USAGE_TRACKING = env.bool("ENABLE_API_USAGE_TRACKING", default=True) @@ -512,6 +517,10 @@ LOGOUT_URL = "/admin/logout/" # Enable E2E tests +E2E_TEST_AUTH_TOKEN = env.str("E2E_TEST_AUTH_TOKEN", default=None) +if E2E_TEST_AUTH_TOKEN is not None: + MIDDLEWARE.append("e2etests.middleware.E2ETestMiddleware") + ENABLE_FE_E2E = env.bool("ENABLE_FE_E2E", default=False) # Email associated with user that is used by front end for end to end testing purposes E2E_TEST_EMAIL_DOMAIN = "flagsmithe2etestdomain.io" @@ -521,6 +530,18 @@ E2E_CHANGE_EMAIL_USER = f"e2e_change_email@{E2E_TEST_EMAIL_DOMAIN}" # User email address used for the rest of the E2E tests E2E_USER = f"e2e_user@{E2E_TEST_EMAIL_DOMAIN}" +E2E_NON_ADMIN_USER_WITH_ORG_PERMISSIONS = ( + f"e2e_non_admin_user_with_org_permissions@{E2E_TEST_EMAIL_DOMAIN}" +) +E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS = ( + f"e2e_non_admin_user_with_project_permissions@{E2E_TEST_EMAIL_DOMAIN}" +) +E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS = ( + f"e2e_non_admin_user_with_env_permissions@{E2E_TEST_EMAIL_DOMAIN}" +) +E2E_NON_ADMIN_USER_WITH_A_ROLE = ( + f"e2e_non_admin_user_with_a_role@{E2E_TEST_EMAIL_DOMAIN}" +) # Identity for E2E segment tests E2E_IDENTITY = "test-identity" diff --git a/api/app/urls.py b/api/app/urls.py index a110c7d111c0..65caa58df8ba 100644 --- a/api/app/urls.py +++ b/api/app/urls.py @@ -15,6 +15,9 @@ re_path(r"^api/v2/", include("api.urls.v2", namespace="api-v2")), re_path(r"^admin/", admin.site.urls), re_path(r"^health", include("health_check.urls", namespace="health")), + # Aptible health checks must be on /healthcheck and cannot redirect + # see https://www.aptible.com/docs/core-concepts/apps/connecting-to-apps/app-endpoints/https-endpoints/health-checks + path("healthcheck", include("health_check.urls", namespace="aptible")), re_path(r"^version", views.version_info, name="version-info"), re_path( r"^sales-dashboard/", diff --git a/api/app_analytics/analytics_db_service.py b/api/app_analytics/analytics_db_service.py index 5ccd612247db..59933d808966 100644 --- a/api/app_analytics/analytics_db_service.py +++ b/api/app_analytics/analytics_db_service.py @@ -35,8 +35,7 @@ def get_usage_data( ) -> list[UsageData]: now = timezone.now() - date_stop = date_start = None - period_starts_at = period_ends_at = None + date_start = date_stop = None match period: case constants.CURRENT_BILLING_PERIOD: @@ -47,10 +46,8 @@ def get_usage_data( days=30 ) month_delta = relativedelta(now, starts_at).months - period_starts_at = relativedelta(months=month_delta) + starts_at - period_ends_at = now - date_start = f"-{(now - period_starts_at).days}d" - date_stop = "now()" + date_start = relativedelta(months=month_delta) + starts_at + date_stop = now case constants.PREVIOUS_BILLING_PERIOD: if not getattr(organisation, "subscription_information_cache", None): @@ -61,16 +58,12 @@ def get_usage_data( ) month_delta = relativedelta(now, starts_at).months - 1 month_delta += relativedelta(now, starts_at).years * 12 - period_starts_at = relativedelta(months=month_delta) + starts_at - period_ends_at = relativedelta(months=month_delta + 1) + starts_at - date_start = f"-{(now - period_starts_at).days}d" - date_stop = f"-{(now - period_ends_at).days}d" + date_start = relativedelta(months=month_delta) + starts_at + date_stop = relativedelta(months=month_delta + 1) + starts_at case constants.NINETY_DAY_PERIOD: - period_starts_at = now - relativedelta(days=90) - period_ends_at = now - date_start = "-90d" - date_stop = "now()" + date_start = now - relativedelta(days=90) + date_stop = now if settings.USE_POSTGRES_FOR_ANALYTICS: kwargs = { @@ -79,10 +72,10 @@ def get_usage_data( "project_id": project_id, } - if period_starts_at: - assert period_ends_at - kwargs["date_start"] = period_starts_at - kwargs["date_stop"] = period_ends_at + if date_start: + assert date_stop + kwargs["date_start"] = date_start + kwargs["date_stop"] = date_stop return get_usage_data_from_local_db(**kwargs) diff --git a/api/app_analytics/cache.py b/api/app_analytics/cache.py index aea7c84f7184..f3f2a74416d7 100644 --- a/api/app_analytics/cache.py +++ b/api/app_analytics/cache.py @@ -1,7 +1,9 @@ -from app_analytics.tasks import track_request -from django.utils import timezone +from collections import defaultdict -CACHE_FLUSH_INTERVAL = 60 # seconds +from app_analytics.tasks import track_feature_evaluation, track_request +from app_analytics.track import track_feature_evaluation_influxdb +from django.conf import settings +from django.utils import timezone class APIUsageCache: @@ -29,5 +31,52 @@ def track_request(self, resource: int, host: str, environment_key: str): self._cache[key] = 1 else: self._cache[key] += 1 - if (timezone.now() - self._last_flushed_at).seconds > CACHE_FLUSH_INTERVAL: + if ( + timezone.now() - self._last_flushed_at + ).seconds > settings.PG_API_USAGE_CACHE_SECONDS: + self._flush() + + +class FeatureEvaluationCache: + def __init__(self): + self._cache = {} + self._last_flushed_at = timezone.now() + + def _flush(self): + evaluation_data = defaultdict(dict) + for (environment_id, feature_name), eval_count in self._cache.items(): + evaluation_data[environment_id][feature_name] = eval_count + + for environment_id, feature_evaluations in evaluation_data.items(): + if settings.USE_POSTGRES_FOR_ANALYTICS: + track_feature_evaluation.delay( + kwargs={ + "environment_id": environment_id, + "feature_evaluations": feature_evaluations, + } + ) + + elif settings.INFLUXDB_TOKEN: + track_feature_evaluation_influxdb.delay( + kwargs={ + "environment_id": environment_id, + "feature_evaluations": feature_evaluations, + } + ) + + self._cache = {} + self._last_flushed_at = timezone.now() + + def track_feature_evaluation( + self, environment_id: int, feature_name: str, evaluation_count: int + ): + key = (environment_id, feature_name) + if key not in self._cache: + self._cache[key] = evaluation_count + else: + self._cache[key] += evaluation_count + + if ( + timezone.now() - self._last_flushed_at + ).seconds > settings.FEATURE_EVALUATION_CACHE_SECONDS: self._flush() diff --git a/api/app_analytics/influxdb_wrapper.py b/api/app_analytics/influxdb_wrapper.py index 4a88c596338b..4a61ece6ee17 100644 --- a/api/app_analytics/influxdb_wrapper.py +++ b/api/app_analytics/influxdb_wrapper.py @@ -1,8 +1,10 @@ import logging import typing from collections import defaultdict +from datetime import datetime, timedelta from django.conf import settings +from django.utils import timezone from influxdb_client import InfluxDBClient, Point from influxdb_client.client.exceptions import InfluxDBError from influxdb_client.client.write_api import SYNCHRONOUS @@ -20,11 +22,6 @@ influx_org = settings.INFLUXDB_ORG read_bucket = settings.INFLUXDB_BUCKET + "_downsampled_15m" -range_bucket_mappings = { - "-24h": settings.INFLUXDB_BUCKET + "_downsampled_15m", - "-7d": settings.INFLUXDB_BUCKET + "_downsampled_15m", - "-30d": settings.INFLUXDB_BUCKET + "_downsampled_1h", -} retries = Retry(connect=3, read=3, redirect=3) # Set a timeout to prevent threads being potentially stuck open due to network weirdness influxdb_client = InfluxDBClient( @@ -43,6 +40,13 @@ ) +def get_range_bucket_mappings(date_start: datetime) -> str: + now = timezone.now() + if (now - date_start).days > 10: + return settings.INFLUXDB_BUCKET + "_downsampled_1h" + return settings.INFLUXDB_BUCKET + "_downsampled_15m" + + class InfluxDBWrapper: def __init__(self, name): self.name = name @@ -76,15 +80,22 @@ def write(self): @staticmethod def influx_query_manager( - date_start: str = "-30d", - date_stop: str = "now()", + date_start: datetime | None = None, + date_stop: datetime | None = None, drop_columns: typing.Tuple[str, ...] = DEFAULT_DROP_COLUMNS, filters: str = "|> filter(fn:(r) => r._measurement == 'api_call')", extra: str = "", bucket: str = read_bucket, ): + now = timezone.now() + if date_start is None: + date_start = now - timedelta(days=30) + + if date_stop is None: + date_stop = now + # Influx throws an error for an empty range, so just return a list. - if date_start == "-0d" and date_stop == "now()": + if date_start == date_stop: return [] query_api = influxdb_client.query_api() @@ -92,7 +103,7 @@ def influx_query_manager( query = ( f'from(bucket:"{bucket}")' - f" |> range(start: {date_start}, stop: {date_stop})" + f" |> range(start: {date_start.isoformat()}, stop: {date_stop.isoformat()})" f" {filters}" f" |> drop(columns: {drop_columns_input})" f"{extra}" @@ -108,7 +119,9 @@ def influx_query_manager( def get_events_for_organisation( - organisation_id: id, date_start: str = "-30d", date_stop: str = "now()" + organisation_id: id, + date_start: datetime | None = None, + date_stop: datetime | None = None, ) -> int: """ Query influx db for usage for given organisation id @@ -116,6 +129,13 @@ def get_events_for_organisation( :param organisation_id: an id of the organisation to get usage for :return: a number of request counts for organisation """ + now = timezone.now() + if date_start is None: + date_start = now - timedelta(days=30) + + if date_stop is None: + date_stop = now + result = InfluxDBWrapper.influx_query_manager( filters=build_filter_string( [ @@ -145,7 +165,9 @@ def get_events_for_organisation( def get_event_list_for_organisation( - organisation_id: int, date_start: str = "-30d", date_stop: str = "now()" + organisation_id: int, + date_start: datetime | None = None, + date_stop: datetime | None = None, ) -> tuple[dict[str, list[int]], list[str]]: """ Query influx db for usage for given organisation id @@ -154,6 +176,13 @@ def get_event_list_for_organisation( :return: a number of request counts for organisation in chart.js scheme """ + now = timezone.now() + if date_start is None: + date_start = now - timedelta(days=30) + + if date_stop is None: + date_stop = now + results = InfluxDBWrapper.influx_query_manager( filters=f'|> filter(fn:(r) => r._measurement == "api_call") \ |> filter(fn: (r) => r["organisation_id"] == "{organisation_id}")', @@ -163,15 +192,12 @@ def get_event_list_for_organisation( ) dataset = defaultdict(list) labels = [] + + date_difference = date_stop - date_start + required_records = date_difference.days + 1 for result in results: for record in result.records: dataset[record["resource"]].append(record["_value"]) - if date_stop == "now()": - required_records = abs(int(date_start[:-1])) + 1 - else: - required_records = ( - abs(int(date_start[:-1])) - abs(int(date_stop[:-1])) + 1 - ) if len(labels) != required_records: labels.append(record.values["_time"].strftime("%Y-%m-%d")) return dataset, labels @@ -181,8 +207,8 @@ def get_multiple_event_list_for_organisation( organisation_id: int, project_id: int = None, environment_id: int = None, - date_start: str = "-30d", - date_stop: str = "now()", + date_start: datetime | None = None, + date_stop: datetime | None = None, ) -> list[UsageData]: """ Query influx db for usage for given organisation id @@ -193,6 +219,13 @@ def get_multiple_event_list_for_organisation( :return: a number of requests for flags, traits, identities, environment-document """ + now = timezone.now() + if date_start is None: + date_start = now - timedelta(days=30) + + if date_stop is None: + date_stop = now + filters = [ 'r._measurement == "api_call"', f'r["organisation_id"] == "{organisation_id}"', @@ -227,9 +260,16 @@ def get_usage_data( organisation_id: int, project_id: int | None = None, environment_id: int | None = None, - date_start: str = "-30d", - date_stop: str = "now()", + date_start: datetime | None = None, + date_stop: datetime | None = None, ) -> list[UsageData]: + now = timezone.now() + if date_start is None: + date_start = now - timedelta(days=30) + + if date_stop is None: + date_stop = now + events_list = get_multiple_event_list_for_organisation( organisation_id=organisation_id, project_id=project_id, @@ -243,7 +283,7 @@ def get_usage_data( def get_multiple_event_list_for_feature( environment_id: int, feature_name: str, - date_start: str = "-30d", + date_start: datetime | None = None, aggregate_every: str = "24h", ) -> list[dict]: """ @@ -264,11 +304,14 @@ def get_multiple_event_list_for_feature( :param environment_id: an id of the environment to get usage for :param feature_name: the name of the feature to get usage for - :param date_start: the influx time period to filter on, e.g. -30d, -7d, etc. + :param date_start: the influx datetime period to filter on :param aggregate_every: the influx time period to aggregate the data by, e.g. 24h :return: a list of dicts with feature and request count in a specific environment """ + now = timezone.now() + if date_start is None: + date_start = now - timedelta(days=30) results = InfluxDBWrapper.influx_query_manager( date_start=date_start, @@ -297,15 +340,20 @@ def get_multiple_event_list_for_feature( def get_feature_evaluation_data( feature_name: str, environment_id: int, period: str = "30d" ) -> typing.List[FeatureEvaluationData]: + assert period.endswith("d") + days = int(period[:-1]) + date_start = timezone.now() - timedelta(days=days) data = get_multiple_event_list_for_feature( feature_name=feature_name, environment_id=environment_id, - date_start=f"-{period}", + date_start=date_start, ) return FeatureEvaluationDataSchema(many=True).load(data) -def get_top_organisations(date_start: str, limit: str = ""): +def get_top_organisations( + date_start: datetime | None = None, limit: str = "" +) -> dict[int, int]: """ Query influx db top used organisations @@ -315,10 +363,14 @@ def get_top_organisations(date_start: str, limit: str = ""): :return: top organisations in descending order based on api calls. """ + now = timezone.now() + if date_start is None: + date_start = now - timedelta(days=30) + if limit: limit = f"|> limit(n:{limit})" - bucket = range_bucket_mappings[date_start] + bucket = get_range_bucket_mappings(date_start) results = InfluxDBWrapper.influx_query_manager( date_start=date_start, bucket=bucket, @@ -333,6 +385,7 @@ def get_top_organisations(date_start: str, limit: str = ""): ) dataset = {} + for result in results: for record in result.records: try: @@ -347,7 +400,9 @@ def get_top_organisations(date_start: str, limit: str = ""): return dataset -def get_current_api_usage(organisation_id: int, date_start: str) -> int: +def get_current_api_usage( + organisation_id: int, date_start: datetime | None = None +) -> int: """ Query influx db for api usage @@ -356,6 +411,9 @@ def get_current_api_usage(organisation_id: int, date_start: str) -> int: :return: number of current api calls """ + now = timezone.now() + if date_start is None: + date_start = now - timedelta(days=30) bucket = read_bucket results = InfluxDBWrapper.influx_query_manager( diff --git a/api/app_analytics/management/commands/sendapiusagetoinflux.py b/api/app_analytics/management/commands/sendapiusagetoinflux.py index 92ecafbe184a..a7e00979cda8 100644 --- a/api/app_analytics/management/commands/sendapiusagetoinflux.py +++ b/api/app_analytics/management/commands/sendapiusagetoinflux.py @@ -101,3 +101,5 @@ def handle( write_api = influxdb_client.write_api(write_options=SYNCHRONOUS) write_api.write(bucket=bucket_name, record=record) + + self.stdout.write(self.style.SUCCESS("Successfully sent data to InfluxDB")) diff --git a/api/app_analytics/views.py b/api/app_analytics/views.py index d89b76e3fb97..1f71efc33a2a 100644 --- a/api/app_analytics/views.py +++ b/api/app_analytics/views.py @@ -4,14 +4,9 @@ get_total_events_count, get_usage_data, ) -from app_analytics.tasks import ( - track_feature_evaluation, - track_feature_evaluation_v2, -) -from app_analytics.track import ( - track_feature_evaluation_influxdb, - track_feature_evaluation_influxdb_v2, -) +from app_analytics.cache import FeatureEvaluationCache +from app_analytics.tasks import track_feature_evaluation_v2 +from app_analytics.track import track_feature_evaluation_influxdb_v2 from django.conf import settings from drf_yasg.utils import swagger_auto_schema from rest_framework import status @@ -38,6 +33,7 @@ ) logger = logging.getLogger(__name__) +feature_evaluation_cache = FeatureEvaluationCache() class SDKAnalyticsFlagsV2(CreateAPIView): @@ -141,26 +137,10 @@ def post(self, request, *args, **kwargs): content_type="application/json", status=status.HTTP_200_OK, ) - - if settings.USE_POSTGRES_FOR_ANALYTICS: - track_feature_evaluation.delay( - args=( - request.environment.id, - request.data, - ) + for feature_name, eval_count in self.request.data.items(): + feature_evaluation_cache.track_feature_evaluation( + request.environment.id, feature_name, eval_count ) - elif settings.INFLUXDB_TOKEN: - # Due to load issues on the task processor, we - # explicitly run this task in a separate thread. - # TODO: batch influx data to prevent large amounts - # of tasks. - track_feature_evaluation_influxdb.run_in_thread( - args=( - request.environment.id, - request.data, - ) - ) - return Response(status=status.HTTP_200_OK) def _is_data_valid(self) -> bool: diff --git a/api/conftest.py b/api/conftest.py index ab0aa198e9e3..ff6732ee44d3 100644 --- a/api/conftest.py +++ b/api/conftest.py @@ -1018,14 +1018,20 @@ def flagsmith_environments_v2_table(dynamodb: DynamoDBServiceResource) -> Table: @pytest.fixture() -def feature_external_resource( - feature: Feature, post_request_mock: MagicMock, mocker: MockerFixture -) -> FeatureExternalResource: - mocker.patch( +def mock_github_client_generate_token(mocker: MockerFixture) -> MagicMock: + return mocker.patch( "integrations.github.client.generate_token", return_value="mocked_token", ) + +@pytest.fixture() +def feature_external_resource( + feature: Feature, + post_request_mock: MagicMock, + mocker: MockerFixture, + mock_github_client_generate_token: MagicMock, +) -> FeatureExternalResource: return FeatureExternalResource.objects.create( url="https://github.com/repositoryownertest/repositorynametest/issues/11", type="GITHUB_ISSUE", @@ -1035,16 +1041,26 @@ def feature_external_resource( @pytest.fixture() -def feature_with_value_external_resource( - feature_with_value: Feature, +def feature_external_resource_gh_pr( + feature: Feature, post_request_mock: MagicMock, mocker: MockerFixture, + mock_github_client_generate_token: MagicMock, ) -> FeatureExternalResource: - mocker.patch( - "integrations.github.client.generate_token", - return_value="mocked_token", + return FeatureExternalResource.objects.create( + url="https://github.com/repositoryownertest/repositorynametest/pull/1", + type="GITHUB_PR", + feature=feature, + metadata='{"status": "open"}', ) + +@pytest.fixture() +def feature_with_value_external_resource( + feature_with_value: Feature, + post_request_mock: MagicMock, + mock_github_client_generate_token: MagicMock, +) -> FeatureExternalResource: return FeatureExternalResource.objects.create( url="https://github.com/repositoryownertest/repositorynametest/issues/11", type="GITHUB_ISSUE", @@ -1069,6 +1085,7 @@ def github_repository( repository_owner="repositoryownertest", repository_name="repositorynametest", project=project, + tagging_enabled=True, ) @@ -1120,3 +1137,10 @@ def handle(self, record: logging.LogRecord) -> None: self.messages.append(self.format(record)) return InspectingHandler() + + +@pytest.fixture +def set_github_webhook_secret() -> None: + from django.conf import settings + + settings.GITHUB_WEBHOOK_SECRET = "secret-key" diff --git a/api/core/management/commands/waitfordb.py b/api/core/management/commands/waitfordb.py index 91c37468bc26..811af752994e 100644 --- a/api/core/management/commands/waitfordb.py +++ b/api/core/management/commands/waitfordb.py @@ -45,6 +45,7 @@ def handle( database: str, **options: Any, ) -> None: + start = time.monotonic() wait_between_checks = 0.25 diff --git a/api/custom_auth/oauth/serializers.py b/api/custom_auth/oauth/serializers.py index 6a1e80ab90af..cf16c008191b 100644 --- a/api/custom_auth/oauth/serializers.py +++ b/api/custom_auth/oauth/serializers.py @@ -6,13 +6,11 @@ from django.db.models import F from rest_framework import serializers from rest_framework.authtoken.models import Token -from rest_framework.exceptions import PermissionDenied -from organisations.invites.models import Invite from users.auth_type import AuthType from users.models import SignUpType -from ..constants import USER_REGISTRATION_WITHOUT_INVITE_ERROR_MESSAGE +from ..serializers import InviteLinkValidationMixin from .github import GithubUser from .google import get_user_info @@ -20,7 +18,7 @@ UserModel = get_user_model() -class OAuthLoginSerializer(serializers.Serializer): +class OAuthLoginSerializer(InviteLinkValidationMixin, serializers.Serializer): access_token = serializers.CharField( required=True, help_text="Code or access token returned from the FE interaction with the third party login provider.", @@ -85,12 +83,9 @@ def _get_user(self, user_data: dict): if not existing_user: sign_up_type = self.validated_data.get("sign_up_type") - if not ( - settings.ALLOW_REGISTRATION_WITHOUT_INVITE - or sign_up_type == SignUpType.INVITE_LINK.value - or Invite.objects.filter(email=email).exists() - ): - raise PermissionDenied(USER_REGISTRATION_WITHOUT_INVITE_ERROR_MESSAGE) + self._validate_registration_invite( + email=email, sign_up_type=self.validated_data.get("sign_up_type") + ) return UserModel.objects.create( **user_data, email=email.lower(), sign_up_type=sign_up_type diff --git a/api/custom_auth/serializers.py b/api/custom_auth/serializers.py index 55bb43e595ae..11bd80828a6b 100644 --- a/api/custom_auth/serializers.py +++ b/api/custom_auth/serializers.py @@ -5,7 +5,7 @@ from rest_framework.exceptions import PermissionDenied from rest_framework.validators import UniqueValidator -from organisations.invites.models import Invite +from organisations.invites.models import Invite, InviteLink from users.auth_type import AuthType from users.constants import DEFAULT_DELETE_ORPHAN_ORGANISATIONS_VALUE from users.models import FFAdminUser, SignUpType @@ -23,7 +23,28 @@ class Meta: fields = ("key",) -class CustomUserCreateSerializer(UserCreateSerializer): +class InviteLinkValidationMixin: + invite_hash = serializers.CharField(required=False, write_only=True) + + def _validate_registration_invite(self, email: str, sign_up_type: str) -> None: + if settings.ALLOW_REGISTRATION_WITHOUT_INVITE: + return + + valid = False + + match sign_up_type: + case SignUpType.INVITE_LINK.value: + valid = InviteLink.objects.filter( + hash=self.initial_data.get("invite_hash") + ).exists() + case SignUpType.INVITE_EMAIL.value: + valid = Invite.objects.filter(email__iexact=email.lower()).exists() + + if not valid: + raise PermissionDenied(USER_REGISTRATION_WITHOUT_INVITE_ERROR_MESSAGE) + + +class CustomUserCreateSerializer(UserCreateSerializer, InviteLinkValidationMixin): key = serializers.SerializerMethodField() class Meta(UserCreateSerializer.Meta): @@ -58,6 +79,10 @@ def validate(self, attrs): self.context.get("request"), email=email, raise_exception=True ) + self._validate_registration_invite( + email=email, sign_up_type=attrs.get("sign_up_type") + ) + attrs["email"] = email.lower() return attrs @@ -66,16 +91,6 @@ def get_key(instance): token, _ = Token.objects.get_or_create(user=instance) return token.key - def save(self, **kwargs): - if not ( - settings.ALLOW_REGISTRATION_WITHOUT_INVITE - or self.validated_data.get("sign_up_type") == SignUpType.INVITE_LINK.value - or Invite.objects.filter(email=self.validated_data.get("email")) - ): - raise PermissionDenied(USER_REGISTRATION_WITHOUT_INVITE_ERROR_MESSAGE) - - return super(CustomUserCreateSerializer, self).save(**kwargs) - class CustomUserDelete(serializers.Serializer): current_password = serializers.CharField( diff --git a/api/e2etests/e2e_seed_data.py b/api/e2etests/e2e_seed_data.py index d21d0054031b..eba97bd65cb8 100644 --- a/api/e2etests/e2e_seed_data.py +++ b/api/e2etests/e2e_seed_data.py @@ -4,13 +4,34 @@ from edge_api.identities.models import EdgeIdentity from environments.identities.models import Identity from environments.models import Environment +from environments.permissions.constants import ( + UPDATE_FEATURE_STATE, + VIEW_ENVIRONMENT, + VIEW_IDENTITIES, +) +from environments.permissions.models import UserEnvironmentPermission from organisations.models import Organisation, OrganisationRole, Subscription -from projects.models import Project -from users.models import FFAdminUser +from organisations.permissions.models import UserOrganisationPermission +from organisations.permissions.permissions import ( + CREATE_PROJECT, + MANAGE_USER_GROUPS, +) +from organisations.subscriptions.constants import SCALE_UP +from projects.models import Project, UserProjectPermission +from projects.permissions import ( + CREATE_ENVIRONMENT, + CREATE_FEATURE, + VIEW_AUDIT_LOG, + VIEW_PROJECT, +) +from users.models import FFAdminUser, UserPermissionGroup # Password used by all the test users PASSWORD = "Str0ngp4ssw0rd!" +PROJECT_PERMISSION_PROJECT = "My Test Project 5 Project Permission" +ENV_PERMISSION_PROJECT = "My Test Project 6 Env Permission" + def delete_user_and_its_organisations(user_email: str) -> None: user: FFAdminUser | None = FFAdminUser.objects.filter(email=user_email).first() @@ -25,6 +46,18 @@ def teardown() -> None: delete_user_and_its_organisations(user_email=settings.E2E_SIGNUP_USER) delete_user_and_its_organisations(user_email=settings.E2E_USER) delete_user_and_its_organisations(user_email=settings.E2E_CHANGE_EMAIL_USER) + delete_user_and_its_organisations( + user_email=settings.E2E_NON_ADMIN_USER_WITH_ORG_PERMISSIONS + ) + delete_user_and_its_organisations( + user_email=settings.E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS + ) + delete_user_and_its_organisations( + user_email=settings.E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS + ) + delete_user_and_its_organisations( + user_email=settings.E2E_NON_ADMIN_USER_WITH_A_ROLE + ) def seed_data() -> None: @@ -36,6 +69,44 @@ def seed_data() -> None: username=settings.E2E_USER, ) org_admin.add_organisation(organisation, OrganisationRole.ADMIN) + non_admin_user_with_org_permissions: FFAdminUser = FFAdminUser.objects.create_user( + email=settings.E2E_NON_ADMIN_USER_WITH_ORG_PERMISSIONS, + password=PASSWORD, + ) + non_admin_user_with_project_permissions: FFAdminUser = ( + FFAdminUser.objects.create_user( + email=settings.E2E_NON_ADMIN_USER_WITH_PROJECT_PERMISSIONS, + password=PASSWORD, + ) + ) + non_admin_user_with_env_permissions: FFAdminUser = FFAdminUser.objects.create_user( + email=settings.E2E_NON_ADMIN_USER_WITH_ENV_PERMISSIONS, + password=PASSWORD, + ) + non_admin_user_with_a_role: FFAdminUser = FFAdminUser.objects.create_user( + email=settings.E2E_NON_ADMIN_USER_WITH_A_ROLE, + password=PASSWORD, + ) + non_admin_user_with_org_permissions.add_organisation( + organisation, + ) + non_admin_user_with_project_permissions.add_organisation( + organisation, + ) + non_admin_user_with_env_permissions.add_organisation( + organisation, + ) + non_admin_user_with_a_role.add_organisation( + organisation, + ) + + # Add permissions to the non-admin user with org permissions + user_org_permission = UserOrganisationPermission.objects.create( + user=non_admin_user_with_org_permissions, organisation=organisation + ) + user_org_permission.add_permission(CREATE_PROJECT) + user_org_permission.add_permission(MANAGE_USER_GROUPS) + UserPermissionGroup.objects.create(name="TestGroup", organisation=organisation) # We add different projects and environments to give each e2e test its own isolated context. project_test_data = [ @@ -49,7 +120,17 @@ def seed_data() -> None: {"name": "My Test Project 2", "environments": ["Development"]}, {"name": "My Test Project 3", "environments": ["Development"]}, {"name": "My Test Project 4", "environments": ["Development"]}, + { + "name": PROJECT_PERMISSION_PROJECT, + "environments": ["Development"], + }, + {"name": ENV_PERMISSION_PROJECT, "environments": ["Development"]}, + {"name": "My Test Project 7 Role", "environments": ["Development"]}, ] + # Upgrade organisation seats + Subscription.objects.filter(organisation__in=org_admin.organisations.all()).update( + max_seats=8, plan=SCALE_UP, subscription_id="test_subscription_id" + ) # Create projects and environments projects = [] @@ -58,19 +139,59 @@ def seed_data() -> None: project = Project.objects.create( name=project_info["name"], organisation=organisation ) + if project_info["name"] == PROJECT_PERMISSION_PROJECT: + # Add permissions to the non-admin user with project permissions + user_proj_permission: UserProjectPermission = ( + UserProjectPermission.objects.create( + user=non_admin_user_with_project_permissions, project=project + ) + ) + [ + user_proj_permission.add_permission(permission_key) + for permission_key in [ + VIEW_PROJECT, + CREATE_ENVIRONMENT, + CREATE_FEATURE, + VIEW_AUDIT_LOG, + ] + ] projects.append(project) for env_name in project_info["environments"]: environment = Environment.objects.create(name=env_name, project=project) + + if project_info["name"] == ENV_PERMISSION_PROJECT: + # Add permissions to the non-admin user with env permissions + user_env_permission = UserEnvironmentPermission.objects.create( + user=non_admin_user_with_env_permissions, environment=environment + ) + user_env_proj_permission: UserProjectPermission = ( + UserProjectPermission.objects.create( + user=non_admin_user_with_env_permissions, project=project + ) + ) + user_env_proj_permission.add_permission(VIEW_PROJECT) + user_env_proj_permission.add_permission(CREATE_FEATURE) + [ + user_env_permission.add_permission(permission_key) + for permission_key in [ + VIEW_ENVIRONMENT, + UPDATE_FEATURE_STATE, + VIEW_IDENTITIES, + ] + ] environments.append(environment) - # We're only creating identities for 3 of the 5 environments because + # We're only creating identities for 6 of the 7 environments because # they are necessary for the environments created above and to keep # the e2e tests isolated." identities_test_data = [ {"identifier": settings.E2E_IDENTITY, "environment": environments[2]}, {"identifier": settings.E2E_IDENTITY, "environment": environments[3]}, {"identifier": settings.E2E_IDENTITY, "environment": environments[4]}, + {"identifier": settings.E2E_IDENTITY, "environment": environments[5]}, + {"identifier": settings.E2E_IDENTITY, "environment": environments[6]}, + {"identifier": settings.E2E_IDENTITY, "environment": environments[7]}, ] for identity_info in identities_test_data: @@ -82,8 +203,3 @@ def seed_data() -> None: EdgeIdentity(engine_identity).save() else: Identity.objects.create(**identity_info) - - # Upgrade organisation seats - Subscription.objects.filter(organisation__in=org_admin.organisations.all()).update( - max_seats=2 - ) diff --git a/api/e2etests/middleware.py b/api/e2etests/middleware.py new file mode 100644 index 000000000000..c57c3aeef4ec --- /dev/null +++ b/api/e2etests/middleware.py @@ -0,0 +1,16 @@ +from django.conf import settings + + +class E2ETestMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + request.is_e2e = False + if ( + request.META.get("HTTP_X_E2E_TEST_AUTH_TOKEN") + == settings.E2E_TEST_AUTH_TOKEN + ): + request.is_e2e = True + + return self.get_response(request) diff --git a/api/e2etests/permissions.py b/api/e2etests/permissions.py index 43a2b5f1a46f..c7d977261190 100644 --- a/api/e2etests/permissions.py +++ b/api/e2etests/permissions.py @@ -1,13 +1,8 @@ -import os - +from django.views import View from rest_framework.permissions import BasePermission +from rest_framework.request import Request class E2ETestPermission(BasePermission): - def has_permission(self, request, view): - if "E2E_TEST_AUTH_TOKEN" not in os.environ: - return False - return ( - request.META.get("HTTP_X_E2E_TEST_AUTH_TOKEN") - == os.environ["E2E_TEST_AUTH_TOKEN"] - ) + def has_permission(self, request: Request, view: View) -> bool: + return getattr(request, "is_e2e", False) is True diff --git a/api/edge_api/identities/models.py b/api/edge_api/identities/models.py index d67bcb5831c8..1e062ecd005e 100644 --- a/api/edge_api/identities/models.py +++ b/api/edge_api/identities/models.py @@ -167,15 +167,52 @@ def remove_feature_override(self, feature_state: FeatureStateModel) -> None: def save(self, user: FFAdminUser = None, master_api_key: MasterAPIKey = None): self.dynamo_wrapper.put_item(self.to_document()) - changes = self._get_changes() - if changes["feature_overrides"]: + changeset = self._get_changes() + self._update_feature_overrides( + changeset=changeset, + user=user, + master_api_key=master_api_key, + ) + self._reset_initial_state() + + def delete( + self, user: FFAdminUser = None, master_api_key: MasterAPIKey = None + ) -> None: + self.dynamo_wrapper.delete_item(self._engine_identity_model.composite_key) + self._engine_identity_model.identity_features.clear() + changeset = self._get_changes() + self._update_feature_overrides( + changeset=changeset, + user=user, + master_api_key=master_api_key, + ) + self._reset_initial_state() + + def synchronise_features(self, valid_feature_names: typing.Collection[str]) -> None: + identity_feature_names = { + fs.feature.name for fs in self._engine_identity_model.identity_features + } + if not identity_feature_names.issubset(valid_feature_names): + self._engine_identity_model.prune_features(list(valid_feature_names)) + sync_identity_document_features.delay(args=(str(self.identity_uuid),)) + + def to_document(self) -> dict: + return map_engine_identity_to_identity_document(self._engine_identity_model) + + def _update_feature_overrides( + self, + changeset: IdentityChangeset, + user: FFAdminUser = None, + master_api_key: MasterAPIKey = None, + ) -> None: + if changeset["feature_overrides"]: # TODO: would this be simpler if we put a wrapper around FeatureStateModel instead? generate_audit_log_records.delay( kwargs={ "environment_api_key": self.environment_api_key, "identifier": self.identifier, "user_id": getattr(user, "id", None), - "changes": changes, + "changes": changeset, "identity_uuid": str(self.identity_uuid), "master_api_key_id": getattr(master_api_key, "id", None), } @@ -183,23 +220,11 @@ def save(self, user: FFAdminUser = None, master_api_key: MasterAPIKey = None): update_flagsmith_environments_v2_identity_overrides.delay( kwargs={ "environment_api_key": self.environment_api_key, - "changes": changes, + "changes": changeset, "identity_uuid": str(self.identity_uuid), "identifier": self.identifier, } ) - self._reset_initial_state() - - def synchronise_features(self, valid_feature_names: typing.Collection[str]) -> None: - identity_feature_names = { - fs.feature.name for fs in self._engine_identity_model.identity_features - } - if not identity_feature_names.issubset(valid_feature_names): - self._engine_identity_model.prune_features(list(valid_feature_names)) - sync_identity_document_features.delay(args=(str(self.identity_uuid),)) - - def to_document(self) -> dict: - return map_engine_identity_to_identity_document(self._engine_identity_model) def _get_changes(self) -> IdentityChangeset: previous_instance = self._initial_state diff --git a/api/edge_api/identities/views.py b/api/edge_api/identities/views.py index c8ac3c0e3d50..9065324457c5 100644 --- a/api/edge_api/identities/views.py +++ b/api/edge_api/identities/views.py @@ -160,7 +160,11 @@ def get_environment_from_request(self): ) def perform_destroy(self, instance): - EdgeIdentity.dynamo_wrapper.delete_item(instance["composite_key"]) + edge_identity = EdgeIdentity.from_identity_document(instance) + edge_identity.delete( + user=self.request.user, + master_api_key=getattr(self.request, "master_api_key", None), + ) @swagger_auto_schema( responses={200: EdgeIdentityTraitsSerializer(many=True)}, @@ -281,7 +285,7 @@ def list(self, request, *args, **kwargs): def perform_destroy(self, instance): self.identity.remove_feature_override(instance) self.identity.save( - user=self.request.user.id, + user=self.request.user, master_api_key=getattr(self.request, "master_api_key", None), ) diff --git a/api/environments/identities/models.py b/api/environments/identities/models.py index 9fe99df13df9..60f36f8a4464 100644 --- a/api/environments/identities/models.py +++ b/api/environments/identities/models.py @@ -4,12 +4,12 @@ from django.db import models from django.db.models import Prefetch, Q from django.utils import timezone -from flag_engine.identities.traits.types import TraitValue from flag_engine.segments.evaluator import evaluate_identity_in_segment from environments.identities.managers import IdentityManager from environments.identities.traits.models import Trait from environments.models import Environment +from environments.sdk.types import SDKTraitData from features.models import FeatureState from features.multivariate.models import MultivariateFeatureStateValue from segments.models import Segment @@ -196,7 +196,11 @@ def get_all_user_traits(self): def __str__(self): return "Account %s" % self.identifier - def generate_traits(self, trait_data_items, persist=False): + def generate_traits( + self, + trait_data_items: list[SDKTraitData], + persist: bool = False, + ) -> list[Trait]: """ Given a list of trait data items, validated by TraitSerializerFull, generate a list of TraitModel objects for the given identity. @@ -232,7 +236,7 @@ def generate_traits(self, trait_data_items, persist=False): def update_traits( self, - trait_data_items: list[dict[str, TraitValue]], + trait_data_items: list[SDKTraitData], ) -> list[Trait]: """ Given a list of traits, update any that already exist and create any new ones. diff --git a/api/environments/identities/serializers.py b/api/environments/identities/serializers.py index bd4b1ffaed1f..6c0d9df9229e 100644 --- a/api/environments/identities/serializers.py +++ b/api/environments/identities/serializers.py @@ -59,6 +59,7 @@ class _TraitSerializer(serializers.Serializer): help_text="Can be of type string, boolean, float or integer." ) + identifier = serializers.CharField() flags = serializers.ListField(child=SDKFeatureStateSerializer()) traits = serializers.ListSerializer(child=_TraitSerializer()) diff --git a/api/environments/identities/traits/fields.py b/api/environments/identities/traits/fields.py index fd5d3d335f9c..ceadc03b69c2 100644 --- a/api/environments/identities/traits/fields.py +++ b/api/environments/identities/traits/fields.py @@ -1,4 +1,5 @@ import logging +from typing import Any from rest_framework import serializers @@ -6,6 +7,7 @@ ACCEPTED_TRAIT_VALUE_TYPES, TRAIT_STRING_VALUE_MAX_LENGTH, ) +from environments.sdk.types import SDKTraitValueData from features.value_types import STRING logger = logging.getLogger(__name__) @@ -16,7 +18,7 @@ class TraitValueField(serializers.Field): Custom field to extract the type of the field on deserialization. """ - def to_internal_value(self, data): + def to_internal_value(self, data: Any) -> SDKTraitValueData: data_type = type(data).__name__ if data_type not in ACCEPTED_TRAIT_VALUE_TYPES: @@ -28,7 +30,7 @@ def to_internal_value(self, data): ) return {"type": data_type, "value": data} - def to_representation(self, value): + def to_representation(self, value: Any) -> Any: return_value = value.get("value") if isinstance(value, dict) else value if return_value is None: diff --git a/api/environments/identities/views.py b/api/environments/identities/views.py index f403b8001dc1..bbf9e2ce566d 100644 --- a/api/environments/identities/views.py +++ b/api/environments/identities/views.py @@ -309,6 +309,7 @@ def _get_all_feature_states_for_user_response( serializer = serializer_class( { "flags": all_feature_states, + "identifier": identity.identifier, "traits": identity.identity_traits.all(), }, context=self.get_serializer_context(), diff --git a/api/environments/permissions/permissions.py b/api/environments/permissions/permissions.py index dc033d7249f1..020b7a065249 100644 --- a/api/environments/permissions/permissions.py +++ b/api/environments/permissions/permissions.py @@ -45,6 +45,8 @@ def has_permission(self, request, view): def has_object_permission(self, request, view, obj): if view.action == "clone": return request.user.has_project_permission(CREATE_ENVIRONMENT, obj.project) + elif view.action == "get_document": + return request.user.has_environment_permission(VIEW_ENVIRONMENT, obj) return request.user.is_environment_admin(obj) or view.action in [ "user_permissions" diff --git a/api/environments/sdk/serializers.py b/api/environments/sdk/serializers.py index 8ee5d254b94b..7029daa143b9 100644 --- a/api/environments/sdk/serializers.py +++ b/api/environments/sdk/serializers.py @@ -2,7 +2,6 @@ from collections import defaultdict from core.constants import BOOLEAN, FLOAT, INTEGER, STRING -from django.utils import timezone from rest_framework import serializers from environments.identities.models import Identity @@ -12,6 +11,12 @@ from environments.identities.traits.fields import TraitValueField from environments.identities.traits.models import Trait from environments.identities.traits.serializers import TraitSerializerBasic +from environments.sdk.services import ( + get_identified_transient_identity_and_traits, + get_persisted_identity_and_traits, + get_transient_identity_and_traits, +) +from environments.sdk.types import SDKTraitData from features.serializers import ( FeatureStateSerializerFull, SDKFeatureStateSerializer, @@ -125,7 +130,11 @@ def create(self, validated_data): class IdentifyWithTraitsSerializer( HideSensitiveFieldsSerializerMixin, serializers.Serializer ): - identifier = serializers.CharField(write_only=True, required=True) + identifier = serializers.CharField( + required=False, + allow_blank=True, + allow_null=True, + ) transient = serializers.BooleanField(write_only=True, default=False) traits = TraitSerializerBasic(required=False, many=True) flags = SDKFeatureStateSerializer(read_only=True, many=True) @@ -137,44 +146,46 @@ def save(self, **kwargs): Create the identity with the associated traits (optionally store traits if flag set on org) """ + identifier = self.validated_data.get("identifier") environment = self.context["environment"] - transient = self.validated_data["transient"] - trait_data_items = self.validated_data.get("traits", []) + sdk_trait_data: list[SDKTraitData] = self.validated_data.get("traits", []) - if transient: - identity = Identity( - created_date=timezone.now(), - identifier=self.validated_data["identifier"], + if not identifier: + # We have a fully transient identity that should never be persisted. + identity, traits = get_transient_identity_and_traits( environment=environment, + sdk_trait_data=sdk_trait_data, ) - trait_models = identity.generate_traits(trait_data_items, persist=False) - else: - identity, created = Identity.objects.get_or_create( - identifier=self.validated_data["identifier"], environment=environment + elif transient: + # Don't persist incoming data but load presently stored + # overrides and traits, if any. + identity, traits = get_identified_transient_identity_and_traits( + environment=environment, + identifier=identifier, + sdk_trait_data=sdk_trait_data, ) - if not created and environment.project.organisation.persist_trait_data: - # if this is an update and we're persisting traits, then we need to - # partially update any traits and return the full list - trait_models = identity.update_traits(trait_data_items) - else: - # generate traits for the identity and store them if configured to do so - trait_models = identity.generate_traits( - trait_data_items, - persist=environment.project.organisation.persist_trait_data, - ) + else: + # Persist the identity in accordance with individual trait transiency + # and persistence settings outside of request context. + identity, traits = get_persisted_identity_and_traits( + environment=environment, + identifier=identifier, + sdk_trait_data=sdk_trait_data, + ) all_feature_states = identity.get_all_feature_states( - traits=trait_models, + traits=traits, additional_filters=self.context.get("feature_states_additional_filters"), ) - identify_integrations(identity, all_feature_states, trait_models) + identify_integrations(identity, all_feature_states, traits) return { "identity": identity, - "traits": trait_models, + "identifier": identity.identifier, + "traits": traits, "flags": all_feature_states, } diff --git a/api/environments/sdk/services.py b/api/environments/sdk/services.py new file mode 100644 index 000000000000..500f35ac8b92 --- /dev/null +++ b/api/environments/sdk/services.py @@ -0,0 +1,120 @@ +import hashlib +import uuid +from itertools import chain +from operator import itemgetter +from typing import TypeAlias + +from django.utils import timezone + +from environments.identities.models import Identity +from environments.identities.traits.models import Trait +from environments.models import Environment +from environments.sdk.types import SDKTraitData + +IdentityAndTraits: TypeAlias = tuple[Identity, list[Trait]] + + +def get_transient_identity_and_traits( + environment: Environment, + sdk_trait_data: list[SDKTraitData], +) -> IdentityAndTraits: + """ + Get a transient `Identity` instance with a randomly generated identifier. + All traits are marked as transient. + """ + return ( + ( + identity := _get_transient_identity( + environment=environment, + identifier=get_transient_identifier(sdk_trait_data), + ) + ), + identity.generate_traits(_ensure_transient(sdk_trait_data), persist=False), + ) + + +def get_identified_transient_identity_and_traits( + environment: Environment, + identifier: str, + sdk_trait_data: list[SDKTraitData], +) -> IdentityAndTraits: + """ + Get a transient `Identity` instance. + If present in storage, it's a previously persisted identity with its traits, + combined with incoming traits provided to `sdk_trait_data` argument. + All traits constructed from `sdk_trait_data` are marked as transient. + """ + sdk_trait_data = _ensure_transient(sdk_trait_data) + if identity := Identity.objects.filter( + environment=environment, + identifier=identifier, + ).first(): + return identity, identity.update_traits(sdk_trait_data) + return ( + identity := _get_transient_identity( + environment=environment, + identifier=identifier, + ) + ), identity.generate_traits(sdk_trait_data, persist=False) + + +def get_persisted_identity_and_traits( + environment: Environment, + identifier: str, + sdk_trait_data: list[SDKTraitData], +) -> IdentityAndTraits: + """ + Retrieve a previously persisted `Identity` instance or persist a new one. + Traits are persisted based on the organisation-level setting or a + `"transient"` attribute provided with each individual trait. + """ + identity, created = Identity.objects.get_or_create( + environment=environment, + identifier=identifier, + ) + persist_trait_data = environment.project.organisation.persist_trait_data + if created: + return identity, identity.generate_traits( + sdk_trait_data, + persist=persist_trait_data, + ) + if persist_trait_data: + return identity, identity.update_traits(sdk_trait_data) + return identity, list( + { + trait.trait_key: trait + for trait in chain( + identity.identity_traits.all(), + identity.generate_traits(sdk_trait_data, persist=False), + ) + }.values() + ) + + +def get_transient_identifier(sdk_trait_data: list[SDKTraitData]) -> str: + if sdk_trait_data: + return hashlib.sha256( + "".join( + f'{trait["trait_key"]}{trait["trait_value"]["value"]}' + for trait in sorted(sdk_trait_data, key=itemgetter("trait_key")) + ).encode(), + usedforsecurity=False, + ).hexdigest() + return uuid.uuid4().hex + + +def _get_transient_identity( + environment: Environment, + identifier: str, +) -> Identity: + return Identity( + created_date=timezone.now(), + environment=environment, + identifier=identifier, + ) + + +def _ensure_transient(sdk_trait_data: list[SDKTraitData]) -> list[SDKTraitData]: + for sdk_trait_data_item in sdk_trait_data: + sdk_trait_data_item["transient"] = True + return sdk_trait_data diff --git a/api/environments/sdk/types.py b/api/environments/sdk/types.py new file mode 100644 index 000000000000..cf369f5a1907 --- /dev/null +++ b/api/environments/sdk/types.py @@ -0,0 +1,14 @@ +import typing + +from typing_extensions import NotRequired + + +class SDKTraitValueData(typing.TypedDict): + type: str + value: str + + +class SDKTraitData(typing.TypedDict): + trait_key: str + trait_value: SDKTraitValueData + transient: NotRequired[bool] diff --git a/api/environments/views.py b/api/environments/views.py index 4afc0ab52567..085fa6853c10 100644 --- a/api/environments/views.py +++ b/api/environments/views.py @@ -225,7 +225,10 @@ def user_permissions(self, request, *args, **kwargs): @swagger_auto_schema(responses={200: SDKEnvironmentDocumentModel}) @action(detail=True, methods=["GET"], url_path="document") def get_document(self, request, api_key: str): - return Response(Environment.get_environment_document(api_key)) + environment = ( + self.get_object() + ) # use get_object to ensure permissions check is performed + return Response(Environment.get_environment_document(environment.api_key)) @swagger_auto_schema(request_body=no_body, responses={202: ""}) @action(detail=True, methods=["POST"], url_path="enable-v2-versioning") diff --git a/api/features/feature_external_resources/models.py b/api/features/feature_external_resources/models.py index 6dcfc4d99a7f..8df2428e3158 100644 --- a/api/features/feature_external_resources/models.py +++ b/api/features/feature_external_resources/models.py @@ -1,3 +1,4 @@ +import json import logging from django.db import models @@ -11,9 +12,11 @@ from environments.models import Environment from features.models import Feature, FeatureState +from integrations.github.constants import GitHubEventType, GitHubTag from integrations.github.github import call_github_task +from integrations.github.models import GithubRepository from organisations.models import Organisation -from webhooks.webhooks import WebhookEventType +from projects.tags.models import Tag, TagType logger = logging.getLogger(__name__) @@ -24,6 +27,20 @@ class ResourceType(models.TextChoices): GITHUB_PR = "GITHUB_PR", "GitHub PR" +tag_by_type_and_state = { + ResourceType.GITHUB_ISSUE.value: { + "open": GitHubTag.ISSUE_OPEN.value, + "closed": GitHubTag.ISSUE_CLOSED.value, + }, + ResourceType.GITHUB_PR.value: { + "open": GitHubTag.PR_OPEN.value, + "closed": GitHubTag.PR_CLOSED.value, + "merged": GitHubTag.PR_MERGED.value, + "draft": GitHubTag.PR_DRAFT.value, + }, +} + + class FeatureExternalResource(LifecycleModelMixin, models.Model): url = models.URLField() type = models.CharField(max_length=20, choices=ResourceType.choices) @@ -49,12 +66,33 @@ class Meta: @hook(AFTER_SAVE) def execute_after_save_actions(self): + # Tag the feature with the external resource type + metadata = json.loads(self.metadata) if self.metadata else {} + state = metadata.get("state", "open") + # Add a comment to GitHub Issue/PR when feature is linked to the GH external resource + # and tag the feature with the corresponding tag if tagging is enabled if ( - Organisation.objects.prefetch_related("github_config") + github_configuration := Organisation.objects.prefetch_related( + "github_config" + ) .get(id=self.feature.project.organisation_id) .github_config.first() ): + github_repo = GithubRepository.objects.get( + github_configuration=github_configuration.id, + project=self.feature.project, + ) + if github_repo.tagging_enabled: + github_tag = Tag.objects.get( + label=tag_by_type_and_state[self.type][state], + project=self.feature.project, + is_system_tag=True, + type=TagType.GITHUB.value, + ) + self.feature.tags.add(github_tag) + self.feature.save() + feature_states: list[FeatureState] = [] environments = Environment.objects.filter( @@ -74,7 +112,7 @@ def execute_after_save_actions(self): call_github_task( organisation_id=self.feature.project.organisation_id, - type=WebhookEventType.FEATURE_EXTERNAL_RESOURCE_ADDED.value, + type=GitHubEventType.FEATURE_EXTERNAL_RESOURCE_ADDED.value, feature=self.feature, segment_name=None, url=None, @@ -92,7 +130,7 @@ def execute_before_save_actions(self) -> None: call_github_task( organisation_id=self.feature.project.organisation_id, - type=WebhookEventType.FEATURE_EXTERNAL_RESOURCE_REMOVED.value, + type=GitHubEventType.FEATURE_EXTERNAL_RESOURCE_REMOVED.value, feature=self.feature, segment_name=None, url=self.url, diff --git a/api/features/feature_external_resources/views.py b/api/features/feature_external_resources/views.py index c9636bba1132..002a8e5da89a 100644 --- a/api/features/feature_external_resources/views.py +++ b/api/features/feature_external_resources/views.py @@ -1,10 +1,16 @@ +import re + from django.shortcuts import get_object_or_404 from rest_framework import status, viewsets from rest_framework.response import Response from features.models import Feature from features.permissions import FeatureExternalResourcePermissions -from integrations.github.client import get_github_issue_pr_title_and_state +from integrations.github.client import ( + get_github_issue_pr_title_and_state, + label_github_issue_pr, +) +from integrations.github.models import GithubRepository from organisations.models import Organisation from .models import FeatureExternalResource @@ -48,14 +54,13 @@ def create(self, request, *args, **kwargs): ), ) - if not ( - ( - Organisation.objects.prefetch_related("github_config") - .get(id=feature.project.organisation_id) - .github_config.first() - ) - or not hasattr(feature.project, "github_project") - ): + github_configuration = ( + Organisation.objects.prefetch_related("github_config") + .get(id=feature.project.organisation_id) + .github_config.first() + ) + + if not github_configuration or not hasattr(feature.project, "github_project"): return Response( data={ "detail": "This Project doesn't have a valid GitHub integration configuration" @@ -63,7 +68,42 @@ def create(self, request, *args, **kwargs): content_type="application/json", status=status.HTTP_400_BAD_REQUEST, ) - return super().create(request, *args, **kwargs) + + # Get repository owner and name, and issue/PR number from the external resource URL + url = request.data.get("url") + if request.data.get("type") == "GITHUB_PR": + pattern = r"github.com/([^/]+)/([^/]+)/pull/(\d+)$" + elif request.data.get("type") == "GITHUB_ISSUE": + pattern = r"github.com/([^/]+)/([^/]+)/issues/(\d+)$" + else: + return Response( + data={"detail": "Incorrect GitHub type"}, + content_type="application/json", + status=status.HTTP_400_BAD_REQUEST, + ) + + url_match = re.search(pattern, url) + if url_match: + owner, repo, issue = url_match.groups() + if GithubRepository.objects.get( + github_configuration=github_configuration, + repository_owner=owner, + repository_name=repo, + ).tagging_enabled: + label_github_issue_pr( + installation_id=github_configuration.installation_id, + owner=owner, + repo=repo, + issue=issue, + ) + response = super().create(request, *args, **kwargs) + return response + else: + return Response( + data={"detail": "Invalid GitHub Issue/PR URL"}, + content_type="application/json", + status=status.HTTP_400_BAD_REQUEST, + ) def perform_update(self, serializer): external_resource_id = int(self.kwargs["pk"]) diff --git a/api/features/models.py b/api/features/models.py index 6a80c5c90251..1493a54c4849 100644 --- a/api/features/models.py +++ b/api/features/models.py @@ -74,6 +74,7 @@ STRING, ) from features.versioning.models import EnvironmentFeatureVersion +from integrations.github.constants import GitHubEventType from metadata.models import Metadata from projects.models import Project from projects.tags.models import Tag @@ -139,7 +140,6 @@ class Meta: @hook(AFTER_SAVE) def create_github_comment(self) -> None: from integrations.github.github import call_github_task - from webhooks.webhooks import WebhookEventType if ( self.external_resources.exists() @@ -150,7 +150,7 @@ def create_github_comment(self) -> None: call_github_task( organisation_id=self.project.organisation_id, - type=WebhookEventType.FLAG_DELETED.value, + type=GitHubEventType.FLAG_DELETED.value, feature=self, segment_name=None, url=None, @@ -406,7 +406,6 @@ def _get_environment(self) -> "Environment": @hook(AFTER_DELETE) def create_github_comment(self) -> None: from integrations.github.github import call_github_task - from webhooks.webhooks import WebhookEventType if ( self.feature.external_resources.exists() @@ -416,7 +415,7 @@ def create_github_comment(self) -> None: call_github_task( self.feature.project.organisation_id, - WebhookEventType.SEGMENT_OVERRIDE_DELETED.value, + GitHubEventType.SEGMENT_OVERRIDE_DELETED.value, self.feature, self.segment.name, None, diff --git a/api/features/serializers.py b/api/features/serializers.py index a2a297238637..5d4de0005517 100644 --- a/api/features/serializers.py +++ b/api/features/serializers.py @@ -19,6 +19,7 @@ from environments.sdk.serializers_mixins import ( HideSensitiveFieldsSerializerMixin, ) +from integrations.github.constants import GitHubEventType from integrations.github.github import call_github_task from metadata.serializers import MetadataSerializer, SerializerWithMetadata from projects.models import Project @@ -30,7 +31,6 @@ from util.drf_writable_nested.serializers import ( DeleteBeforeUpdateWritableNestedModelSerializer, ) -from webhooks.webhooks import WebhookEventType from .constants import INTERSECTION, UNION from .feature_segments.serializers import ( @@ -478,7 +478,7 @@ def save(self, **kwargs): call_github_task( organisation_id=feature_state.feature.project.organisation_id, - type=WebhookEventType.FLAG_UPDATED.value, + type=GitHubEventType.FLAG_UPDATED.value, feature=feature_state.feature, segment_name=None, url=None, diff --git a/api/features/versioning/serializers.py b/api/features/versioning/serializers.py index ec21cdfae5ae..90295f222b8a 100644 --- a/api/features/versioning/serializers.py +++ b/api/features/versioning/serializers.py @@ -8,10 +8,10 @@ CustomCreateSegmentOverrideFeatureStateSerializer, ) from features.versioning.models import EnvironmentFeatureVersion +from integrations.github.constants import GitHubEventType from integrations.github.github import call_github_task from segments.models import Segment from users.models import FFAdminUser -from webhooks.webhooks import WebhookEventType class CustomEnvironmentFeatureVersionFeatureStateSerializer( @@ -36,7 +36,7 @@ def save(self, **kwargs): call_github_task( organisation_id=feature_state.environment.project.organisation_id, - type=WebhookEventType.FLAG_UPDATED.value, + type=GitHubEventType.FLAG_UPDATED.value, feature=feature_state.feature, segment_name=None, url=None, diff --git a/api/features/views.py b/api/features/views.py index ec0aad480f73..b276bd8f4b98 100644 --- a/api/features/views.py +++ b/api/features/views.py @@ -1,5 +1,6 @@ import logging import typing +from datetime import timedelta from functools import reduce from app_analytics.analytics_db_service import get_feature_evaluation_data @@ -9,6 +10,7 @@ from django.conf import settings from django.core.cache import caches from django.db.models import Max, Q, QuerySet +from django.utils import timezone from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_page from drf_yasg import openapi @@ -352,8 +354,15 @@ def get_influx_data(self, request, pk, project_pk): query_serializer = GetInfluxDataQuerySerializer(data=request.query_params) query_serializer.is_valid(raise_exception=True) + period = query_serializer.data["period"] + now = timezone.now() + if period.endswith("h"): + date_start = now - timedelta(hours=int(period[:-1])) + elif period.endswith("d"): + date_start = now - timedelta(days=int(period[:-1])) + else: + raise ValidationError("Malformed period supplied") - date_start = f"-{query_serializer.data['period']}" events_list = get_multiple_event_list_for_feature( feature_name=feature.name, date_start=date_start, diff --git a/api/integrations/github/client.py b/api/integrations/github/client.py index b4307fd736f9..4d4293b30a48 100644 --- a/api/integrations/github/client.py +++ b/api/integrations/github/client.py @@ -1,3 +1,4 @@ +import json import logging from enum import Enum from typing import Any @@ -5,11 +6,15 @@ import requests from django.conf import settings from github import Auth, Github +from requests.exceptions import HTTPError from integrations.github.constants import ( GITHUB_API_CALLS_TIMEOUT, GITHUB_API_URL, GITHUB_API_VERSION, + GITHUB_FLAGSMITH_LABEL, + GITHUB_FLAGSMITH_LABEL_COLOR, + GITHUB_FLAGSMITH_LABEL_DESCRIPTION, ) from integrations.github.dataclasses import ( IssueQueryParams, @@ -159,6 +164,9 @@ def fetch_search_github_resource( "id": i["id"], "title": i["title"], "number": i["number"], + "state": i["state"], + "merged": i.get("merged", False), + "draft": i.get("draft", False), } for i in json_response["items"] ] @@ -244,3 +252,45 @@ def fetch_github_repo_contributors( ] return build_paginated_response(results, response) + + +def create_flagsmith_flag_label( + installation_id: str, owner: str, repo: str +) -> dict[str, Any]: + # Create "Flagsmith Flag" label in linked repo + url = f"{GITHUB_API_URL}repos/{owner}/{repo}/labels" + headers = build_request_headers(installation_id) + payload = { + "name": GITHUB_FLAGSMITH_LABEL, + "color": GITHUB_FLAGSMITH_LABEL_COLOR, + "description": GITHUB_FLAGSMITH_LABEL_DESCRIPTION, + } + try: + response = requests.post( + url, json=payload, headers=headers, timeout=GITHUB_API_CALLS_TIMEOUT + ) + response.raise_for_status() + return response.json() + + except HTTPError: + response_content = response.content.decode("utf-8") + error_data = json.loads(response_content) + if any( + error["code"] == "already_exists" for error in error_data.get("errors", []) + ): + logger.warning("Label already exists") + return {"message": "Label already exists"}, 200 + + +def label_github_issue_pr( + installation_id: str, owner: str, repo: str, issue: str +) -> dict[str, Any]: + # Label linked GitHub Issue or PR with the "Flagsmith Flag" label + url = f"{GITHUB_API_URL}repos/{owner}/{repo}/issues/{issue}/labels" + headers = build_request_headers(installation_id) + payload = [GITHUB_FLAGSMITH_LABEL] + response = requests.post( + url, json=payload, headers=headers, timeout=GITHUB_API_CALLS_TIMEOUT + ) + response.raise_for_status() + return response.json() diff --git a/api/integrations/github/constants.py b/api/integrations/github/constants.py index 929ea690b87e..897b3a2c6725 100644 --- a/api/integrations/github/constants.py +++ b/api/integrations/github/constants.py @@ -1,3 +1,5 @@ +from enum import Enum + GITHUB_API_URL = "https://api.github.com/" GITHUB_API_VERSION = "2022-11-28" @@ -7,11 +9,49 @@ | :--- | :----- | :------ | :------ |\n""" FEATURE_TABLE_ROW = "| [%s](%s) | %s | %s | %s |\n" LINK_SEGMENT_TITLE = "Segment `%s` values:\n" -UNLINKED_FEATURE_TEXT = "### The feature flag `%s` was unlinked from the issue/PR" -UPDATED_FEATURE_TEXT = "Flagsmith Feature `%s` has been updated:\n" -DELETED_FEATURE_TEXT = "### The Feature Flag `%s` was deleted" +UNLINKED_FEATURE_TEXT = "**The feature flag `%s` was unlinked from the issue/PR**" +UPDATED_FEATURE_TEXT = "**Flagsmith Feature `%s` has been updated:**\n" +FEATURE_UPDATED_FROM_GHA_TEXT = ( + "**Flagsmith Feature `%s` has been updated from GHA:**\n" +) +DELETED_FEATURE_TEXT = "**The Feature Flag `%s` was deleted**" DELETED_SEGMENT_OVERRIDE_TEXT = ( - "### The Segment Override `%s` for Feature Flag `%s` was deleted" + "**The Segment Override `%s` for Feature Flag `%s` was deleted**" ) FEATURE_ENVIRONMENT_URL = "%s/project/%s/environment/%s/features?feature=%s&tab=%s" GITHUB_API_CALLS_TIMEOUT = 10 + +GITHUB_TAG_COLOR = "#838992" +GITHUB_FLAGSMITH_LABEL = "Flagsmith Flag" +GITHUB_FLAGSMITH_LABEL_DESCRIPTION = ( + "This GitHub Issue/PR is linked to a Flagsmith Feature Flag" +) +GITHUB_FLAGSMITH_LABEL_COLOR = "6633FF" + + +class GitHubEventType(Enum): + FLAG_UPDATED = "FLAG_UPDATED" + FLAG_DELETED = "FLAG_DELETED" + FLAG_UPDATED_FROM_GHA = "FLAG_UPDATED_FROM_GHA" + FEATURE_EXTERNAL_RESOURCE_ADDED = "FEATURE_EXTERNAL_RESOURCE_ADDED" + FEATURE_EXTERNAL_RESOURCE_REMOVED = "FEATURE_EXTERNAL_RESOURCE_REMOVED" + SEGMENT_OVERRIDE_DELETED = "SEGMENT_OVERRIDE_DELETED" + + +class GitHubTag(Enum): + PR_OPEN = "PR Open" + PR_MERGED = "PR Merged" + PR_CLOSED = "PR Closed" + PR_DRAFT = "PR Draft" + ISSUE_OPEN = "Issue Open" + ISSUE_CLOSED = "Issue Closed" + + +github_tag_description = { + GitHubTag.PR_OPEN.value: "This feature has a linked PR open", + GitHubTag.PR_MERGED.value: "This feature has a linked PR merged", + GitHubTag.PR_CLOSED.value: "This feature has a linked PR closed", + GitHubTag.PR_DRAFT.value: "This feature has a linked PR draft", + GitHubTag.ISSUE_OPEN.value: "This feature has a linked issue open", + GitHubTag.ISSUE_CLOSED.value: "This feature has a linked issue closed", +} diff --git a/api/integrations/github/github.py b/api/integrations/github/github.py index ddbbdcc04698..6b3573903dde 100644 --- a/api/integrations/github/github.py +++ b/api/integrations/github/github.py @@ -4,6 +4,7 @@ from typing import Any from core.helpers import get_current_site_url +from django.db.models import Q from django.utils.formats import get_format from features.models import Feature, FeatureState, FeatureStateValue @@ -17,14 +18,69 @@ LINK_SEGMENT_TITLE, UNLINKED_FEATURE_TEXT, UPDATED_FEATURE_TEXT, + GitHubEventType, + GitHubTag, ) from integrations.github.dataclasses import GithubData from integrations.github.models import GithubConfiguration from integrations.github.tasks import call_github_app_webhook_for_feature_state -from webhooks.webhooks import WebhookEventType +from projects.tags.models import Tag, TagType logger = logging.getLogger(__name__) +tag_by_event_type = { + "pull_request": { + "closed": GitHubTag.PR_CLOSED.value, + "converted_to_draft": GitHubTag.PR_DRAFT.value, + "opened": GitHubTag.PR_OPEN.value, + "reopened": GitHubTag.PR_OPEN.value, + "ready_for_review": GitHubTag.PR_OPEN.value, + "merged": GitHubTag.PR_MERGED.value, + }, + "issues": { + "closed": GitHubTag.ISSUE_CLOSED.value, + "opened": GitHubTag.ISSUE_OPEN.value, + "reopened": GitHubTag.ISSUE_OPEN.value, + }, +} + + +def tag_feature_per_github_event( + event_type: str, action: str, metadata: dict[str, Any] +) -> None: + + # Get Feature with external resource of type GITHUB and url matching the resource URL + feature = Feature.objects.filter( + Q(external_resources__type="GITHUB_PR") + | Q(external_resources__type="GITHUB_ISSUE"), + external_resources__url=metadata.get("html_url"), + ).first() + + if feature: + if ( + event_type == "pull_request" + and action == "closed" + and metadata.get("merged") + ): + action = "merged" + # Get corresponding project Tag to tag the feature + github_tag = Tag.objects.get( + label=tag_by_event_type[event_type][action], + project=feature.project_id, + is_system_tag=True, + type=TagType.GITHUB.value, + ) + tag_label_pattern = "Issue" if event_type == "issues" else "PR" + # Remove all GITHUB tags from the feature which label starts with issue or pr depending on event_type + feature.tags.remove( + *feature.tags.filter( + Q(type=TagType.GITHUB.value) & Q(label__startswith=tag_label_pattern) + ) + ) + + feature.tags.add(github_tag) + feature.save() + def handle_installation_deleted(payload: dict[str, Any]) -> None: installation_id = payload.get("installation", {}).get("id") @@ -42,6 +98,10 @@ def handle_installation_deleted(payload: dict[str, Any]) -> None: def handle_github_webhook_event(event_type: str, payload: dict[str, Any]) -> None: if event_type == "installation" and payload.get("action") == "deleted": handle_installation_deleted(payload) + elif event_type in tag_by_event_type: + action = str(payload.get("action")) + metadata = payload.get("issue", {}) or payload.get("pull_request", {}) + tag_feature_per_github_event(event_type, action, metadata) def generate_body_comment( @@ -53,13 +113,12 @@ def generate_body_comment( segment_name: str | None = None, ) -> str: - is_update = event_type == WebhookEventType.FLAG_UPDATED.value - is_removed = event_type == WebhookEventType.FEATURE_EXTERNAL_RESOURCE_REMOVED.value + is_removed = event_type == GitHubEventType.FEATURE_EXTERNAL_RESOURCE_REMOVED.value is_segment_override_deleted = ( - event_type == WebhookEventType.SEGMENT_OVERRIDE_DELETED.value + event_type == GitHubEventType.SEGMENT_OVERRIDE_DELETED.value ) - if event_type == WebhookEventType.FLAG_DELETED.value: + if event_type == GitHubEventType.FLAG_DELETED.value: return DELETED_FEATURE_TEXT % (name) if is_removed: @@ -68,7 +127,12 @@ def generate_body_comment( if is_segment_override_deleted and segment_name is not None: return DELETED_SEGMENT_OVERRIDE_TEXT % (segment_name, name) - result = UPDATED_FEATURE_TEXT % (name) if is_update else LINK_FEATURE_TITLE % (name) + result = "" + if event_type == GitHubEventType.FLAG_UPDATED.value: + result = UPDATED_FEATURE_TEXT % (name) + else: + result = LINK_FEATURE_TITLE % (name) + last_segment_name = "" if len(feature_states) > 0 and not feature_states[0].get("segment_name"): result += FEATURE_TABLE_HEADER @@ -125,7 +189,7 @@ def generate_data( if check_not_none(feature_state_value): feature_env_data["feature_state_value"] = feature_state_value - if type is not WebhookEventType.FEATURE_EXTERNAL_RESOURCE_REMOVED.value: + if type is not GitHubEventType.FEATURE_EXTERNAL_RESOURCE_REMOVED.value: feature_env_data["environment_name"] = feature_state.environment.name feature_env_data["enabled"] = feature_state.enabled feature_env_data["last_updated"] = feature_state.updated_at.strftime( @@ -150,7 +214,7 @@ def generate_data( type=type, url=( url - if type == WebhookEventType.FEATURE_EXTERNAL_RESOURCE_REMOVED.value + if type == GitHubEventType.FEATURE_EXTERNAL_RESOURCE_REMOVED.value else None ), feature_states=feature_states_list if feature_states else None, diff --git a/api/integrations/github/migrations/0004_githubrepository_tagging_enabled.py b/api/integrations/github/migrations/0004_githubrepository_tagging_enabled.py new file mode 100644 index 000000000000..a3ded07330d2 --- /dev/null +++ b/api/integrations/github/migrations/0004_githubrepository_tagging_enabled.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.25 on 2024-08-07 17:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('github', '0003_auto_20240528_0640'), + ] + + operations = [ + migrations.AddField( + model_name='githubrepository', + name='tagging_enabled', + field=models.BooleanField(default=False), + ), + ] diff --git a/api/integrations/github/models.py b/api/integrations/github/models.py index 532e0760b9ff..546b009d601e 100644 --- a/api/integrations/github/models.py +++ b/api/integrations/github/models.py @@ -3,8 +3,14 @@ from core.models import SoftDeleteExportableModel from django.db import models -from django_lifecycle import BEFORE_DELETE, LifecycleModelMixin, hook +from django_lifecycle import ( + AFTER_CREATE, + BEFORE_DELETE, + LifecycleModelMixin, + hook, +) +from integrations.github.constants import GITHUB_TAG_COLOR from organisations.models import Organisation logger: logging.Logger = logging.getLogger(name=__name__) @@ -48,6 +54,7 @@ class GithubRepository(LifecycleModelMixin, SoftDeleteExportableModel): null=False, on_delete=models.CASCADE, ) + tagging_enabled = models.BooleanField(default=False) class Meta: constraints = [ @@ -84,3 +91,23 @@ def delete_feature_external_resources( # Filter by url containing the repository owner and name url__regex=pattern, ).delete() + + @hook(AFTER_CREATE) + def create_github_tags( + self, + ) -> None: + from integrations.github.constants import ( + GitHubTag, + github_tag_description, + ) + from projects.tags.models import Tag, TagType + + for tag_label in GitHubTag: + tag, created = Tag.objects.get_or_create( + color=GITHUB_TAG_COLOR, + description=github_tag_description[tag_label.value], + label=tag_label.value, + project=self.project, + is_system_tag=True, + type=TagType.GITHUB.value, + ) diff --git a/api/integrations/github/serializers.py b/api/integrations/github/serializers.py index b3dc96c5263e..9d1cf3a81635 100644 --- a/api/integrations/github/serializers.py +++ b/api/integrations/github/serializers.py @@ -27,6 +27,7 @@ class Meta: "project", "repository_owner", "repository_name", + "tagging_enabled", ) read_only_fields = ( "id", diff --git a/api/integrations/github/tasks.py b/api/integrations/github/tasks.py index 8e94c9007b5d..970067daf0c4 100644 --- a/api/integrations/github/tasks.py +++ b/api/integrations/github/tasks.py @@ -6,8 +6,8 @@ from features.models import Feature from integrations.github.client import post_comment_to_github +from integrations.github.constants import GitHubEventType from integrations.github.dataclasses import CallGithubData -from webhooks.webhooks import WebhookEventType logger = logging.getLogger(__name__) @@ -29,8 +29,9 @@ def send_post_request(data: CallGithubData) -> None: ) if ( - event_type == WebhookEventType.FLAG_UPDATED.value - or event_type == WebhookEventType.FLAG_DELETED.value + event_type == GitHubEventType.FLAG_UPDATED.value + or event_type == GitHubEventType.FLAG_DELETED.value + or event_type == GitHubEventType.FLAG_UPDATED_FROM_GHA.value ): for resource in data.feature_external_resources: url = resource.get("url") @@ -40,7 +41,7 @@ def send_post_request(data: CallGithubData) -> None: installation_id, split_url[1], split_url[2], split_url[4], body ) - elif event_type == WebhookEventType.FEATURE_EXTERNAL_RESOURCE_REMOVED.value: + elif event_type == GitHubEventType.FEATURE_EXTERNAL_RESOURCE_REMOVED.value: url = data.github_data.url pathname = urlparse(url).path split_url = pathname.split("/") @@ -80,8 +81,8 @@ def generate_feature_external_resources( ] if ( - github_event_data.type == WebhookEventType.FLAG_DELETED.value - or github_event_data.type == WebhookEventType.SEGMENT_OVERRIDE_DELETED.value + github_event_data.type == GitHubEventType.FLAG_DELETED.value + or github_event_data.type == GitHubEventType.SEGMENT_OVERRIDE_DELETED.value ): feature_external_resources = generate_feature_external_resources( list( @@ -100,7 +101,7 @@ def generate_feature_external_resources( if ( github_event_data.type - == WebhookEventType.FEATURE_EXTERNAL_RESOURCE_REMOVED.value + == GitHubEventType.FEATURE_EXTERNAL_RESOURCE_REMOVED.value ): data = CallGithubData( event_type=github_event_data.type, diff --git a/api/integrations/github/views.py b/api/integrations/github/views.py index 4cc2f3efea57..8d48a17dfa84 100644 --- a/api/integrations/github/views.py +++ b/api/integrations/github/views.py @@ -15,13 +15,17 @@ from integrations.github.client import ( ResourceType, + create_flagsmith_flag_label, delete_github_installation, fetch_github_repo_contributors, fetch_github_repositories, fetch_search_github_resource, ) from integrations.github.exceptions import DuplicateGitHubIntegration -from integrations.github.github import handle_github_webhook_event +from integrations.github.github import ( + handle_github_webhook_event, + tag_by_event_type, +) from integrations.github.helpers import github_webhook_payload_is_valid from integrations.github.models import GithubConfiguration, GithubRepository from integrations.github.permissions import HasPermissionToGithubConfiguration @@ -136,10 +140,20 @@ def get_queryset(self): except ValueError: raise ValidationError({"github_pk": ["Must be an integer"]}) - def create(self, request, *args, **kwargs): + def create(self, request, *args, **kwargs) -> Response | None: try: - return super().create(request, *args, **kwargs) + response: Response = super().create(request, *args, **kwargs) + github_configuration: GithubConfiguration = GithubConfiguration.objects.get( + id=self.kwargs["github_pk"] + ) + if request.data.get("tagging_enabled", False): + create_flagsmith_flag_label( + installation_id=github_configuration.installation_id, + owner=request.data.get("repository_owner"), + repo=request.data.get("repository_name"), + ) + return response except IntegrityError as e: if re.search( @@ -150,6 +164,19 @@ def create(self, request, *args, **kwargs): detail="Duplication error. The GitHub repository already linked" ) + def update(self, request, *args, **kwargs) -> Response | None: + response: Response = super().update(request, *args, **kwargs) + github_configuration: GithubConfiguration = GithubConfiguration.objects.get( + id=self.kwargs["github_pk"] + ) + if request.data.get("tagging_enabled", False): + create_flagsmith_flag_label( + installation_id=github_configuration.installation_id, + owner=request.data.get("repository_owner"), + repo=request.data.get("repository_name"), + ) + return response + @api_view(["GET"]) @permission_classes([IsAuthenticated, HasPermissionToGithubConfiguration]) @@ -250,7 +277,7 @@ def github_webhook(request) -> Response: payload_body=payload, secret_token=secret, signature_header=signature ): data = json.loads(payload.decode("utf-8")) - if github_event == "installation": + if github_event == "installation" or github_event in tag_by_event_type: handle_github_webhook_event(event_type=github_event, payload=data) return Response({"detail": "Event processed"}, status=200) else: diff --git a/api/organisations/subscription_info_cache.py b/api/organisations/subscription_info_cache.py index 9fc0819b1d84..a5ec5a017e94 100644 --- a/api/organisations/subscription_info_cache.py +++ b/api/organisations/subscription_info_cache.py @@ -1,7 +1,9 @@ import typing +from datetime import timedelta from app_analytics.influxdb_wrapper import get_top_organisations from django.conf import settings +from django.utils import timezone from .chargebee import get_subscription_metadata_from_id from .models import Organisation, OrganisationSubscriptionInformationCache @@ -70,9 +72,19 @@ def _update_caches_with_influx_data( if not settings.INFLUXDB_TOKEN: return - for date_start, limit in (("-30d", ""), ("-7d", ""), ("-24h", "100")): - key = f"api_calls_{date_start[1:]}" + for _date_start, limit in (("-30d", ""), ("-7d", ""), ("-24h", "100")): + key = f"api_calls_{_date_start[1:]}" + + now = timezone.now() + if _date_start.endswith("d"): + date_start = now - timedelta(days=int(_date_start[1:-1])) + elif _date_start.endswith("h"): + date_start = now - timedelta(hours=int(_date_start[1:-1])) + else: + assert False, "Expecting either days (d) or hours (h)" # pragma: no cover + org_calls = get_top_organisations(date_start, limit) + for org_id, calls in org_calls.items(): subscription_info_cache = organisation_info_cache_dict.get(org_id) if not subscription_info_cache: diff --git a/api/organisations/task_helpers.py b/api/organisations/task_helpers.py index 509811cbf622..65d382358615 100644 --- a/api/organisations/task_helpers.py +++ b/api/organisations/task_helpers.py @@ -69,6 +69,7 @@ def _send_api_usage_notification( context = { "organisation": organisation, "matched_threshold": matched_threshold, + "grace_period": not hasattr(organisation, "breached_grace_period"), } send_mail( @@ -118,10 +119,9 @@ def handle_api_usage_notification_for_organisation(organisation: Organisation) - month_delta = relativedelta(now, billing_starts_at).months period_starts_at = relativedelta(months=month_delta) + billing_starts_at - days = relativedelta(now, period_starts_at).days allowed_api_calls = subscription_cache.allowed_30d_api_calls - api_usage = get_current_api_usage(organisation.id, f"-{days}d") + api_usage = get_current_api_usage(organisation.id, period_starts_at) # For some reason the allowed API calls is set to 0 so default to the max free plan. allowed_api_calls = allowed_api_calls or MAX_API_CALLS_IN_FREE_PLAN @@ -140,6 +140,7 @@ def handle_api_usage_notification_for_organisation(organisation: Organisation) - return if OrganisationAPIUsageNotification.objects.filter( + organisation_id=organisation.id, notified_at__gt=period_starts_at, percent_usage__gte=matched_threshold, ).exists(): diff --git a/api/organisations/tasks.py b/api/organisations/tasks.py index 0ebdc1f5d6ca..f03a9fba3e81 100644 --- a/api/organisations/tasks.py +++ b/api/organisations/tasks.py @@ -192,7 +192,7 @@ def charge_for_api_call_count_overages(): continue subscription_cache = organisation.subscription_information_cache - api_usage = get_current_api_usage(organisation.id, "30d") + api_usage = get_current_api_usage(organisation.id) # Grace period for organisations < 200% of usage. if api_usage / subscription_cache.allowed_30d_api_calls < 2.0: @@ -306,7 +306,7 @@ def restrict_use_due_to_api_limit_grace_period_over() -> None: OrganisationBreachedGracePeriod.objects.get_or_create(organisation=organisation) subscription_cache = organisation.subscription_information_cache - api_usage = get_current_api_usage(organisation.id, "30d") + api_usage = get_current_api_usage(organisation.id) if api_usage / subscription_cache.allowed_30d_api_calls < 1.0: logger.info( f"API use for organisation {organisation.id} has fallen to below limit, so not restricting use." diff --git a/api/organisations/templates/organisations/api_usage_notification.html b/api/organisations/templates/organisations/api_usage_notification.html index 19b14d13b1b1..74deaac7fc7d 100644 --- a/api/organisations/templates/organisations/api_usage_notification.html +++ b/api/organisations/templates/organisations/api_usage_notification.html @@ -19,8 +19,8 @@ for overages after our first grace period of 30 days. {% else %} Please note that once 100% use has been breached, the serving of feature flags and admin access may - be disabled after a 7-day grace period. Please reach out to support@flagsmith.com in order to upgrade - your account. + be disabled{% if grace_period %} after a 7-day grace period{% endif %}. Please reach out to + support@flagsmith.com in order to upgrade your account. {% endif %} diff --git a/api/organisations/templates/organisations/api_usage_notification.txt b/api/organisations/templates/organisations/api_usage_notification.txt index f5646e87abcd..e02fe3f967c5 100644 --- a/api/organisations/templates/organisations/api_usage_notification.txt +++ b/api/organisations/templates/organisations/api_usage_notification.txt @@ -8,8 +8,8 @@ If this is expected, no action is required. If you are expecting to go over, you limits by reaching out to support@flagsmith.com. We will automatically charge for overages after our first grace period of 30 days. {% else %} -Please note that once 100% use has been breached, the serving of feature flags and admin access may be disabled after a -7-day grace period. Please reach out to support@flagsmith.com in order to upgrade your account. +Please note that once 100% use has been breached, the serving of feature flags and admin access may be disabled{% if grace_period %} +after a 7-day grace period{% endif %}. Please reach out to support@flagsmith.com in order to upgrade your account. {% endif %} Thank you! diff --git a/api/organisations/templates/organisations/api_usage_notification_limit.html b/api/organisations/templates/organisations/api_usage_notification_limit.html index 15f4bf9ea3e5..fa79e1196f96 100644 --- a/api/organisations/templates/organisations/api_usage_notification_limit.html +++ b/api/organisations/templates/organisations/api_usage_notification_limit.html @@ -18,8 +18,8 @@ more information. You can reach out to support@flagsmith.com if you’d like to take advantage of better contracted rates. {% else %} - Please note that the serving of feature flags and admin access will be disabled after a 7 day grace - period until the next subscription period. If you’d like to continue service you can upgrade your + Please note that the serving of feature flags and admin access will be disabled{% if grace_period %} after a 7 day grace + period{% endif %} until the next subscription period. If you’d like to continue service you can upgrade your organisation’s account (see pricing page). {% endif %} diff --git a/api/organisations/templates/organisations/api_usage_notification_limit.txt b/api/organisations/templates/organisations/api_usage_notification_limit.txt index faf301b74c22..65c8d1eaa48c 100644 --- a/api/organisations/templates/organisations/api_usage_notification_limit.txt +++ b/api/organisations/templates/organisations/api_usage_notification_limit.txt @@ -7,9 +7,9 @@ has reached {{ matched_threshold }}% of your API usage within the current subscr We will charge for overages after our first grace period of 30 days. Please see the pricing page for more information. You can reach out to support@flagsmith.com if you’d like to take advantage of better contracted rates. {% else %} -Please note that the serving of feature flags and admin access will be disabled after a 7 day grace period until the -next subscription period. If you’d like to continue service you can upgrade your organisation’s account (see pricing -page). +Please note that the serving of feature flags and admin access will be disabled{% if grace_period %} after a 7 day +grace period{% endif %} until the next subscription period. If you’d like to continue service you can upgrade your +organisation’s account (see pricing page). {% endif %} Thank you! diff --git a/api/permissions/permission_service.py b/api/permissions/permission_service.py index aa938401f228..9cff1f13e486 100644 --- a/api/permissions/permission_service.py +++ b/api/permissions/permission_service.py @@ -134,12 +134,13 @@ def get_permitted_environments_for_user( return queryset.prefetch_related("metadata") return queryset - base_filter = get_base_permission_filter( + environment_ids_from_base_filter = get_object_id_from_base_permission_filter( user, Environment, permission_key, tag_ids=tag_ids ) - filter_ = base_filter & Q(project=project) + queryset = Environment.objects.filter( + id__in=environment_ids_from_base_filter, project=project + ) - queryset = Environment.objects.filter(filter_) if prefetch_metadata: queryset = queryset.prefetch_related("metadata") @@ -148,7 +149,7 @@ def get_permitted_environments_for_user( # the select parameters. This leads to an N+1 query for # lists of environments when description is included, as # each environment object re-queries the DB seperately. - return queryset.distinct().defer("description") + return queryset.defer("description") def get_permitted_environments_for_master_api_key( diff --git a/api/projects/permissions.py b/api/projects/permissions.py index 1b4bddc24a45..2df8503cc1d1 100644 --- a/api/projects/permissions.py +++ b/api/projects/permissions.py @@ -48,12 +48,14 @@ def has_permission(self, request, view): subscription_metadata = ( organisation.subscription.get_subscription_metadata() ) + total_projects_created = Project.objects.filter( organisation=organisation ).count() if ( subscription_metadata.projects and total_projects_created >= subscription_metadata.projects + and not getattr(request, "is_e2e", False) is True ): return False if organisation.restrict_project_create_to_admin: diff --git a/api/projects/tags/migrations/0006_alter_tag_type.py b/api/projects/tags/migrations/0006_alter_tag_type.py new file mode 100644 index 000000000000..84736c31bd3f --- /dev/null +++ b/api/projects/tags/migrations/0006_alter_tag_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.25 on 2024-05-27 15:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tags', '0005_add_tag_fields_for_stale_flags_logic'), + ] + + operations = [ + migrations.AlterField( + model_name='tag', + name='type', + field=models.CharField(choices=[('NONE', 'None'), ('STALE', 'Stale'), ('GITHUB', 'Github')], default='NONE', help_text='Field used to provide a consistent identifier for the FE and API to use for business logic.', max_length=100), + ), + ] diff --git a/api/projects/tags/models.py b/api/projects/tags/models.py index 597bebdd0d9a..35c75eecfb7e 100644 --- a/api/projects/tags/models.py +++ b/api/projects/tags/models.py @@ -7,6 +7,7 @@ class TagType(models.Choices): NONE = "NONE" STALE = "STALE" + GITHUB = "GITHUB" class Tag(AbstractBaseExportableModel): diff --git a/api/sales_dashboard/views.py b/api/sales_dashboard/views.py index 27861b5b7022..53513ab87d8b 100644 --- a/api/sales_dashboard/views.py +++ b/api/sales_dashboard/views.py @@ -1,4 +1,5 @@ import json +from datetime import timedelta import re2 as re from app_analytics.influxdb_wrapper import ( @@ -19,6 +20,7 @@ from django.shortcuts import get_object_or_404 from django.template import loader from django.urls import reverse, reverse_lazy +from django.utils import timezone from django.utils.safestring import mark_safe from django.views.generic import ListView from django.views.generic.edit import FormView @@ -163,10 +165,11 @@ def organisation_info(request: HttpRequest, organisation_id: int) -> HttpRespons date_range = request.GET.get("date_range", "180d") context["date_range"] = date_range - date_start = f"-{date_range}" - date_stop = "now()" + assert date_range.endswith("d") + now = timezone.now() + date_start = now - timedelta(days=int(date_range[:-1])) event_list, labels = get_event_list_for_organisation( - organisation_id, date_start, date_stop + organisation_id, date_start ) context["event_list"] = event_list context["traits"] = mark_safe(json.dumps(event_list["traits"])) @@ -176,13 +179,16 @@ def organisation_info(request: HttpRequest, organisation_id: int) -> HttpRespons json.dumps(event_list["environment-document"]) ) context["labels"] = mark_safe(json.dumps(labels)) + + date_starts = {} + date_starts["24h"] = now - timedelta(days=1) + date_starts["7d"] = now - timedelta(days=7) + date_starts["30d"] = now - timedelta(days=30) context["api_calls"] = { # TODO: this could probably be reduced to a single influx request # rather than 3 - range_: get_events_for_organisation( - organisation_id, date_start=f"-{range_}" - ) - for range_ in ("24h", "7d", "30d") + period: get_events_for_organisation(organisation_id, date_start=_date_start) + for period, _date_start in date_starts.items() } return HttpResponse(template.render(context, request)) diff --git a/api/scripts/run-docker.sh b/api/scripts/run-docker.sh index 939a339b504b..88ae045c98f5 100755 --- a/api/scripts/run-docker.sh +++ b/api/scripts/run-docker.sh @@ -1,8 +1,14 @@ #!/bin/sh set -e +function waitfordb() { + if [ -z "${SKIP_WAIT_FOR_DB}" ]; then + python manage.py waitfordb "$@" + fi +} + function migrate () { - python manage.py waitfordb && python manage.py migrate && python manage.py createcachetable + waitfordb && python manage.py migrate && python manage.py createcachetable } function serve() { # configuration parameters for statsd. Docs can be found here: @@ -10,7 +16,7 @@ function serve() { export STATSD_PORT=${STATSD_PORT:-8125} export STATSD_PREFIX=${STATSD_PREFIX:-flagsmith.api} - python manage.py waitfordb + waitfordb exec gunicorn --bind 0.0.0.0:8000 \ --worker-tmp-dir /dev/shm \ @@ -26,9 +32,9 @@ function serve() { app.wsgi } function run_task_processor() { - python manage.py waitfordb --waitfor 30 --migrations + waitfordb --waitfor 30 --migrations if [[ -n "$ANALYTICS_DATABASE_URL" || -n "$DJANGO_DB_NAME_ANALYTICS" ]]; then - python manage.py waitfordb --waitfor 30 --migrations --database analytics + waitfordb --waitfor 30 --migrations --database analytics fi RUN_BY_PROCESSOR=1 exec python manage.py runprocessor \ --sleepintervalms ${TASK_PROCESSOR_SLEEP_INTERVAL:-500} \ diff --git a/api/segments/migrations/0023_add_versioning_to_segments.py b/api/segments/migrations/0023_add_versioning_to_segments.py index 0e7c8f0d5a03..16473f1cf587 100644 --- a/api/segments/migrations/0023_add_versioning_to_segments.py +++ b/api/segments/migrations/0023_add_versioning_to_segments.py @@ -1,9 +1,17 @@ # Generated by Django 3.2.25 on 2024-06-10 15:31 -import os +from pathlib import Path import django.db.models.deletion from django.db import migrations, models +parent_dir = Path(__file__).parent.resolve() + +with open(parent_dir / "sql/0023_add_versioning_to_segments.sql") as f: + segment_versioning_sql_forwards = f.read() + +with open(parent_dir / "sql/0023_add_versioning_to_segments_reverse.sql") as f: + segment_versioning_sql_reverse = f.read() + class Migration(migrations.Migration): @@ -46,13 +54,7 @@ class Migration(migrations.Migration): ), ), migrations.RunSQL( - sql=open( - os.path.join( - os.path.dirname(__file__), - "sql", - "0023_add_versioning_to_segments.sql", - ) - ).read(), - reverse_sql=migrations.RunSQL.noop, + sql=segment_versioning_sql_forwards, + reverse_sql=segment_versioning_sql_reverse, ), ] diff --git a/api/segments/migrations/sql/0023_add_versioning_to_segments_reverse.sql b/api/segments/migrations/sql/0023_add_versioning_to_segments_reverse.sql new file mode 100644 index 000000000000..7d78f144d9ec --- /dev/null +++ b/api/segments/migrations/sql/0023_add_versioning_to_segments_reverse.sql @@ -0,0 +1,3 @@ +UPDATE segments_segment +SET deleted_at = now() +WHERE version_of_id <> id; diff --git a/api/tests/integration/custom_auth/end_to_end/test_custom_auth_integration.py b/api/tests/integration/custom_auth/end_to_end/test_custom_auth_integration.py index aec4b9b4327f..e274475119b5 100644 --- a/api/tests/integration/custom_auth/end_to_end/test_custom_auth_integration.py +++ b/api/tests/integration/custom_auth/end_to_end/test_custom_auth_integration.py @@ -12,7 +12,7 @@ from organisations.invites.models import Invite from organisations.models import Organisation -from users.models import FFAdminUser +from users.models import FFAdminUser, SignUpType def test_register_and_login_workflows(db: None, api_client: APIClient) -> None: @@ -124,6 +124,7 @@ def test_can_register_with_invite_if_registration_disabled_without_invite( "password": password, "first_name": "test", "last_name": "register", + "sign_up_type": SignUpType.INVITE_EMAIL.value, } Invite.objects.create(email=email, organisation=organisation) diff --git a/api/tests/integration/e2etests/end_to_end/test_integration_e2e_tests.py b/api/tests/integration/e2etests/end_to_end/test_integration_e2e_tests.py index c2d187007839..731ffe28d3a2 100644 --- a/api/tests/integration/e2etests/end_to_end/test_integration_e2e_tests.py +++ b/api/tests/integration/e2etests/end_to_end/test_integration_e2e_tests.py @@ -5,6 +5,7 @@ from rest_framework.test import APIClient from organisations.models import Subscription +from organisations.subscriptions.constants import SCALE_UP from users.models import FFAdminUser @@ -14,8 +15,8 @@ def test_e2e_teardown(settings, db) -> None: token = "test-token" register_url = "/api/v1/auth/users/" settings.ENABLE_FE_E2E = True - - os.environ["E2E_TEST_AUTH_TOKEN"] = token + settings.E2E_TEST_AUTH_TOKEN = token + settings.MIDDLEWARE.append("e2etests.middleware.E2ETestMiddleware") client = APIClient(HTTP_X_E2E_TEST_AUTH_TOKEN=token) @@ -41,7 +42,9 @@ def test_e2e_teardown(settings, db) -> None: for subscription in Subscription.objects.filter( organisation__in=e2e_user.organisations.all() ): - assert subscription.max_seats == 2 + assert subscription.max_seats == 8 + assert subscription.plan == SCALE_UP + assert subscription.subscription_id == "test_subscription_id" def test_e2e_teardown_with_incorrect_token(settings, db): diff --git a/api/tests/integration/edge_api/identities/conftest.py b/api/tests/integration/edge_api/identities/conftest.py index f22e02366dda..298bd6165ee6 100644 --- a/api/tests/integration/edge_api/identities/conftest.py +++ b/api/tests/integration/edge_api/identities/conftest.py @@ -1,4 +1,13 @@ +from typing import Any + import pytest +from boto3.dynamodb.conditions import Key +from flag_engine.identities.models import IdentityModel + +from edge_api.identities.models import EdgeIdentity +from environments.dynamodb.wrappers.environment_wrapper import ( + DynamoEnvironmentV2Wrapper, +) @pytest.fixture() @@ -6,3 +15,26 @@ def webhook_mock(mocker): return mocker.patch( "edge_api.identities.serializers.call_environment_webhook_for_feature_state_change" ) + + +@pytest.fixture() +def identity_overrides_v2( + dynamo_enabled_environment: int, + identity_document_without_fs: dict[str, Any], + identity_document: dict[str, Any], + dynamodb_wrapper_v2: DynamoEnvironmentV2Wrapper, +) -> list[str]: + edge_identity = EdgeIdentity.from_identity_document(identity_document_without_fs) + for feature_override in IdentityModel.model_validate( + identity_document + ).identity_features: + edge_identity.add_feature_override(feature_override) + edge_identity.save() + return [ + item["document_key"] + for item in dynamodb_wrapper_v2.query_get_all_items( + KeyConditionExpression=Key("environment_id").eq( + str(dynamo_enabled_environment) + ), + ) + ] diff --git a/api/tests/integration/edge_api/identities/test_edge_identity_viewset.py b/api/tests/integration/edge_api/identities/test_edge_identity_viewset.py index ad9a95385a93..ffdadfe5d6a1 100644 --- a/api/tests/integration/edge_api/identities/test_edge_identity_viewset.py +++ b/api/tests/integration/edge_api/identities/test_edge_identity_viewset.py @@ -1,11 +1,20 @@ import json import urllib +from typing import Any +from boto3.dynamodb.conditions import Key from django.urls import reverse from rest_framework import status from rest_framework.exceptions import NotFound +from rest_framework.test import APIClient from edge_api.identities.views import EdgeIdentityViewSet +from environments.dynamodb.wrappers.environment_wrapper import ( + DynamoEnvironmentV2Wrapper, +) +from environments.dynamodb.wrappers.identity_wrapper import ( + DynamoIdentityWrapper, +) def test_get_identities_returns_bad_request_if_dynamo_is_not_enabled( @@ -125,12 +134,15 @@ def test_create_identity_returns_400_if_identity_already_exists( def test_delete_identity( - admin_client, - dynamo_enabled_environment, - environment_api_key, - identity_document, - edge_identity_dynamo_wrapper_mock, -): + admin_client: APIClient, + dynamo_enabled_environment: int, + environment_api_key: str, + identity_document_without_fs: dict[str, Any], + identity_document: dict[str, Any], + dynamodb_identity_wrapper: DynamoIdentityWrapper, + dynamodb_wrapper_v2: DynamoEnvironmentV2Wrapper, + identity_overrides_v2: list[str], +) -> None: # Given identity_uuid = identity_document["identity_uuid"] url = reverse( @@ -138,20 +150,22 @@ def test_delete_identity( args=[environment_api_key, identity_uuid], ) - edge_identity_dynamo_wrapper_mock.get_item_from_uuid_or_404.return_value = ( - identity_document - ) # When response = admin_client.delete(url) # Then assert response.status_code == status.HTTP_204_NO_CONTENT - edge_identity_dynamo_wrapper_mock.get_item_from_uuid_or_404.assert_called_with( - identity_uuid - ) - edge_identity_dynamo_wrapper_mock.delete_item.assert_called_with( - identity_document["composite_key"] + assert not dynamodb_identity_wrapper.query_items( + IndexName="identity_uuid-index", + KeyConditionExpression=Key("identity_uuid").eq(identity_uuid), + )["Count"] + assert not list( + dynamodb_wrapper_v2.query_get_all_items( + KeyConditionExpression=Key("environment_id").eq( + str(dynamo_enabled_environment) + ) + ) ) diff --git a/api/tests/integration/environments/identities/test_integration_identities.py b/api/tests/integration/environments/identities/test_integration_identities.py index 445eedacfbb4..0537fb66f6ef 100644 --- a/api/tests/integration/environments/identities/test_integration_identities.py +++ b/api/tests/integration/environments/identities/test_integration_identities.py @@ -1,8 +1,11 @@ +import hashlib import json +from typing import Any, Generator from unittest import mock import pytest from django.urls import reverse +from pytest_lazyfixture import lazy_fixture from rest_framework import status from rest_framework.test import APIClient @@ -224,13 +227,64 @@ def test_get_feature_states_for_identity_only_makes_one_query_to_get_mv_feature_ assert len(second_identity_response_json["flags"]) == 3 -def test_get_feature_states_for_identity__transient_identity__segment_match_expected( +@pytest.fixture +def existing_identity_identifier_data( + identity_identifier: str, + identity: int, +) -> dict[str, Any]: + return {"identifier": identity_identifier} + + +@pytest.fixture +def transient_identifier( + segment_condition_property: str, + segment_condition_value: str, +) -> Generator[str, None, None]: + return hashlib.sha256( + f"avalue_a{segment_condition_property}{segment_condition_value}".encode() + ).hexdigest() + + +@pytest.mark.parametrize( + "transient_data", + [ + pytest.param({"transient": True}, id="with-transient-true"), + pytest.param({"transient": False}, id="with-transient-false"), + pytest.param({}, id="missing-transient"), + ], +) +@pytest.mark.parametrize( + "identifier_data,expected_identifier", + [ + pytest.param( + lazy_fixture("existing_identity_identifier_data"), + lazy_fixture("identity_identifier"), + id="existing-identifier", + ), + pytest.param({"identifier": "unseen"}, "unseen", id="new-identifier"), + pytest.param( + {"identifier": ""}, + lazy_fixture("transient_identifier"), + id="blank-identifier", + ), + pytest.param( + {"identifier": None}, + lazy_fixture("transient_identifier"), + id="null-identifier", + ), + pytest.param({}, lazy_fixture("transient_identifier"), id="missing-identifier"), + ], +) +def test_get_feature_states_for_identity__segment_match_expected( sdk_client: APIClient, feature: int, segment: int, segment_condition_property: str, segment_condition_value: str, segment_featurestate: int, + identifier_data: dict[str, Any], + transient_data: dict[str, Any], + expected_identifier: str, ) -> None: # Given url = reverse("api-v1:sdk-identities") @@ -242,14 +296,15 @@ def test_get_feature_states_for_identity__transient_identity__segment_match_expe url, data=json.dumps( { - "identifier": "unseen", + **identifier_data, + **transient_data, "traits": [ { "trait_key": segment_condition_property, "trait_value": segment_condition_value, - } + }, + {"trait_key": "a", "trait_value": "value_a"}, ], - "transient": True, } ), content_type="application/json", @@ -258,6 +313,7 @@ def test_get_feature_states_for_identity__transient_identity__segment_match_expe # Then assert response.status_code == status.HTTP_200_OK response_json = response.json() + assert response_json["identifier"] == expected_identifier assert ( flag_data := next( ( @@ -272,6 +328,29 @@ def test_get_feature_states_for_identity__transient_identity__segment_match_expe assert flag_data["feature_state_value"] == "segment override" +def test_get_feature_states_for_identity__empty_traits__random_identifier_expected( + sdk_client: APIClient, + environment: int, +) -> None: + # Given + url = reverse("api-v1:sdk-identities") + + # When + response_1 = sdk_client.post( + url, + data=json.dumps({"traits": []}), + content_type="application/json", + ) + response_2 = sdk_client.post( + url, + data=json.dumps({"traits": []}), + content_type="application/json", + ) + + # Then + assert response_1.json()["identifier"] != response_2.json()["identifier"] + + def test_get_feature_states_for_identity__transient_trait__segment_match_expected( sdk_client: APIClient, feature: int, diff --git a/api/tests/unit/app_analytics/test_unit_app_analytics_cache.py b/api/tests/unit/app_analytics/test_unit_app_analytics_cache.py index de5f9114d589..88ccc0cbe852 100644 --- a/api/tests/unit/app_analytics/test_unit_app_analytics_cache.py +++ b/api/tests/unit/app_analytics/test_unit_app_analytics_cache.py @@ -1,12 +1,18 @@ -from app_analytics.cache import CACHE_FLUSH_INTERVAL, APIUsageCache +from app_analytics.cache import APIUsageCache, FeatureEvaluationCache from app_analytics.models import Resource from django.utils import timezone from freezegun import freeze_time +from pytest_django.fixtures import SettingsWrapper from pytest_mock import MockerFixture -def test_api_usage_cache(mocker: MockerFixture) -> None: +def test_api_usage_cache( + mocker: MockerFixture, + settings: SettingsWrapper, +) -> None: # Given + settings.PG_API_USAGE_CACHE_SECONDS = 60 + cache = APIUsageCache() now = timezone.now() mocked_track_request_task = mocker.patch("app_analytics.cache.track_request") @@ -25,7 +31,7 @@ def test_api_usage_cache(mocker: MockerFixture) -> None: assert not mocked_track_request_task.called # Now, let's move the time forward - frozen_time.tick(CACHE_FLUSH_INTERVAL + 1) + frozen_time.tick(settings.PG_API_USAGE_CACHE_SECONDS + 1) # let's track another request(to trigger flush) cache.track_request( @@ -71,3 +77,92 @@ def test_api_usage_cache(mocker: MockerFixture) -> None: # finally, make sure track_request task was not called assert not mocked_track_request_task.called + + +def test_feature_evaluation_cache( + mocker: MockerFixture, + settings: SettingsWrapper, +): + # Given + settings.FEATURE_EVALUATION_CACHE_SECONDS = 60 + settings.USE_POSTGRES_FOR_ANALYTICS = False + settings.INFLUXDB_TOKEN = "token" + + mocked_track_evaluation_task = mocker.patch( + "app_analytics.cache.track_feature_evaluation" + ) + mocked_track_feature_evaluation_influxdb_task = mocker.patch( + "app_analytics.cache.track_feature_evaluation_influxdb" + ) + environment_1_id = 1 + environment_2_id = 2 + feature_1_name = "feature_1_name" + feature_2_name = "feature_2_name" + + cache = FeatureEvaluationCache() + now = timezone.now() + + with freeze_time(now) as frozen_time: + # Track some feature evaluations + for _ in range(10): + cache.track_feature_evaluation(environment_1_id, feature_1_name, 1) + cache.track_feature_evaluation(environment_1_id, feature_2_name, 1) + cache.track_feature_evaluation(environment_2_id, feature_2_name, 1) + + # Make sure the internal tasks were not called + assert not mocked_track_evaluation_task.delay.called + assert not mocked_track_feature_evaluation_influxdb_task.delay.called + + # Now, let's move the time forward + frozen_time.tick(settings.FEATURE_EVALUATION_CACHE_SECONDS + 1) + + # track another evaluation(to trigger cache flush) + cache.track_feature_evaluation(environment_1_id, feature_1_name, 1) + + # Then + mocked_track_feature_evaluation_influxdb_task.delay.assert_has_calls( + [ + mocker.call( + kwargs={ + "environment_id": environment_1_id, + "feature_evaluations": { + feature_1_name: 11, + feature_2_name: 10, + }, + }, + ), + mocker.call( + kwargs={ + "environment_id": environment_2_id, + "feature_evaluations": {feature_2_name: 10}, + }, + ), + ] + ) + # task responsible for tracking evaluation using postgres was not called + assert not mocked_track_evaluation_task.delay.called + + # Next, let's enable postgres tracking + settings.USE_POSTGRES_FOR_ANALYTICS = True + + # rest the mock + mocked_track_feature_evaluation_influxdb_task.reset_mock() + + # Track another evaluation + cache.track_feature_evaluation(environment_1_id, feature_1_name, 1) + + # move time forward again + frozen_time.tick(settings.FEATURE_EVALUATION_CACHE_SECONDS + 1) + + # track another one(to trigger cache flush) + cache.track_feature_evaluation(environment_1_id, feature_1_name, 1) + + # Assert that the call was made with only the data tracked after the flush interval. + mocked_track_evaluation_task.delay.assert_called_once_with( + kwargs={ + "environment_id": environment_1_id, + "feature_evaluations": {feature_1_name: 2}, + } + ) + # and the task for influx was not called + assert not mocked_track_feature_evaluation_influxdb_task.delay.called diff --git a/api/tests/unit/app_analytics/test_unit_app_analytics_influxdb_wrapper.py b/api/tests/unit/app_analytics/test_unit_app_analytics_influxdb_wrapper.py index 85f8607d7947..d82b6f162978 100644 --- a/api/tests/unit/app_analytics/test_unit_app_analytics_influxdb_wrapper.py +++ b/api/tests/unit/app_analytics/test_unit_app_analytics_influxdb_wrapper.py @@ -1,4 +1,4 @@ -from datetime import date, timedelta +from datetime import date, datetime, timedelta from typing import Generator, Type from unittest import mock from unittest.mock import MagicMock @@ -9,11 +9,13 @@ from app_analytics.influxdb_wrapper import ( InfluxDBWrapper, build_filter_string, + get_current_api_usage, get_event_list_for_organisation, get_events_for_organisation, get_feature_evaluation_data, get_multiple_event_list_for_feature, get_multiple_event_list_for_organisation, + get_range_bucket_mappings, get_top_organisations, get_usage_data, ) @@ -21,6 +23,7 @@ from django.utils import timezone from influxdb_client.client.exceptions import InfluxDBError from influxdb_client.rest import ApiException +from pytest_django.fixtures import SettingsWrapper from pytest_mock import MockerFixture from urllib3.exceptions import HTTPError @@ -84,10 +87,12 @@ def test_write_handles_errors( # but the exception was not raised +@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") def test_influx_db_query_when_get_events_then_query_api_called(monkeypatch): expected_query = ( ( - f'from(bucket:"{read_bucket}") |> range(start: -30d, stop: now()) ' + f'from(bucket:"{read_bucket}") |> range(start: 2022-12-20T09:09:47.325132+00:00, ' + "stop: 2023-01-19T09:09:47.325132+00:00) " f'|> filter(fn:(r) => r._measurement == "api_call") ' f'|> filter(fn: (r) => r["_field"] == "request_count") ' f'|> filter(fn: (r) => r["organisation_id"] == "{org_id}") ' @@ -117,10 +122,11 @@ def test_influx_db_query_when_get_events_then_query_api_called(monkeypatch): assert call[2]["query"].replace(" ", "").replace("\n", "") == expected_query +@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") def test_influx_db_query_when_get_events_list_then_query_api_called(monkeypatch): query = ( f'from(bucket:"{read_bucket}") ' - f"|> range(start: -30d, stop: now()) " + f"|> range(start: 2022-12-20T09:09:47.325132+00:00, stop: 2023-01-19T09:09:47.325132+00:00) " f'|> filter(fn:(r) => r._measurement == "api_call") ' f'|> filter(fn: (r) => r["organisation_id"] == "{org_id}") ' f'|> drop(columns: ["organisation", "organisation_id", "type", "project", ' @@ -180,6 +186,7 @@ def test_influx_db_query_when_get_events_list_then_query_api_called(monkeypatch) ), ), ) +@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") def test_influx_db_query_when_get_multiple_events_for_organisation_then_query_api_called( monkeypatch, project_id, environment_id, expected_filters ): @@ -187,7 +194,7 @@ def test_influx_db_query_when_get_multiple_events_for_organisation_then_query_ap expected_query = ( ( f'from(bucket:"{read_bucket}") ' - "|> range(start: -30d, stop: now()) " + "|> range(start: 2022-12-20T09:09:47.325132+00:00, stop: 2023-01-19T09:09:47.325132+00:00) " f"{build_filter_string(expected_filters)}" '|> drop(columns: ["organisation", "organisation_id", "type", "project", ' '"project_id", "environment", "environment_id", "host"]) ' @@ -217,12 +224,13 @@ def test_influx_db_query_when_get_multiple_events_for_organisation_then_query_ap assert call[2]["query"].replace(" ", "").replace("\n", "") == expected_query +@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") def test_influx_db_query_when_get_multiple_events_for_feature_then_query_api_called( monkeypatch, ): query = ( f'from(bucket:"{read_bucket}") ' - "|> range(start: -30d, stop: now()) " + "|> range(start: 2022-12-20T09:09:47.325132+00:00, stop: 2023-01-19T09:09:47.325132+00:00) " '|> filter(fn:(r) => r._measurement == "feature_evaluation") ' '|> filter(fn: (r) => r["_field"] == "request_count") ' f'|> filter(fn: (r) => r["environment_id"] == "{env_id}") ' @@ -248,6 +256,7 @@ def test_influx_db_query_when_get_multiple_events_for_feature_then_query_api_cal mock_query_api.query.assert_called_once_with(org=influx_org, query=query) +@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") def test_get_usage_data(mocker): # Given influx_data = [ @@ -276,12 +285,14 @@ def test_get_usage_data(mocker): usage_data = get_usage_data(org_id) # Then + date_start = datetime.fromisoformat("2022-12-20T09:09:47.325132+00:00") + date_stop = datetime.fromisoformat("2023-01-19T09:09:47.325132+00:00") mocked_get_multiple_event_list_for_organisation.assert_called_once_with( organisation_id=org_id, environment_id=None, project_id=None, - date_start="-30d", - date_stop="now()", + date_start=date_start, + date_stop=date_stop, ) assert len(usage_data) == 2 @@ -299,6 +310,7 @@ def test_get_usage_data(mocker): assert usage_data[1].environment_document == 10 +@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") def test_get_feature_evaluation_data(mocker): # Given influx_data = [ @@ -318,8 +330,9 @@ def test_get_feature_evaluation_data(mocker): ) # Then + date_start = datetime.fromisoformat("2022-12-20T09:09:47.325132+00:00") mocked_get_multiple_event_list_for_feature.assert_called_once_with( - feature_name=feature_name, environment_id=env_id, date_start="-30d" + feature_name=feature_name, environment_id=env_id, date_start=date_start ) assert len(feature_evaluation_data) == 2 @@ -331,17 +344,17 @@ def test_get_feature_evaluation_data(mocker): assert feature_evaluation_data[1].count == 200 -@pytest.mark.parametrize("date_stop", ["now()", "-5d"]) @pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") def test_get_event_list_for_organisation_with_date_stop_set_to_now_and_previously( - date_stop: str, mocker: MockerFixture, organisation: Organisation, ) -> None: # Given + now = timezone.now() one_day_ago = now - timedelta(days=1) two_days_ago = now - timedelta(days=2) + date_stop = now record_mock1 = mock.MagicMock() record_mock1.__getitem__.side_effect = lambda key: { @@ -377,6 +390,7 @@ def test_get_event_list_for_organisation_with_date_stop_set_to_now_and_previousl assert labels == ["2023-01-18", "2023-01-17"] +@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") @pytest.mark.parametrize("limit", ["10", ""]) def test_get_top_organisations( limit: str, @@ -399,9 +413,11 @@ def test_get_top_organisations( ) influx_mock.return_value = [result] + now = timezone.now() + date_start = now - timedelta(days=30) # When - dataset = get_top_organisations(date_start="-30d", limit=limit) + dataset = get_top_organisations(date_start=date_start, limit=limit) # Then assert dataset == {123: 23, 456: 43} @@ -409,9 +425,10 @@ def test_get_top_organisations( influx_mock.assert_called_once() influx_query_call = influx_mock.call_args assert influx_query_call.kwargs["bucket"] == "test_bucket_downsampled_1h" - assert influx_query_call.kwargs["date_start"] == "-30d" + assert influx_query_call.kwargs["date_start"] == date_start +@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") def test_get_top_organisations_value_error( mocker: MockerFixture, ) -> None: @@ -432,9 +449,11 @@ def test_get_top_organisations_value_error( ) influx_mock.return_value = [result] + now = timezone.now() + date_start = now - timedelta(days=30) # When - dataset = get_top_organisations(date_start="-30d") + dataset = get_top_organisations(date_start=date_start) # Then # The wrongly typed data does not stop the remaining data @@ -444,10 +463,90 @@ def test_get_top_organisations_value_error( def test_early_return_for_empty_range_for_influx_query_manager() -> None: # When + now = timezone.now() results = InfluxDBWrapper.influx_query_manager( - date_start="-0d", - date_stop="now()", + date_start=now, + date_stop=now, ) # Then assert results == [] + + +def test_get_range_bucket_mappings_when_less_than_10_days( + settings: SettingsWrapper, +) -> None: + # Given + two_days = timezone.now() - timedelta(days=2) + + # When + result = get_range_bucket_mappings(two_days) + + # Then + assert result == settings.INFLUXDB_BUCKET + "_downsampled_15m" + + +def test_get_range_bucket_mappings_when_more_than_10_days( + settings: SettingsWrapper, +) -> None: + # Given + twelve_days = timezone.now() - timedelta(days=12) + + # When + result = get_range_bucket_mappings(twelve_days) + + # Then + assert result == settings.INFLUXDB_BUCKET + "_downsampled_1h" + + +def test_influx_query_manager_when_date_start_is_set_to_none( + mocker: MockerFixture, +) -> None: + # Given + mock_client = mocker.patch("app_analytics.influxdb_wrapper.influxdb_client") + + # When + InfluxDBWrapper.influx_query_manager() + + # Then + mock_client.query_api.assert_called_once() + + +@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") +def test_get_top_organisation_when_date_start_is_set_to_none( + mocker: MockerFixture, +) -> None: + # Given + influx_mock = mocker.patch( + "app_analytics.influxdb_wrapper.InfluxDBWrapper.influx_query_manager" + ) + now = timezone.now() + date_start = now - timedelta(days=30) + + # When + get_top_organisations() + + # Then + influx_query_call = influx_mock.call_args + assert influx_query_call.kwargs["bucket"] == "test_bucket_downsampled_1h" + assert influx_query_call.kwargs["date_start"] == date_start + + +def test_get_current_api_usage(mocker: MockerFixture) -> None: + # Given + influx_mock = mocker.patch( + "app_analytics.influxdb_wrapper.InfluxDBWrapper.influx_query_manager" + ) + record_mock = mock.MagicMock() + record_mock.values = {"organisation": "1-TestCorp"} + record_mock.get_value.return_value = 43 + + result = mock.MagicMock() + result.records = [record_mock] + influx_mock.return_value = [result] + + # When + result = get_current_api_usage(organisation_id=1) + + # Then + assert result == 43 diff --git a/api/tests/unit/app_analytics/test_unit_app_analytics_views.py b/api/tests/unit/app_analytics/test_unit_app_analytics_views.py index a9276fa868ea..371f54bf329a 100644 --- a/api/tests/unit/app_analytics/test_unit_app_analytics_views.py +++ b/api/tests/unit/app_analytics/test_unit_app_analytics_views.py @@ -36,8 +36,8 @@ def test_sdk_analytics_does_not_allow_bad_data(mocker, settings, environment): view = SDKAnalyticsFlags(request=request) - mocked_track_feature_eval = mocker.patch( - "app_analytics.views.track_feature_evaluation_influxdb" + mocked_feature_eval_cache = mocker.patch( + "app_analytics.views.feature_evaluation_cache" ) # When @@ -45,34 +45,7 @@ def test_sdk_analytics_does_not_allow_bad_data(mocker, settings, environment): # Then assert response.status_code == status.HTTP_200_OK - mocked_track_feature_eval.assert_not_called() - - -def test_sdk_analytics_allows_valid_data(mocker, settings, environment, feature): - # Given - settings.INFLUXDB_TOKEN = "some-token" - - data = {feature.name: 12} - request = mocker.MagicMock( - data=data, - environment=environment, - query_params={}, - ) - - view = SDKAnalyticsFlags(request=request) - - mocked_track_feature_eval = mocker.patch( - "app_analytics.views.track_feature_evaluation_influxdb" - ) - - # When - response = view.post(request) - - # Then - assert response.status_code == status.HTTP_200_OK - mocked_track_feature_eval.run_in_thread.assert_called_once_with( - args=(environment.id, data) - ) + mocked_feature_eval_cache.track_feature_evaluation.assert_not_called() def test_get_usage_data(mocker, admin_client, organisation): @@ -168,8 +141,8 @@ def test_get_usage_data__current_billing_period( organisation_id=organisation.id, environment_id=None, project_id=None, - date_start="-28d", - date_stop="now()", + date_start=four_weeks_ago, + date_stop=now, ) @@ -195,6 +168,7 @@ def test_get_usage_data__previous_billing_period( now = timezone.now() week_from_now = now + timedelta(days=7) four_weeks_ago = now - timedelta(days=28) + target_start_at = now - timedelta(days=59) OrganisationSubscriptionInformationCache.objects.create( organisation=organisation, @@ -229,8 +203,8 @@ def test_get_usage_data__previous_billing_period( organisation_id=organisation.id, environment_id=None, project_id=None, - date_start="-59d", - date_stop="-28d", + date_start=target_start_at, + date_stop=four_weeks_ago, ) @@ -256,7 +230,7 @@ def test_get_usage_data__ninety_day_period( now = timezone.now() week_from_now = now + timedelta(days=7) four_weeks_ago = now - timedelta(days=28) - + ninety_days_ago = now - timedelta(days=90) OrganisationSubscriptionInformationCache.objects.create( organisation=organisation, current_billing_term_starts_at=four_weeks_ago, @@ -290,8 +264,8 @@ def test_get_usage_data__ninety_day_period( organisation_id=organisation.id, environment_id=None, project_id=None, - date_start="-90d", - date_stop="now()", + date_start=ninety_days_ago, + date_stop=now, ) @@ -432,24 +406,20 @@ def test_set_sdk_analytics_flags_without_identifier( assert feature_evaluation_raw.evaluation_count is feature_request_count -def test_set_sdk_analytics_flags_v1_to_influxdb( +def test_sdk_analytics_flags_v1( api_client: APIClient, environment: Environment, feature: Feature, - identity: Identity, - settings: SettingsWrapper, mocker: MockerFixture, ) -> None: # Given - settings.INFLUXDB_TOKEN = "some-token" - url = reverse("api-v1:analytics-flags") api_client.credentials(HTTP_X_ENVIRONMENT_KEY=environment.api_key) feature_request_count = 2 data = {feature.name: feature_request_count} - mocked_track_feature_eval = mocker.patch( - "app_analytics.views.track_feature_evaluation_influxdb" + mocked_feature_evaluation_cache = mocker.patch( + "app_analytics.views.feature_evaluation_cache" ) # When @@ -459,9 +429,6 @@ def test_set_sdk_analytics_flags_v1_to_influxdb( # Then assert response.status_code == status.HTTP_200_OK - mocked_track_feature_eval.run_in_thread.assert_called_once_with( - args=( - environment.id, - data, - ) + mocked_feature_evaluation_cache.track_feature_evaluation.assert_called_once_with( + environment.id, feature.name, feature_request_count ) diff --git a/api/tests/unit/custom_auth/conftest.py b/api/tests/unit/custom_auth/conftest.py new file mode 100644 index 000000000000..17d5f760c4c1 --- /dev/null +++ b/api/tests/unit/custom_auth/conftest.py @@ -0,0 +1,9 @@ +import pytest + +from organisations.invites.models import InviteLink +from organisations.models import Organisation + + +@pytest.fixture() +def invite_link(organisation: Organisation) -> InviteLink: + return InviteLink.objects.create(organisation=organisation) diff --git a/api/tests/unit/custom_auth/oauth/test_unit_oauth_serializers.py b/api/tests/unit/custom_auth/oauth/test_unit_oauth_serializers.py index bd21e9fc5d08..11a0519e0b6f 100644 --- a/api/tests/unit/custom_auth/oauth/test_unit_oauth_serializers.py +++ b/api/tests/unit/custom_auth/oauth/test_unit_oauth_serializers.py @@ -1,5 +1,7 @@ +from typing import Type from unittest import mock +import pytest from django.test import RequestFactory from django.utils import timezone from pytest_django.fixtures import SettingsWrapper @@ -11,6 +13,7 @@ GoogleLoginSerializer, OAuthLoginSerializer, ) +from organisations.invites.models import InviteLink from users.models import FFAdminUser, SignUpType @@ -128,7 +131,11 @@ def test_OAuthLoginSerializer_calls_is_authentication_method_valid_correctly_if_ def test_OAuthLoginSerializer_allows_registration_if_sign_up_type_is_invite_link( - settings: SettingsWrapper, rf: RequestFactory, mocker: MockerFixture, db: None + settings: SettingsWrapper, + rf: RequestFactory, + mocker: MockerFixture, + db: None, + invite_link: InviteLink, ): # Given settings.ALLOW_REGISTRATION_WITHOUT_INVITE = False @@ -140,6 +147,7 @@ def test_OAuthLoginSerializer_allows_registration_if_sign_up_type_is_invite_link data={ "access_token": "some_token", "sign_up_type": SignUpType.INVITE_LINK.value, + "invite_hash": invite_link.hash, }, context={"request": request}, ) @@ -153,3 +161,38 @@ def test_OAuthLoginSerializer_allows_registration_if_sign_up_type_is_invite_link # Then assert user + + +@pytest.mark.parametrize( + "serializer_class", (GithubLoginSerializer, GithubLoginSerializer) +) +def test_OAuthLoginSerializer_allows_login_if_allow_registration_without_invite_is_false( + settings: SettingsWrapper, + rf: RequestFactory, + mocker: MockerFixture, + admin_user: FFAdminUser, + serializer_class: Type[OAuthLoginSerializer], +): + # Given + settings.ALLOW_REGISTRATION_WITHOUT_INVITE = False + + request = rf.post("/api/v1/auth/users/") + + serializer = serializer_class( + data={"access_token": "some_token"}, + context={"request": request}, + ) + # monkey patch the get_user_info method to return the mock user data + serializer.get_user_info = lambda: { + "email": admin_user.email, + "github_user_id": "abc123", + "google_user_id": "abc123", + } + + serializer.is_valid(raise_exception=True) + + # When + user = serializer.save() + + # Then + assert user diff --git a/api/tests/unit/custom_auth/oauth/test_unit_oauth_views.py b/api/tests/unit/custom_auth/oauth/test_unit_oauth_views.py index 0f742267b71b..99a451bab4eb 100644 --- a/api/tests/unit/custom_auth/oauth/test_unit_oauth_views.py +++ b/api/tests/unit/custom_auth/oauth/test_unit_oauth_views.py @@ -9,6 +9,7 @@ from organisations.invites.models import Invite from organisations.models import Organisation +from users.models import SignUpType @mock.patch("custom_auth.oauth.serializers.get_user_info") @@ -66,7 +67,13 @@ def test_can_register_with_google_with_invite_if_registration_disabled( Invite.objects.create(organisation=organisation, email=email) # When - response = client.post(url, data={"access_token": "some-token"}) + response = client.post( + url, + data={ + "access_token": "some-token", + "sign_up_type": SignUpType.INVITE_EMAIL.value, + }, + ) # Then assert response.status_code == status.HTTP_200_OK @@ -89,7 +96,13 @@ def test_can_register_with_github_with_invite_if_registration_disabled( Invite.objects.create(organisation=organisation, email=email) # When - response = client.post(url, data={"access_token": "some-token"}) + response = client.post( + url, + data={ + "access_token": "some-token", + "sign_up_type": SignUpType.INVITE_EMAIL.value, + }, + ) # Then assert response.status_code == status.HTTP_200_OK diff --git a/api/tests/unit/custom_auth/test_unit_custom_auth_serializer.py b/api/tests/unit/custom_auth/test_unit_custom_auth_serializer.py index 00f099e1ace6..010a861f30ab 100644 --- a/api/tests/unit/custom_auth/test_unit_custom_auth_serializer.py +++ b/api/tests/unit/custom_auth/test_unit_custom_auth_serializer.py @@ -1,7 +1,13 @@ +import pytest from django.test import RequestFactory from pytest_django.fixtures import SettingsWrapper +from rest_framework.exceptions import PermissionDenied +from custom_auth.constants import ( + USER_REGISTRATION_WITHOUT_INVITE_ERROR_MESSAGE, +) from custom_auth.serializers import CustomUserCreateSerializer +from organisations.invites.models import InviteLink from users.models import FFAdminUser, SignUpType user_dict = { @@ -70,6 +76,7 @@ def test_CustomUserCreateSerializer_calls_is_authentication_method_valid_correct def test_CustomUserCreateSerializer_allows_registration_if_sign_up_type_is_invite_link( + invite_link: InviteLink, db: None, settings: SettingsWrapper, rf: RequestFactory, @@ -80,6 +87,7 @@ def test_CustomUserCreateSerializer_allows_registration_if_sign_up_type_is_invit data = { **user_dict, "sign_up_type": SignUpType.INVITE_LINK.value, + "invite_hash": invite_link.hash, } serializer = CustomUserCreateSerializer( @@ -92,3 +100,48 @@ def test_CustomUserCreateSerializer_allows_registration_if_sign_up_type_is_invit # Then assert user + + +def test_invite_link_validation_mixin_validate_fails_if_invite_link_hash_not_provided( + settings: SettingsWrapper, + db: None, +) -> None: + # Given + settings.ALLOW_REGISTRATION_WITHOUT_INVITE = False + + serializer = CustomUserCreateSerializer( + data={ + **user_dict, + "sign_up_type": SignUpType.INVITE_LINK.value, + } + ) + + # When + with pytest.raises(PermissionDenied) as exc_info: + serializer.is_valid(raise_exception=True) + + # Then + assert exc_info.value.detail == USER_REGISTRATION_WITHOUT_INVITE_ERROR_MESSAGE + + +def test_invite_link_validation_mixin_validate_fails_if_invite_link_hash_not_valid( + invite_link: InviteLink, + settings: SettingsWrapper, +) -> None: + # Given + settings.ALLOW_REGISTRATION_WITHOUT_INVITE = False + + serializer = CustomUserCreateSerializer( + data={ + **user_dict, + "sign_up_type": SignUpType.INVITE_LINK.value, + "invite_hash": "invalid-hash", + } + ) + + # When + with pytest.raises(PermissionDenied) as exc_info: + serializer.is_valid(raise_exception=True) + + # Then + assert exc_info.value.detail == USER_REGISTRATION_WITHOUT_INVITE_ERROR_MESSAGE diff --git a/api/tests/unit/environments/identities/test_unit_identities_views.py b/api/tests/unit/environments/identities/test_unit_identities_views.py index bf9e030058d0..b1e31cdb50b2 100644 --- a/api/tests/unit/environments/identities/test_unit_identities_views.py +++ b/api/tests/unit/environments/identities/test_unit_identities_views.py @@ -1,7 +1,9 @@ import json import urllib +from typing import Any from unittest import mock +import pytest from core.constants import FLAGSMITH_UPDATED_AT_HEADER, STRING from django.test import override_settings from django.urls import reverse @@ -1143,20 +1145,31 @@ def test_post_identities__server_key_only_feature__server_key_auth__return_expec assert response.json()["flags"] +@pytest.mark.parametrize( + "identity_data", + [ + pytest.param( + {"identifier": "transient", "transient": True}, + id="new-identifier-transient-true", + ), + pytest.param({"identifier": ""}, id="blank-identifier"), + pytest.param({"identifier": None}, id="null-identifier"), + pytest.param({}, id="missing_identifier"), + ], +) def test_post_identities__transient__no_persistence( environment: Environment, api_client: APIClient, + identity_data: dict[str, Any], ) -> None: # Given - identifier = "transient" trait_key = "trait_key" api_client.credentials(HTTP_X_ENVIRONMENT_KEY=environment.api_key) url = reverse("api-v1:sdk-identities") data = { - "identifier": identifier, + **identity_data, "traits": [{"trait_key": trait_key, "trait_value": "bar"}], - "transient": True, } # When @@ -1166,10 +1179,74 @@ def test_post_identities__transient__no_persistence( # Then assert response.status_code == status.HTTP_200_OK - assert not Identity.objects.filter(identifier=identifier).exists() + assert not Identity.objects.exists() assert not Trait.objects.filter(trait_key=trait_key).exists() +@pytest.mark.parametrize( + "trait_transiency_data", + [ + pytest.param({"transient": True}, id="trait-transient-true"), + pytest.param({"transient": False}, id="trait-transient-false"), + pytest.param({}, id="trait-default"), + ], +) +def test_post_identities__existing__transient__no_persistence( + environment: Environment, + identity: Identity, + trait: Trait, + identity_featurestate: FeatureState, + api_client: APIClient, + trait_transiency_data: dict[str, Any], +) -> None: + # Given + feature_state_value = "identity override" + identity_featurestate.feature_state_value.string_value = feature_state_value + identity_featurestate.feature_state_value.save() + + trait_key = "trait_key" + + api_client.credentials(HTTP_X_ENVIRONMENT_KEY=environment.api_key) + url = reverse("api-v1:sdk-identities") + data = { + "identifier": identity.identifier, + "transient": True, + "traits": [ + {"trait_key": trait_key, "trait_value": "bar", **trait_transiency_data} + ], + } + + # When + response = api_client.post( + url, data=json.dumps(data), content_type="application/json" + ) + + # Then + assert response.status_code == status.HTTP_200_OK + response_json = response.json() + + # identity overrides are correctly loaded + assert response_json["flags"][0]["feature_state_value"] == feature_state_value + + # previously persisted traits not provided in the request + # are not marked as transient in the response + assert response_json["traits"][0]["trait_key"] == trait.trait_key + assert not response_json["traits"][0].get("transient") + + # every trait provided in the request for a transient identity + # is marked as transient + assert response_json["traits"][1]["trait_key"] == trait_key + assert response_json["traits"][1]["transient"] + + assert ( + persisted_trait := Trait.objects.filter( + identity=identity, trait_key=trait.trait_key + ).first() + ) + assert persisted_trait.trait_value == trait.trait_value + assert not Trait.objects.filter(identity=identity, trait_key=trait_key).exists() + + def test_post_identities__transient_traits__no_persistence( environment: Environment, api_client: APIClient, diff --git a/api/tests/unit/environments/test_unit_environments_views.py b/api/tests/unit/environments/test_unit_environments_views.py index 90760acec236..d5fec31fe8de 100644 --- a/api/tests/unit/environments/test_unit_environments_views.py +++ b/api/tests/unit/environments/test_unit_environments_views.py @@ -569,7 +569,7 @@ def test_view_environment_with_staff__query_count_is_expected( url = reverse("api-v1:environments:environment-list") data = {"project": project.id} - expected_query_count = 7 + expected_query_count = 9 # When with django_assert_num_queries(expected_query_count): response = staff_client.get(url, data=data, content_type="application/json") @@ -766,11 +766,13 @@ def test_audit_log_entry_created_when_environment_updated( def test_get_document( environment: Environment, project: Project, - admin_client_new: APIClient, + staff_client: APIClient, feature: Feature, segment: Segment, + with_environment_permissions: WithEnvironmentPermissionsCallable, ) -> None: # Given + with_environment_permissions([VIEW_ENVIRONMENT]) # and some sample data to make sure we're testing all of the document segment_rule = SegmentRule.objects.create( @@ -786,13 +788,28 @@ def test_get_document( ) # When - response = admin_client_new.get(url) + response = staff_client.get(url) # Then assert response.status_code == status.HTTP_200_OK assert response.json() +def test_cannot_get_environment_document_without_permission( + staff_client: APIClient, environment: Environment +) -> None: + # Given + url = reverse( + "api-v1:environments:environment-get-document", args=[environment.api_key] + ) + + # When + response = staff_client.get(url) + + # Then + assert response.status_code == status.HTTP_403_FORBIDDEN + + def test_get_all_trait_keys_for_environment_only_returns_distinct_keys( identity: Identity, admin_client_new: APIClient, diff --git a/api/tests/unit/features/test_unit_feature_external_resources_views.py b/api/tests/unit/features/test_unit_feature_external_resources_views.py index 901454b6add6..d69b05766c0e 100644 --- a/api/tests/unit/features/test_unit_feature_external_resources_views.py +++ b/api/tests/unit/features/test_unit_feature_external_resources_views.py @@ -75,17 +75,15 @@ def test_create_feature_external_resource( github_configuration: GithubConfiguration, github_repository: GithubRepository, post_request_mock: MagicMock, - mocker: MockerFixture, + mock_github_client_generate_token: MagicMock, ) -> None: # Given - mocker.patch( - "integrations.github.client.generate_token", - return_value="mocked_token", + repository_owner_name = ( + f"{github_repository.repository_owner}/{github_repository.repository_name}" ) - feature_external_resource_data = { "type": "GITHUB_ISSUE", - "url": "https://github.com/repoowner/repo-name/issues/35", + "url": f"https://github.com/{repository_owner_name}/issues/35", "feature": feature_with_value.id, "metadata": {"state": "open"}, } @@ -130,7 +128,7 @@ def test_create_feature_external_resource( ) ) post_request_mock.assert_called_with( - "https://api.github.com/repos/repoowner/repo-name/issues/35/comments", + f"https://api.github.com/repos/{repository_owner_name}/issues/35/comments", json={"body": f"{expected_comment_body}"}, headers={ "Accept": "application/vnd.github.v3+json", @@ -157,7 +155,7 @@ def test_create_feature_external_resource( # And When responses.add( method="GET", - url=f"{GITHUB_API_URL}repos/repoowner/repo-name/issues/35", + url=f"{GITHUB_API_URL}repos/{repository_owner_name}/issues/35", status=200, json={"title": "resource name", "state": "open"}, ) @@ -183,6 +181,66 @@ def test_create_feature_external_resource( ) +def test_cannot_create_feature_external_resource_with_an_invalid_gh_url( + admin_client_new: APIClient, + feature: Feature, + project: Project, + github_configuration: GithubConfiguration, + github_repository: GithubRepository, +) -> None: + # Given + feature_external_resource_data = { + "type": "GITHUB_ISSUE", + "url": "https://github.com/repoowner/repo-name/pull/1", + "feature": feature.id, + "metadata": {"state": "open"}, + } + + url = reverse( + "api-v1:projects:feature-external-resources-list", + kwargs={"project_pk": project.id, "feature_pk": feature.id}, + ) + + # When + response = admin_client_new.post( + url, data=feature_external_resource_data, format="json" + ) + + # Then + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["detail"] == "Invalid GitHub Issue/PR URL" + + +def test_cannot_create_feature_external_resource_with_an_incorrect_gh_type( + admin_client_new: APIClient, + feature: Feature, + project: Project, + github_configuration: GithubConfiguration, + github_repository: GithubRepository, +) -> None: + # Given + feature_external_resource_data = { + "type": "GITHUB_INCORRECT_TYPE", + "url": "https://github.com/repoowner/repo-name/pull/1", + "feature": feature.id, + "metadata": {"state": "open"}, + } + + url = reverse( + "api-v1:projects:feature-external-resources-list", + kwargs={"project_pk": project.id, "feature_pk": feature.id}, + ) + + # When + response = admin_client_new.post( + url, data=feature_external_resource_data, format="json" + ) + + # Then + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json()["detail"] == "Incorrect GitHub type" + + def test_cannot_create_feature_external_resource_when_doesnt_have_a_valid_github_integration( admin_client_new: APIClient, feature: Feature, @@ -297,11 +355,11 @@ def test_update_feature_external_resource( "integrations.github.client.generate_token", ) mock_generate_token.return_value = "mocked_token" - mock_generate_token.return_value = "mocked_token" feature_external_resource_data = { "type": "GITHUB_ISSUE", "url": "https://github.com/userexample/example-project-repo/issues/12", "feature": feature.id, + "metadata": '{"state": "open"}', } url = reverse( "api-v1:projects:feature-external-resources-detail", @@ -338,7 +396,7 @@ def test_delete_feature_external_resource( post_request_mock.assert_called_with( "https://api.github.com/repos/repositoryownertest/repositorynametest/issues/11/comments", json={ - "body": "### The feature flag `Test Feature1` was unlinked from the issue/PR" + "body": "**The feature flag `Test Feature1` was unlinked from the issue/PR**" }, headers={ "Accept": "application/vnd.github.v3+json", @@ -361,12 +419,9 @@ def test_get_feature_external_resources( github_configuration: GithubConfiguration, github_repository: GithubRepository, feature_external_resource: FeatureExternalResource, - mocker: MockerFixture, + mock_github_client_generate_token: MagicMock, ) -> None: # Given - mocker.patch( - "integrations.github.client.generate_token", - ) url = reverse( "api-v1:projects:feature-external-resources-list", kwargs={"project_pk": project.id, "feature_pk": feature.id}, @@ -446,7 +501,7 @@ def test_create_github_comment_on_feature_state_updated( ).updated_at.strftime(get_format("DATETIME_INPUT_FORMATS")[0]) expected_body_comment = ( - "Flagsmith Feature `Test Feature1` has been updated:\n" + "**Flagsmith Feature `Test Feature1` has been updated:**\n" + expected_default_body( project.id, environment.api_key, @@ -480,14 +535,9 @@ def test_create_github_comment_on_feature_was_deleted( github_repository: GithubRepository, feature_external_resource: FeatureExternalResource, post_request_mock: MagicMock, - mocker: MockerFixture, + mock_github_client_generate_token: MagicMock, ) -> None: # Given - mocker.patch( - "integrations.github.client.generate_token", - return_value="mocked_token", - ) - url = reverse( viewname="api-v1:projects:project-features-detail", kwargs={"project_pk": project.id, "pk": feature.id}, @@ -501,7 +551,7 @@ def test_create_github_comment_on_feature_was_deleted( post_request_mock.assert_called_with( "https://api.github.com/repos/repositoryownertest/repositorynametest/issues/11/comments", - json={"body": "### The Feature Flag `Test Feature1` was deleted"}, + json={"body": "**The Feature Flag `Test Feature1` was deleted**"}, headers={ "Accept": "application/vnd.github.v3+json", "X-GitHub-Api-Version": GITHUB_API_VERSION, @@ -518,18 +568,13 @@ def test_create_github_comment_on_segment_override_updated( github_configuration: GithubConfiguration, github_repository: GithubRepository, post_request_mock: MagicMock, - mocker: MockerFixture, environment: Environment, admin_client: APIClient, feature_with_value_external_resource: FeatureExternalResource, + mock_github_client_generate_token: MagicMock, ) -> None: # Given feature_state = segment_override_for_feature_with_value - mocker.patch( - "integrations.github.client.generate_token", - return_value="mocked_token", - ) - payload = dict(WritableNestedFeatureStateSerializer(instance=feature_state).data) payload["enabled"] = not feature_state.enabled @@ -549,7 +594,7 @@ def test_create_github_comment_on_segment_override_updated( ).updated_at.strftime(get_format("DATETIME_INPUT_FORMATS")[0]) expected_comment_body = ( - "Flagsmith Feature `feature_with_value` has been updated:\n" + "**Flagsmith Feature `feature_with_value` has been updated:**\n" + "\n" + expected_segment_comment_body( project.id, @@ -581,16 +626,11 @@ def test_create_github_comment_on_segment_override_deleted( github_configuration: GithubConfiguration, github_repository: GithubRepository, post_request_mock: MagicMock, - mocker: MockerFixture, admin_client_new: APIClient, feature_with_value_external_resource: FeatureExternalResource, + mock_github_client_generate_token: MagicMock, ) -> None: # Given - mocker.patch( - "integrations.github.client.generate_token", - return_value="mocked_token", - ) - url = reverse( viewname="api-v1:features:feature-segment-detail", kwargs={"pk": feature_with_value_segment.id}, @@ -606,7 +646,7 @@ def test_create_github_comment_on_segment_override_deleted( post_request_mock.assert_called_with( "https://api.github.com/repos/repositoryownertest/repositorynametest/issues/11/comments", json={ - "body": "### The Segment Override `segment` for Feature Flag `feature_with_value` was deleted" + "body": "**The Segment Override `segment` for Feature Flag `feature_with_value` was deleted**" }, headers={ "Accept": "application/vnd.github.v3+json", @@ -664,7 +704,7 @@ def test_create_github_comment_using_v2( response_data["updated_at"], format ).strftime(get_format("DATETIME_INPUT_FORMATS")[0]) expected_comment_body = ( - "Flagsmith Feature `Test Feature1` has been updated:\n" + "**Flagsmith Feature `Test Feature1` has been updated:**\n" + "\n" + expected_segment_comment_body( project.id, @@ -745,19 +785,17 @@ def test_create_feature_external_resource_on_environment_with_v2( segment_override_for_feature_with_value: FeatureState, environment_v2_versioning: Environment, post_request_mock: MagicMock, - mocker: MockerFixture, + mock_github_client_generate_token: MagicMock, ) -> None: # Given feature_id = segment_override_for_feature_with_value.feature_id - - mocker.patch( - "integrations.github.client.generate_token", - return_value="mocked_token", + repository_owner_name = ( + f"{github_repository.repository_owner}/{github_repository.repository_name}" ) feature_external_resource_data = { "type": "GITHUB_ISSUE", - "url": "https://github.com/repoowner/repo-name/issues/35", + "url": f"https://github.com/{repository_owner_name}/issues/35", "feature": feature_id, "metadata": {"state": "open"}, } @@ -804,7 +842,7 @@ def test_create_feature_external_resource_on_environment_with_v2( assert response.status_code == status.HTTP_201_CREATED post_request_mock.assert_called_with( - "https://api.github.com/repos/repoowner/repo-name/issues/35/comments", + f"https://api.github.com/repos/{repository_owner_name}/issues/35/comments", json={"body": f"{expected_comment_body}"}, headers={ "Accept": "application/vnd.github.v3+json", @@ -813,3 +851,37 @@ def test_create_feature_external_resource_on_environment_with_v2( }, timeout=10, ) + + +def test_cannot_create_feature_external_resource_for_the_same_feature_and_resource_uri( + admin_client_new: APIClient, + feature: Feature, + project: Project, + github_configuration: GithubConfiguration, + github_repository: GithubRepository, + feature_external_resource_gh_pr: FeatureExternalResource, +) -> None: + # Given + feature_external_resource_data = { + "type": "GITHUB_PR", + "url": "https://github.com/repositoryownertest/repositorynametest/pull/1", + "feature": feature.id, + "metadata": {"state": "open"}, + } + + url = reverse( + "api-v1:projects:feature-external-resources-list", + kwargs={"project_pk": project.id, "feature_pk": feature.id}, + ) + + # When + response = admin_client_new.post( + url, data=feature_external_resource_data, format="json" + ) + + # Then + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert ( + response.json()["non_field_errors"][0] + == "The fields feature, url must make a unique set." + ) diff --git a/api/tests/unit/features/test_unit_features_views.py b/api/tests/unit/features/test_unit_features_views.py index c0496b26f497..0226d4b1a615 100644 --- a/api/tests/unit/features/test_unit_features_views.py +++ b/api/tests/unit/features/test_unit_features_views.py @@ -425,6 +425,7 @@ def test_put_feature_does_not_update_feature_states( assert all(fs.enabled is False for fs in feature.feature_states.all()) +@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") @mock.patch("features.views.get_multiple_event_list_for_feature") def test_get_project_features_influx_data( mock_get_event_list: mock.MagicMock, @@ -446,6 +447,7 @@ def test_get_project_features_influx_data( "datetime": datetime(2021, 2, 26, 12, 0, 0, tzinfo=pytz.UTC), } ] + one_day_ago = timezone.now() - timedelta(days=1) # When response = admin_client_new.get(url) @@ -455,11 +457,70 @@ def test_get_project_features_influx_data( mock_get_event_list.assert_called_once_with( feature_name=feature.name, environment_id=str(environment.id), # provided as a GET param - date_start="-24h", # this is the default but can be provided as a GET param + date_start=one_day_ago, # this is the default but can be provided as a GET param aggregate_every="24h", # this is the default but can be provided as a GET param ) +@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") +@mock.patch("features.views.get_multiple_event_list_for_feature") +def test_get_project_features_influx_data_with_two_weeks_period( + mock_get_event_list: mock.MagicMock, + feature: Feature, + project: Project, + environment: Environment, + admin_client_new: APIClient, +) -> None: + # Given + base_url = reverse( + "api-v1:projects:project-features-get-influx-data", + args=[project.id, feature.id], + ) + url = f"{base_url}?environment_id={environment.id}&period=14d" + date_start = timezone.now() - timedelta(days=14) + + mock_get_event_list.return_value = [ + { + feature.name: 1, + "datetime": datetime(2021, 2, 26, 12, 0, 0, tzinfo=pytz.UTC), + } + ] + + # When + response = admin_client_new.get(url) + + # Then + assert response.status_code == status.HTTP_200_OK + mock_get_event_list.assert_called_once_with( + feature_name=feature.name, + environment_id=str(environment.id), + date_start=date_start, + aggregate_every="24h", + ) + + +@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") +def test_get_project_features_influx_data_with_malformed_period( + feature: Feature, + project: Project, + environment: Environment, + admin_client_new: APIClient, +) -> None: + # Given + base_url = reverse( + "api-v1:projects:project-features-get-influx-data", + args=[project.id, feature.id], + ) + url = f"{base_url}?environment_id={environment.id}&period=baddata" + + # When + response = admin_client_new.get(url) + + # Then + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data[0] == "Malformed period supplied" + + def test_regular_user_cannot_create_mv_options_when_creating_feature( staff_client: APIClient, with_project_permissions: WithProjectPermissionsCallable, diff --git a/api/tests/unit/integrations/github/test_unit_github_views.py b/api/tests/unit/integrations/github/test_unit_github_views.py index 07d3e9660f14..a1d9add23f36 100644 --- a/api/tests/unit/integrations/github/test_unit_github_views.py +++ b/api/tests/unit/integrations/github/test_unit_github_views.py @@ -1,10 +1,10 @@ import json from typing import Any +from unittest.mock import MagicMock import pytest import requests import responses -from django.conf import settings from django.urls import reverse from pytest_lazyfixture import lazy_fixture from pytest_mock import MockerFixture @@ -12,7 +12,9 @@ from rest_framework.response import Response from rest_framework.test import APIClient +from environments.models import Environment from features.feature_external_resources.models import FeatureExternalResource +from features.models import Feature from integrations.github.constants import GITHUB_API_URL from integrations.github.models import GithubConfiguration, GithubRepository from integrations.github.views import ( @@ -29,6 +31,17 @@ WEBHOOK_PAYLOAD_WITHOUT_INSTALLATION_ID = json.dumps( {"installation": {"test": 765432}, "action": "deleted"} ) +WEBHOOK_PAYLOAD_MERGED = json.dumps( + { + "pull_request": { + "id": 1234567, + "html_url": "https://github.com/repositoryownertest/repositorynametest/issues/11", + "merged": True, + }, + "action": "closed", + } +) + WEBHOOK_SIGNATURE = "sha1=57a1426e19cdab55dd6d0c191743e2958e50ccaa" WEBHOOK_SIGNATURE_WITH_AN_INVALID_INSTALLATION_ID = ( "sha1=081eef49d04df27552587d5df1c6b76e0fe20d21" @@ -36,6 +49,7 @@ WEBHOOK_SIGNATURE_WITHOUT_INSTALLATION_ID = ( "sha1=f99796bd3cebb902864e87ed960c5cca8772ff67" ) +WEBHOOK_MERGED_ACTION_SIGNATURE = "sha1=712ec7a5db14aad99d900da40738ebb9508ecad2" WEBHOOK_SECRET = "secret-key" @@ -246,11 +260,14 @@ def test_cannot_get_github_repository_when_github_pk_in_not_a_number( assert response.json() == {"github_pk": ["Must be an integer"]} +@responses.activate def test_create_github_repository( admin_client_new: APIClient, organisation: Organisation, github_configuration: GithubConfiguration, project: Project, + mocker: MockerFixture, + mock_github_client_generate_token: MagicMock, ) -> None: # Given data = { @@ -258,8 +275,16 @@ def test_create_github_repository( "repository_owner": "repositoryowner", "repository_name": "repositoryname", "project": project.id, + "tagging_enabled": True, } + responses.add( + method="POST", + url=f"{GITHUB_API_URL}repos/repositoryowner/repositoryname/labels", + status=status.HTTP_200_OK, + json={}, + ) + url = reverse( "api-v1:organisations:repositories-list", args=[organisation.id, github_configuration.id], @@ -272,6 +297,53 @@ def test_create_github_repository( assert GithubRepository.objects.filter(repository_owner="repositoryowner").exists() +@responses.activate +def test_create_github_repository_and_label_already_Existe( + admin_client_new: APIClient, + organisation: Organisation, + github_configuration: GithubConfiguration, + project: Project, + mocker: MockerFixture, + mock_github_client_generate_token: MagicMock, +) -> None: + # Given + mocker_logger = mocker.patch("integrations.github.client.logger") + + data = { + "github_configuration": github_configuration.id, + "repository_owner": "repositoryowner", + "repository_name": "repositoryname", + "project": project.id, + "tagging_enabled": True, + } + + mock_response = { + "message": "Validation Failed", + "errors": [{"resource": "Label", "code": "already_exists", "field": "name"}], + "documentation_url": "https://docs.github.com/rest/issues/labels#create-a-label", + "status": "422", + } + + responses.add( + method="POST", + url=f"{GITHUB_API_URL}repos/repositoryowner/repositoryname/labels", + status=status.HTTP_422_UNPROCESSABLE_ENTITY, + json=mock_response, + ) + + url = reverse( + "api-v1:organisations:repositories-list", + args=[organisation.id, github_configuration.id], + ) + # When + response = admin_client_new.post(url, data) + + # Then + mocker_logger.warning.assert_called_once_with("Label already exists") + assert response.status_code == status.HTTP_201_CREATED + assert GithubRepository.objects.filter(repository_owner="repositoryowner").exists() + + def test_cannot_create_github_repository_when_does_not_have_permissions( test_user_client: APIClient, organisation: Organisation, @@ -334,13 +406,9 @@ def test_github_delete_repository( github_configuration: GithubConfiguration, github_repository: GithubRepository, feature_external_resource: FeatureExternalResource, - mocker: MockerFixture, + mock_github_client_generate_token: MagicMock, ) -> None: # Given - mock_generate_token = mocker.patch( - "integrations.github.client.generate_token", - ) - mock_generate_token.return_value = "mocked_token" url = reverse( "api-v1:organisations:repositories-detail", args=[organisation.id, github_configuration.id, github_repository.id], @@ -409,14 +477,10 @@ def test_fetch_pull_requests( organisation: Organisation, github_configuration: GithubConfiguration, github_repository: GithubRepository, + mock_github_client_generate_token: MagicMock, mocker: MockerFixture, ) -> None: - # Given - mock_generate_token = mocker.patch( - "integrations.github.client.generate_token", - ) - mock_generate_token.return_value = "mocked_token" github_request_mock = mocker.patch( "requests.get", side_effect=mocked_requests_get_issues_and_pull_requests ) @@ -448,13 +512,10 @@ def test_fetch_issues( organisation: Organisation, github_configuration: GithubConfiguration, github_repository: GithubRepository, + mock_github_client_generate_token: MagicMock, mocker: MockerFixture, ) -> None: # Given - mock_generate_token = mocker.patch( - "integrations.github.client.generate_token", - ) - mock_generate_token.return_value = "mocked_token" github_request_mock = mocker.patch( "requests.get", side_effect=mocked_requests_get_issues_and_pull_requests ) @@ -491,13 +552,10 @@ def test_fetch_issues_returns_error_on_bad_response_from_github( organisation: Organisation, github_configuration: GithubConfiguration, github_repository: GithubRepository, + mock_github_client_generate_token: MagicMock, mocker: MockerFixture, ) -> None: # Given - mock_generate_token = mocker.patch( - "integrations.github.client.generate_token", - ) - mock_generate_token.return_value = "mocked_token" mocker.patch("requests.get", side_effect=mocked_requests_get_error) url = reverse("api-v1:organisations:get-github-issues", args=[organisation.id]) data = {"repo_owner": "owner", "repo_name": "repo"} @@ -519,13 +577,9 @@ def test_fetch_repositories( organisation: Organisation, github_configuration: GithubConfiguration, github_repository: GithubRepository, - mocker: MockerFixture, + mock_github_client_generate_token: MagicMock, ) -> None: # Given - mock_generate_token = mocker.patch( - "integrations.github.client.generate_token", - ) - mock_generate_token.return_value = "mocked_token" responses.add( method="GET", url=f"{GITHUB_API_URL}installation/repositories", @@ -573,13 +627,11 @@ def test_fetch_repositories( ], ) def test_fetch_issues_and_pull_requests_fails_with_status_400_when_integration_not_configured( - client: APIClient, organisation: Organisation, reverse_url: str, mocker + client: APIClient, + organisation: Organisation, + reverse_url: str, + mock_github_client_generate_token: MagicMock, ) -> None: - # Given - mock_generate_token = mocker.patch( - "integrations.github.client.generate_token", - ) - mock_generate_token.generate_token.return_value = "mocked_token" # When url = reverse(reverse_url, args=[organisation.id]) response = client.get(url) @@ -600,15 +652,9 @@ def test_cannot_fetch_issues_or_prs_when_does_not_have_permissions( organisation: Organisation, github_configuration: GithubConfiguration, github_repository: GithubRepository, - mocker, + mock_github_client_generate_token: MagicMock, reverse_url: str, ) -> None: - # Given - mock_generate_token = mocker.patch( - "integrations.github.client.generate_token", - ) - mock_generate_token.generate_token.return_value = "mocked_token" - # When url = reverse(reverse_url, args=[organisation.id]) response = test_user_client.get(url) @@ -656,9 +702,9 @@ def test_verify_github_webhook_payload_returns_false_on_no_signature_header() -> def test_github_webhook_delete_installation( api_client: APIClient, github_configuration: GithubConfiguration, + set_github_webhook_secret, ) -> None: # Given - settings.GITHUB_WEBHOOK_SECRET = WEBHOOK_SECRET url = reverse("api-v1:github-webhook") # When @@ -675,63 +721,88 @@ def test_github_webhook_delete_installation( assert not GithubConfiguration.objects.filter(installation_id=1234567).exists() -def test_github_webhook_with_non_existing_installation( +def test_github_webhook_merged_a_pull_request( api_client: APIClient, + feature: Feature, github_configuration: GithubConfiguration, + github_repository: GithubRepository, + feature_external_resource: FeatureExternalResource, + set_github_webhook_secret, +) -> None: + # Given + url = reverse("api-v1:github-webhook") + + # When + response = api_client.post( + path=url, + data=WEBHOOK_PAYLOAD_MERGED, + content_type="application/json", + HTTP_X_HUB_SIGNATURE=WEBHOOK_MERGED_ACTION_SIGNATURE, + HTTP_X_GITHUB_EVENT="pull_request", + ) + + # Then + feature.refresh_from_db() + assert response.status_code == status.HTTP_200_OK + assert feature.tags.first().label == "PR Merged" + + +def test_github_webhook_without_installation_id( + api_client: APIClient, mocker: MockerFixture, + set_github_webhook_secret, ) -> None: # Given - settings.GITHUB_WEBHOOK_SECRET = WEBHOOK_SECRET url = reverse("api-v1:github-webhook") mocker_logger = mocker.patch("integrations.github.github.logger") # When response = api_client.post( path=url, - data=WEBHOOK_PAYLOAD_WITH_AN_INVALID_INSTALLATION_ID, + data=WEBHOOK_PAYLOAD_WITHOUT_INSTALLATION_ID, content_type="application/json", - HTTP_X_HUB_SIGNATURE=WEBHOOK_SIGNATURE_WITH_AN_INVALID_INSTALLATION_ID, + HTTP_X_HUB_SIGNATURE=WEBHOOK_SIGNATURE_WITHOUT_INSTALLATION_ID, HTTP_X_GITHUB_EVENT="installation", ) # Then mocker_logger.error.assert_called_once_with( - "GitHub Configuration with installation_id 765432 does not exist" + "The installation_id is not present in the payload: {'installation': {'test': 765432}, 'action': 'deleted'}" ) assert response.status_code == status.HTTP_200_OK -def test_github_webhook_without_installation_id( +def test_github_webhook_with_non_existing_installation( api_client: APIClient, github_configuration: GithubConfiguration, mocker: MockerFixture, + set_github_webhook_secret, ) -> None: # Given - settings.GITHUB_WEBHOOK_SECRET = WEBHOOK_SECRET url = reverse("api-v1:github-webhook") mocker_logger = mocker.patch("integrations.github.github.logger") # When response = api_client.post( path=url, - data=WEBHOOK_PAYLOAD_WITHOUT_INSTALLATION_ID, + data=WEBHOOK_PAYLOAD_WITH_AN_INVALID_INSTALLATION_ID, content_type="application/json", - HTTP_X_HUB_SIGNATURE=WEBHOOK_SIGNATURE_WITHOUT_INSTALLATION_ID, + HTTP_X_HUB_SIGNATURE=WEBHOOK_SIGNATURE_WITH_AN_INVALID_INSTALLATION_ID, HTTP_X_GITHUB_EVENT="installation", ) # Then mocker_logger.error.assert_called_once_with( - "The installation_id is not present in the payload: {'installation': {'test': 765432}, 'action': 'deleted'}" + "GitHub Configuration with installation_id 765432 does not exist" ) assert response.status_code == status.HTTP_200_OK def test_github_webhook_fails_on_signature_header_missing( github_configuration: GithubConfiguration, + set_github_webhook_secret, ) -> None: # Given - settings.GITHUB_WEBHOOK_SECRET = WEBHOOK_SECRET url = reverse("api-v1:github-webhook") # When @@ -751,9 +822,9 @@ def test_github_webhook_fails_on_signature_header_missing( def test_github_webhook_fails_on_bad_signature_header_missing( github_configuration: GithubConfiguration, + set_github_webhook_secret, ) -> None: # Given - settings.GITHUB_WEBHOOK_SECRET = WEBHOOK_SECRET url = reverse("api-v1:github-webhook") # When @@ -774,9 +845,9 @@ def test_github_webhook_fails_on_bad_signature_header_missing( def test_github_webhook_bypass_event( github_configuration: GithubConfiguration, + set_github_webhook_secret, ) -> None: # Given - settings.GITHUB_WEBHOOK_SECRET = WEBHOOK_SECRET url = reverse("api-v1:github-webhook") # When @@ -800,15 +871,10 @@ def test_cannot_fetch_pull_requests_when_github_request_call_failed( organisation: Organisation, github_configuration: GithubConfiguration, github_repository: GithubRepository, - mocker, + mock_github_client_generate_token: MagicMock, ) -> None: - # Given data = {"repo_owner": "owner", "repo_name": "repo"} - mock_generate_token = mocker.patch( - "integrations.github.client.generate_token", - ) - mock_generate_token.return_value = "mocked_token" responses.add( method="GET", url=f"{GITHUB_API_URL}repos/{data['repo_owner']}/{data['repo_name']}/pulls", @@ -833,14 +899,10 @@ def test_cannot_fetch_pulls_when_the_github_response_was_invalid( organisation: Organisation, github_configuration: GithubConfiguration, github_repository: GithubRepository, - mocker, + mock_github_client_generate_token: MagicMock, ) -> None: # Given data = {"repo_owner": "owner", "repo_name": "repo"} - mock_generate_token = mocker.patch( - "integrations.github.client.generate_token", - ) - mock_generate_token.return_value = "mocked_token" responses.add( method="GET", url=f"{GITHUB_API_URL}repos/{data['repo_owner']}/{data['repo_name']}/pulls", @@ -877,7 +939,7 @@ def test_fetch_github_repo_contributors( organisation: Organisation, github_configuration: GithubConfiguration, github_repository: GithubRepository, - mocker: MockerFixture, + mock_github_client_generate_token: MagicMock, ) -> None: # Given url = reverse( @@ -905,11 +967,6 @@ def test_fetch_github_repo_contributors( expected_response = {"results": mocked_github_response} - mock_generate_token = mocker.patch( - "integrations.github.client.generate_token", - ) - mock_generate_token.return_value = "mocked_token" - # Add response for endpoint being tested responses.add( method=responses.GET, @@ -1063,3 +1120,85 @@ def test_send_the_invalid_type_page_or_page_size_param_returns_400( assert response.status_code == status.HTTP_400_BAD_REQUEST response_json = response.json() assert response_json == error_response + + +@responses.activate +def test_label_and_tags_no_added_when_tagging_is_disabled( + admin_client_new: APIClient, + project: Project, + environment: Environment, + github_repository: GithubRepository, + feature_with_value: Feature, + mock_github_client_generate_token: MagicMock, + post_request_mock: MagicMock, +) -> None: + # Given + github_repository.tagging_enabled = False + github_repository.save() + repository_owner_name = ( + f"{github_repository.repository_owner}/{github_repository.repository_name}" + ) + + feature_external_resource_data = { + "type": "GITHUB_ISSUE", + "url": f"https://github.com/{repository_owner_name}/issues/35", + "feature": feature_with_value.id, + "metadata": {"state": "open"}, + } + + url = reverse( + "api-v1:projects:feature-external-resources-list", + kwargs={"project_pk": project.id, "feature_pk": feature_with_value.id}, + ) + + # When + response = admin_client_new.post( + url, data=feature_external_resource_data, format="json" + ) + + # Then + assert response.status_code == status.HTTP_201_CREATED + assert feature_with_value.tags.count() == 0 + + +@responses.activate +def test_update_github_repository( + admin_client_new: APIClient, + organisation: Organisation, + github_configuration: GithubConfiguration, + github_repository: GithubRepository, + project: Project, + mocker: MockerFixture, + mock_github_client_generate_token: MagicMock, +) -> None: + # Given + github_repository.tagging_enabled = False + github_repository.save() + data = { + "github_configuration": github_configuration.id, + "repository_owner": "repositoryowner", + "repository_name": "repositoryname", + "project": project.id, + "tagging_enabled": True, + } + + responses.add( + method="POST", + url=f"{GITHUB_API_URL}repos/repositoryowner/repositoryname/labels", + status=status.HTTP_200_OK, + json={}, + ) + + url = reverse( + "api-v1:organisations:repositories-detail", + args=[organisation.id, github_configuration.id, github_repository.id], + ) + # When + response = admin_client_new.put(url, data) + + # Then + assert response.status_code == status.HTTP_200_OK + assert GithubRepository.objects.filter(repository_owner="repositoryowner").exists() + assert GithubRepository.objects.get( + repository_owner="repositoryowner" + ).tagging_enabled diff --git a/api/tests/unit/organisations/test_unit_organisations_subscription_info_cache.py b/api/tests/unit/organisations/test_unit_organisations_subscription_info_cache.py index bb59c1176b12..12523040d262 100644 --- a/api/tests/unit/organisations/test_unit_organisations_subscription_info_cache.py +++ b/api/tests/unit/organisations/test_unit_organisations_subscription_info_cache.py @@ -1,3 +1,7 @@ +from datetime import timedelta + +import pytest +from django.utils import timezone from task_processor.task_run_method import TaskRunMethod from organisations.chargebee.metadata import ChargebeeObjMetadata @@ -5,18 +9,27 @@ from organisations.subscriptions.constants import SubscriptionCacheEntity +@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") def test_update_caches(mocker, organisation, chargebee_subscription, settings): # Given settings.CHARGEBEE_API_KEY = "api-key" settings.INFLUXDB_TOKEN = "token" settings.TASK_RUN_METHOD = TaskRunMethod.SYNCHRONOUSLY - organisation_usage = {"24h": 25123, "7d": 182957, "30d": 804564} + now = timezone.now() + day_1 = now - timedelta(days=1) + day_7 = now - timedelta(days=7) + day_30 = now - timedelta(days=30) + organisation_usage = { + day_1: 25123, + day_7: 182957, + day_30: 804564, + } mocked_get_top_organisations = mocker.patch( "organisations.subscription_info_cache.get_top_organisations" ) mocked_get_top_organisations.side_effect = lambda t, _: { - organisation.id: organisation_usage.get(f"{t[1:]}") + organisation.id: organisation_usage[t] } chargebee_metadata = ChargebeeObjMetadata(seats=15, api_calls=1000000) @@ -35,15 +48,15 @@ def test_update_caches(mocker, organisation, chargebee_subscription, settings): # Then assert ( organisation.subscription_information_cache.api_calls_24h - == organisation_usage["24h"] + == organisation_usage[day_1] ) assert ( organisation.subscription_information_cache.api_calls_7d - == organisation_usage["7d"] + == organisation_usage[day_7] ) assert ( organisation.subscription_information_cache.api_calls_30d - == organisation_usage["30d"] + == organisation_usage[day_30] ) assert ( organisation.subscription_information_cache.allowed_seats @@ -60,7 +73,7 @@ def test_update_caches(mocker, organisation, chargebee_subscription, settings): assert mocked_get_top_organisations.call_count == 3 assert [call[0] for call in mocked_get_top_organisations.call_args_list] == [ - ("-30d", ""), - ("-7d", ""), - ("-24h", "100"), + (day_30, ""), + (day_7, ""), + (day_1, "100"), ] diff --git a/api/tests/unit/organisations/test_unit_organisations_tasks.py b/api/tests/unit/organisations/test_unit_organisations_tasks.py index 62708193a94a..4d1a38dd9520 100644 --- a/api/tests/unit/organisations/test_unit_organisations_tasks.py +++ b/api/tests/unit/organisations/test_unit_organisations_tasks.py @@ -362,11 +362,28 @@ def test_handle_api_usage_notifications_below_100( organisation=organisation, ).exists() + # Create an OrganisationApiUsageNotification object for another organisation + # to verify that only the correct organisation's notifications are taken into + # account. + another_organisation = Organisation.objects.create(name="Another Organisation") + OrganisationAPIUsageNotification.objects.create( + organisation=another_organisation, + percent_usage=100, + notified_at=now - timedelta(days=1), + ) + # When handle_api_usage_notifications() # Then - mock_api_usage.assert_called_once_with(organisation.id, "-14d") + assert len(mock_api_usage.call_args_list) == 2 + + # We only care about the call for the main organisation, + # not the call for 'another_organisation' + assert mock_api_usage.call_args_list[0].args == ( + organisation.id, + now - timedelta(days=14), + ) assert len(mailoutbox) == 1 email = mailoutbox[0] @@ -410,7 +427,12 @@ def test_handle_api_usage_notifications_below_100( ).count() == 1 ) - assert OrganisationAPIUsageNotification.objects.first() == api_usage_notification + assert ( + OrganisationAPIUsageNotification.objects.filter( + organisation=organisation + ).first() + == api_usage_notification + ) @pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") @@ -452,7 +474,7 @@ def test_handle_api_usage_notifications_below_api_usage_alert_thresholds( handle_api_usage_notifications() # Then - mock_api_usage.assert_called_once_with(organisation.id, "-14d") + mock_api_usage.assert_called_once_with(organisation.id, now - timedelta(days=14)) assert len(mailoutbox) == 0 @@ -502,7 +524,7 @@ def test_handle_api_usage_notifications_above_100( handle_api_usage_notifications() # Then - mock_api_usage.assert_called_once_with(organisation.id, "-14d") + mock_api_usage.assert_called_once_with(organisation.id, now - timedelta(days=14)) assert len(mailoutbox) == 1 email = mailoutbox[0] @@ -612,6 +634,7 @@ def test_handle_api_usage_notifications_for_free_accounts( mailoutbox: list[EmailMultiAlternatives], ) -> None: # Given + now = timezone.now() assert organisation.is_paid is False assert organisation.subscription.is_free_plan is True assert organisation.subscription.max_api_calls == MAX_API_CALLS_IN_FREE_PLAN @@ -634,14 +657,18 @@ def test_handle_api_usage_notifications_for_free_accounts( handle_api_usage_notifications() # Then - mock_api_usage.assert_called_once_with(organisation.id, "-30d") + mock_api_usage.assert_called_once_with(organisation.id, now - timedelta(days=30)) assert len(mailoutbox) == 1 email = mailoutbox[0] assert email.subject == "Flagsmith API use has reached 100%" assert email.body == render_to_string( "organisations/api_usage_notification_limit.txt", - context={"organisation": organisation, "matched_threshold": 100}, + context={ + "organisation": organisation, + "matched_threshold": 100, + "grace_period": True, + }, ) assert len(email.alternatives) == 1 @@ -650,7 +677,11 @@ def test_handle_api_usage_notifications_for_free_accounts( assert email.alternatives[0][0] == render_to_string( "organisations/api_usage_notification_limit.html", - context={"organisation": organisation, "matched_threshold": 100}, + context={ + "organisation": organisation, + "matched_threshold": 100, + "grace_period": True, + }, ) assert email.from_email == "noreply@flagsmith.com" diff --git a/api/tests/unit/organisations/test_unit_organisations_views.py b/api/tests/unit/organisations/test_unit_organisations_views.py index de129f3c9fc1..9ce266788f35 100644 --- a/api/tests/unit/organisations/test_unit_organisations_views.py +++ b/api/tests/unit/organisations/test_unit_organisations_views.py @@ -351,6 +351,7 @@ def test_user_can_get_projects_for_an_organisation( assert response.data[0]["name"] == project.name +@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") @mock.patch("app_analytics.influxdb_wrapper.influxdb_client") def test_should_get_usage_for_organisation( mock_influxdb_client: MagicMock, @@ -365,7 +366,7 @@ def test_should_get_usage_for_organisation( expected_query = ( ( f'from(bucket:"{read_bucket}") ' - "|> range(start: -30d, stop: now()) " + "|> range(start: 2022-12-20T09:09:47.325132+00:00, stop: 2023-01-19T09:09:47.325132+00:00) " '|> filter(fn:(r) => r._measurement == "api_call") ' '|> filter(fn: (r) => r["_field"] == "request_count") ' f'|> filter(fn: (r) => r["organisation_id"] == "{organisation.id}") ' diff --git a/api/tests/unit/projects/test_unit_projects_permissions.py b/api/tests/unit/projects/test_unit_projects_permissions.py index 8662d58bbd5f..f7b874ab0b81 100644 --- a/api/tests/unit/projects/test_unit_projects_permissions.py +++ b/api/tests/unit/projects/test_unit_projects_permissions.py @@ -1,3 +1,4 @@ +import os from unittest import mock import pytest @@ -58,6 +59,31 @@ def test_create_project_has_permission( assert response is True +def test_create_project_has_permission_with_e2e_test_auth_token( + staff_user: FFAdminUser, + organisation: Organisation, + with_organisation_permissions: WithOrganisationPermissionsCallable, +) -> None: + # Given + with_organisation_permissions([CREATE_PROJECT]) + mock_request = mock.MagicMock( + user=staff_user, data={"name": "Test", "organisation": organisation.id} + ) + token = "test-token" + settings.ENABLE_FE_E2E = True + os.environ["E2E_TEST_AUTH_TOKEN"] = token + + mock_request.META = {"E2E_TEST_AUTH_TOKEN": token} + mock_view = mock.MagicMock(action="create", detail=False) + project_permissions = ProjectPermissions() + + # When + response = project_permissions.has_permission(mock_request, mock_view) + + # Then + assert response is True + + def test_admin_can_update_project_has_permission( organisation: Organisation, staff_user: FFAdminUser, diff --git a/api/tests/unit/sales_dashboard/test_unit_sales_dashboard_views.py b/api/tests/unit/sales_dashboard/test_unit_sales_dashboard_views.py index 1ab1dc97fcf0..2cbeacd723cf 100644 --- a/api/tests/unit/sales_dashboard/test_unit_sales_dashboard_views.py +++ b/api/tests/unit/sales_dashboard/test_unit_sales_dashboard_views.py @@ -1,6 +1,9 @@ +from datetime import timedelta + import pytest from django.test import Client, RequestFactory from django.urls import reverse +from django.utils import timezone from pytest_django.fixtures import SettingsWrapper from pytest_mock import MockerFixture from rest_framework.test import APIClient @@ -39,6 +42,7 @@ def test_organisation_subscription_get_api_call_overage( assert result.overage == expected_overage +@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") def test_get_organisation_info__get_event_list_for_organisation( organisation: Organisation, superuser_client: APIClient, @@ -65,7 +69,8 @@ def test_get_organisation_info__get_event_list_for_organisation( # Then assert "label1" in str(response.content) assert "label2" in str(response.content) - event_list_mock.assert_called_once_with(organisation.id, "-180d", "now()") + date_start = timezone.now() - timedelta(days=180) + event_list_mock.assert_called_once_with(organisation.id, date_start) def test_list_organisations_search_by_name( diff --git a/api/tests/unit/segments/test_migrations.py b/api/tests/unit/segments/test_migrations.py deleted file mode 100644 index f2b181a81165..000000000000 --- a/api/tests/unit/segments/test_migrations.py +++ /dev/null @@ -1,104 +0,0 @@ -import pytest -from django.conf import settings as test_settings -from django_test_migrations.migrator import Migrator -from flag_engine.segments import constants -from pytest_django.fixtures import SettingsWrapper - - -@pytest.mark.skipif( - test_settings.SKIP_MIGRATION_TESTS is True, - reason="Skip migration tests to speed up tests where necessary", -) -def test_create_whitelisted_segments_migration( - migrator: Migrator, - settings: SettingsWrapper, -) -> None: - # Given - The migration state is at 0020 (before the migration we want to test). - old_state = migrator.apply_initial_migration( - ("segments", "0020_detach_segment_from_project_cascade_delete") - ) - - Organisation = old_state.apps.get_model("organisations", "Organisation") - Project = old_state.apps.get_model("projects", "Project") - SegmentRule = old_state.apps.get_model("segments", "SegmentRule") - Segment = old_state.apps.get_model("segments", "Segment") - Condition = old_state.apps.get_model("segments", "Condition") - - # Set the limit lower to allow for a faster test. - settings.SEGMENT_RULES_CONDITIONS_LIMIT = 3 - - # Next, create the setup data. - organisation = Organisation.objects.create(name="Big Corp Incorporated") - project = Project.objects.create(name="Huge Project", organisation=organisation) - - segment_1 = Segment.objects.create(name="Segment1", project=project) - segment_2 = Segment.objects.create(name="Segment1", project=project) - segment_rule_1 = SegmentRule.objects.create( - segment=segment_1, - type="ALL", - ) - - # Subnested segment rules. - segment_rule_2 = SegmentRule.objects.create( - rule=segment_rule_1, - type="ALL", - ) - segment_rule_3 = SegmentRule.objects.create( - rule=segment_rule_1, - type="ALL", - ) - - # Lonely segment rules for pass criteria for segment_2. - segment_rule_4 = SegmentRule.objects.create( - segment=segment_2, - type="ALL", - ) - segment_rule_5 = SegmentRule.objects.create( - rule=segment_rule_4, - type="ALL", - ) - - Condition.objects.create( - operator=constants.EQUAL, - property="age", - value="21", - rule=segment_rule_2, - ) - Condition.objects.create( - operator=constants.GREATER_THAN, - property="height", - value="210", - rule=segment_rule_2, - ) - Condition.objects.create( - operator=constants.GREATER_THAN, - property="waist", - value="36", - rule=segment_rule_3, - ) - Condition.objects.create( - operator=constants.LESS_THAN, - property="shoes", - value="12", - rule=segment_rule_3, - ) - - # Sole criteria for segment_2 conditions. - Condition.objects.create( - operator=constants.LESS_THAN, - property="toy_count", - value="7", - rule=segment_rule_5, - ) - - # When we run the migration. - new_state = migrator.apply_tested_migration( - ("segments", "0021_create_whitelisted_segments") - ) - - # Then the first segment is in the whitelist while the second is not. - NewSegment = new_state.apps.get_model("segments", "Segment") - new_segment_1 = NewSegment.objects.get(id=segment_1.id) - new_segment_2 = NewSegment.objects.get(id=segment_2.id) - assert new_segment_1.whitelisted_segment - assert getattr(new_segment_2, "whitelisted_segment", None) is None diff --git a/api/tests/unit/segments/test_unit_segments_migrations.py b/api/tests/unit/segments/test_unit_segments_migrations.py new file mode 100644 index 000000000000..b6f85808bba5 --- /dev/null +++ b/api/tests/unit/segments/test_unit_segments_migrations.py @@ -0,0 +1,239 @@ +import uuid + +import pytest +from django.conf import settings as test_settings +from django_test_migrations.migrator import Migrator +from flag_engine.segments import constants +from pytest_django.fixtures import SettingsWrapper + + +@pytest.mark.skipif( + test_settings.SKIP_MIGRATION_TESTS is True, + reason="Skip migration tests to speed up tests where necessary", +) +def test_create_whitelisted_segments_migration( + migrator: Migrator, + settings: SettingsWrapper, +) -> None: + # Given - The migration state is at 0020 (before the migration we want to test). + old_state = migrator.apply_initial_migration( + ("segments", "0020_detach_segment_from_project_cascade_delete") + ) + + Organisation = old_state.apps.get_model("organisations", "Organisation") + Project = old_state.apps.get_model("projects", "Project") + SegmentRule = old_state.apps.get_model("segments", "SegmentRule") + Segment = old_state.apps.get_model("segments", "Segment") + Condition = old_state.apps.get_model("segments", "Condition") + + # Set the limit lower to allow for a faster test. + settings.SEGMENT_RULES_CONDITIONS_LIMIT = 3 + + # Next, create the setup data. + organisation = Organisation.objects.create(name="Big Corp Incorporated") + project = Project.objects.create(name="Huge Project", organisation=organisation) + + segment_1 = Segment.objects.create(name="Segment1", project=project) + segment_2 = Segment.objects.create(name="Segment1", project=project) + segment_rule_1 = SegmentRule.objects.create( + segment=segment_1, + type="ALL", + ) + + # Subnested segment rules. + segment_rule_2 = SegmentRule.objects.create( + rule=segment_rule_1, + type="ALL", + ) + segment_rule_3 = SegmentRule.objects.create( + rule=segment_rule_1, + type="ALL", + ) + + # Lonely segment rules for pass criteria for segment_2. + segment_rule_4 = SegmentRule.objects.create( + segment=segment_2, + type="ALL", + ) + segment_rule_5 = SegmentRule.objects.create( + rule=segment_rule_4, + type="ALL", + ) + + Condition.objects.create( + operator=constants.EQUAL, + property="age", + value="21", + rule=segment_rule_2, + ) + Condition.objects.create( + operator=constants.GREATER_THAN, + property="height", + value="210", + rule=segment_rule_2, + ) + Condition.objects.create( + operator=constants.GREATER_THAN, + property="waist", + value="36", + rule=segment_rule_3, + ) + Condition.objects.create( + operator=constants.LESS_THAN, + property="shoes", + value="12", + rule=segment_rule_3, + ) + + # Sole criteria for segment_2 conditions. + Condition.objects.create( + operator=constants.LESS_THAN, + property="toy_count", + value="7", + rule=segment_rule_5, + ) + + # When we run the migration. + new_state = migrator.apply_tested_migration( + ("segments", "0021_create_whitelisted_segments") + ) + + # Then the first segment is in the whitelist while the second is not. + NewSegment = new_state.apps.get_model("segments", "Segment") + new_segment_1 = NewSegment.objects.get(id=segment_1.id) + new_segment_2 = NewSegment.objects.get(id=segment_2.id) + assert new_segment_1.whitelisted_segment + assert getattr(new_segment_2, "whitelisted_segment", None) is None + + +@pytest.mark.skipif( + test_settings.SKIP_MIGRATION_TESTS is True, + reason="Skip migration tests to speed up tests where necessary", +) +def test_add_versioning_to_segments_forwards(migrator: Migrator) -> None: + # Given - The migration state is at 0021 (before the migration we want to test). + old_state = migrator.apply_initial_migration( + ("segments", "0022_add_soft_delete_to_segment_rules_and_conditions") + ) + + Organisation = old_state.apps.get_model("organisations", "Organisation") + Project = old_state.apps.get_model("projects", "Project") + SegmentRule = old_state.apps.get_model("segments", "SegmentRule") + Segment = old_state.apps.get_model("segments", "Segment") + Condition = old_state.apps.get_model("segments", "Condition") + + # Next, create the setup data. + organisation = Organisation.objects.create(name="Test Org") + project = Project.objects.create(name="Test Project", organisation_id=organisation.id) + + segment = Segment.objects.create(name="Segment1", project_id=project.id) + segment_rule_1 = SegmentRule.objects.create( + segment_id=segment.id, + type="ALL", + ) + + # Subnested segment rules. + segment_rule_2 = SegmentRule.objects.create( + rule_id=segment_rule_1.id, + type="ALL", + ) + + Condition.objects.create( + operator=constants.EQUAL, + property="age", + value="21", + rule_id=segment_rule_2.id, + ) + + # When we run the migration. + new_state = migrator.apply_tested_migration( + ("segments", "0023_add_versioning_to_segments") + ) + + # Then the version_of attribute is correctly set. + NewSegment = new_state.apps.get_model("segments", "Segment") + new_segment = NewSegment.objects.get(id=segment.id) + assert new_segment.version_of == new_segment + + +@pytest.mark.skipif( + test_settings.SKIP_MIGRATION_TESTS is True, + reason="Skip migration tests to speed up tests where necessary", +) +def test_add_versioning_to_segments_reverse(migrator: Migrator) -> None: + # Given - The migration state is at 0023 (after the migration we want to test). + old_state = migrator.apply_initial_migration( + ("segments", "0023_add_versioning_to_segments") + ) + + Organisation = old_state.apps.get_model("organisations", "Organisation") + Project = old_state.apps.get_model("projects", "Project") + SegmentRule = old_state.apps.get_model("segments", "SegmentRule") + Segment = old_state.apps.get_model("segments", "Segment") + Condition = old_state.apps.get_model("segments", "Condition") + + # Next, create the setup data. + organisation = Organisation.objects.create(name="Test Org") + project = Project.objects.create(name="Test Project", organisation=organisation) + + # Set the version manually since this is normally done via a lifecycle hook + # that doesn't run for models created in a migration state. + segment = Segment.objects.create(name="Segment1", project=project, version=1) + segment_rule_1 = SegmentRule.objects.create( + segment=segment, + type="ALL", + ) + + # We ideally want to call Segment.deep_clone but that's not + # possible when working in a migration state. As such, we + # do the basic amount necessary from that method to allow + # us to test the migration behaviour. + def _deep_clone(segment: Segment) -> Segment: + cloned_segment = Segment.objects.create( + name=segment.name, + project_id=segment.project_id, + description=segment.description, + feature=segment.feature, + uuid=uuid.uuid4(), + version_of_id=segment.id, + ) + + segment.version += 1 + segment.save() + + return cloned_segment + + version_1 = _deep_clone(segment) + version_2 = _deep_clone(segment) + + version_3 = segment + + # Subnested segment rules. + segment_rule_2 = SegmentRule.objects.create( + rule=segment_rule_1, + type="ALL", + ) + + Condition.objects.create( + operator=constants.EQUAL, + property="age", + value="21", + rule=segment_rule_2, + ) + + # When we run the migration in reverse. + new_state = migrator.apply_tested_migration( + ("segments", "0022_add_soft_delete_to_segment_rules_and_conditions") + ) + + # Then any historical versions of the segment are deleted. + NewSegment = new_state.apps.get_model("segments", "Segment") + + new_segment_v1 = NewSegment.objects.get(id=version_1.id) + assert new_segment_v1.deleted_at is not None + + new_segment_v2 = NewSegment.objects.get(id=version_2.id) + assert new_segment_v2.deleted_at is not None + + new_segment_v3 = NewSegment.objects.get(id=version_3.id) + assert new_segment_v3.deleted_at is None diff --git a/api/webhooks/webhooks.py b/api/webhooks/webhooks.py index 75684e886614..e314b5976f9f 100644 --- a/api/webhooks/webhooks.py +++ b/api/webhooks/webhooks.py @@ -39,9 +39,6 @@ class WebhookEventType(enum.Enum): FLAG_DELETED = "FLAG_DELETED" AUDIT_LOG_CREATED = "AUDIT_LOG_CREATED" NEW_VERSION_PUBLISHED = "NEW_VERSION_PUBLISHED" - FEATURE_EXTERNAL_RESOURCE_ADDED = "FEATURE_EXTERNAL_RESOURCE_ADDED" - FEATURE_EXTERNAL_RESOURCE_REMOVED = "FEATURE_EXTERNAL_RESOURCE_REMOVED" - SEGMENT_OVERRIDE_DELETED = "SEGMENT_OVERRIDE_DELETED" class WebhookType(enum.Enum): diff --git a/docs/docs/deployment/hosting/aptible.md b/docs/docs/deployment/hosting/aptible.md new file mode 100644 index 000000000000..92c0c135dd52 --- /dev/null +++ b/docs/docs/deployment/hosting/aptible.md @@ -0,0 +1,75 @@ +--- +title: Aptible +--- + +## Prerequisites + +The options and health check routes described in this document are available from Flagsmith 2.130.0. + +## Configuration + +Running Flagsmith on Aptible requires some configuration tweaks because of how Aptible's application lifecycle works: + +- Don't wait for the database to be available before the Flagsmith API starts. You can do this by setting the + `SKIP_WAIT_FOR_DB` environment variable. +- Add `containers` as an allowed host to comply with Aptible's + [strict health checks](https://www.aptible.com/docs/core-concepts/apps/connecting-to-apps/app-endpoints/https-endpoints/health-checks#strict-health-checks). +- Use the `before_release` tasks from `.aptible.yml` to run database migrations +- Use a Procfile to only start the API and not perform database migrations on startup + +This configuration can be applied by adding the Procfile and `.aptible.yml` configuration files to a +[Docker image](https://www.aptible.com/docs/core-concepts/apps/deploying-apps/image/deploying-with-docker-image/overview#how-do-i-deploy-from-docker-image) +that you build starting from a Flagsmith base image: + +```text title="Procfile" +cmd: serve +``` + +```yaml title=".aptible.yml" +before_release: + - migrate + - bootstrap +``` + +```dockerfile title="Dockerfile" +# Use flagsmith/flagsmith-private-cloud for the Enterprise image +FROM --platform=linux/amd64 flagsmith/flagsmith + +# Don't wait for the database to be available during startup for health checks to succeed +ENV SKIP_WAIT_FOR_DB=1 + +# Use root user to add Aptible files to the container +USER root +RUN mkdir /.aptible/ +ADD Procfile /.aptible/Procfile +ADD .aptible.yml /.aptible/.aptible.yml + +# Use non-root user at runtime +USER nobody +``` + +Before deploying, set the environment variables for your database URL and allowed hosts from the Aptible dashboard, or +using the Aptible CLI: + +```shell +aptible config:set --app flagsmith \ + DATABASE_URL=postgresql://aptible:...@...:23532/db \ + DJANGO_ALLOWED_HOSTS='containers,YOUR_APTIBLE_HOSTNAME' +``` + +## Deployment + +After your image is built and pushed to a container registry that Aptible can access, you can deploy it using the +Aptible CLI as you would any other application: + +```shell +aptible deploy --app flagsmith --docker-image example/my-flagsmith-aptible-image +``` + +Once Flagsmith is running in Aptible, make sure to create the first admin user by visiting `/api/v1/users/config/init/`. + +## Limitations + +The steps described in this document do not deploy the +[asynchronous task processor](/deployment/configuration/task-processor), which may affect performance in production +workloads. diff --git a/docs/docs/deployment/hosting/locally-edge-proxy.md b/docs/docs/deployment/hosting/locally-edge-proxy.md index 5423e39c9525..29fa55191166 100644 --- a/docs/docs/deployment/hosting/locally-edge-proxy.md +++ b/docs/docs/deployment/hosting/locally-edge-proxy.md @@ -294,7 +294,7 @@ domain name and you're good to go. For example, lets say you had your proxy runn above: ```bash -curl "http://localhost:8000/api/v1/flags" -H "x-environment-key: 95DybY5oJoRNhxPZYLrxk4" | jq +curl "http://localhost:8000/api/v1/flags/" -H "x-environment-key: 95DybY5oJoRNhxPZYLrxk4" | jq [ { diff --git a/docs/docs/system-administration/rbac.md b/docs/docs/system-administration/rbac.md index f838fcf84355..b57fcc3d6e85 100644 --- a/docs/docs/system-administration/rbac.md +++ b/docs/docs/system-administration/rbac.md @@ -1,100 +1,173 @@ --- -title: Role Based Access Control +title: Role-based access control --- -Flagsmith provides fine-grained permissions to help larger teams manage access and roles across organisations, projects -and environments. - :::info -The Permissions/Role Based Access features of Flagsmith are _not_ part of the Open Source version. If you want to use -these features as part of a self hosted/on premise solution, please [get in touch](https://flagsmith.com/contact-us/). +Role-based access control requires an [Enterprise subscription](https://www.flagsmith.com/pricing). ::: -## Users, Groups, and Roles +Role-based access control (RBAC) provides fine-grained access management of Flagsmith resources. Using RBAC, you can +ensure users only have the access they need within your Flagsmith organisation. -Permissions can be assigned to Flagsmith individual users, groups, or roles. +For example, RBAC allows you to achieve the following scenarios: -### Users +- Only allow certain users to modify your production environments. +- Grant a default set of permissions to all users that join your Flagsmith organisation. +- Lock down an [Admin API](/clients/rest/#private-admin-api-endpoints) key to a specific set of permissions. +- Provide Flagsmith permissions based on your enterprise identity provider's groups when using + [SAML single sign-on](/system-administration/authentication/SAML/). -Flagsmith Users can be defined as Organisation Administrators or Users. Organisation Administrator is effectively a -super-user role, and gives full read/write access to every Project, Environment, Flag, Remote Config and Segment within -that Organisation. +To add users to your Flagsmith organisation or to manage user permissions, click on your organisation name in the top +left and open the **Users and Permissions** tab. -Users that are not Organisation Administrators must have permissions assigned to them manually at the relevant levels. +## Core concepts -### Groups +The diagram below shows an overview of how permissions are assigned within your Flagsmith organisation: -Groups are a convenient way to manage permissions for multiple Flagsmith users. Groups can contain any number of -Flagsmith users. You can create groups with the Organisation Settings page. +
+```mermaid +graph LR; + R[Custom roles] -->|Assigned to| G[Groups]; + B[Built-in role] -->|Assigned to| U[Users]; + R -->|Assigned to| U; + R -->|Assigned to| A[Admin API keys]; + G -->|Contains many| U; +``` -Members of a group can be designated as an admin for that group. As a group admin, users can manage the membership for -that group, but not the permissions the group has on other entities. +
### Roles -A _Role_ is an entity to which you can attach a set of permissions. Permissions can allow privileges at Organization, -Project, and Environment levels. You can assign a role, along with its associated permissions, to a User or Group. You -will also be able to assign API keys to a Role in future versions. +A role is a set of permissions that, when assigned, allows performing specific actions on your organisation, projects or +project environments. + +**Built-in roles** are predefined by Flagsmith and cannot be modified. All users in your organisation have one of the +following built-in roles: + +- _Organisation Administrator_ grants full access to everything in your Flagsmith organisation. +- _User_ grants no access and requires you to assign permissions using custom roles and/or groups. + +**Custom roles** can be assigned to users, groups or [Admin API](/clients/rest/#private-admin-api-endpoints) keys. Any +number of custom roles can be created and assigned. + +Creating, modifying or assigning roles requires organisation administrator permissions. + +### Groups + +A group is a collection of users. If a custom role is assigned to a group, the role's permissions will be granted to all +group members. Users can belong to any number of groups. + +Creating or modifying existing groups requires organisation administrator permissions. + +Permissions to add or remove users from groups can be granted in two ways: + +- The _manage group membership_ permission allows modifying any group's membership +- A _group admin_ can manage membership only for that group + +## Add users to your organisation + +You can add users to your organisation by sending them an invitation email from Flagsmith, or by sharing an invitation +link directly with them. Both options require organisation administrator permissions, and are available from **Users and +Permissions > Members**. + +Users can also join your organisation directly by logging in to Flagsmith using +[single sign-on](/system-administration/authentication/SAML/). + +### Email invites + +:::info + +If you are self-hosting Flagsmith, you must +[configure an email provider](/deployment/hosting/locally-api#email-environment-variables) before using email invites. + +::: + +To send invitation emails to specific users, click on **Invite members**. Then, fill in the email address and built-in +role of each user you want to invite. + +When a user accepts their email invitation, they will be prompted to sign up for a Flagsmith account, or they can choose +to log in if they already have an account with the same email address. + +Users who have not yet accepted their invitations are listed in the "Pending invites" section at the bottom of this +page. From here you can also resend or revoke any pending invitations. + +### Invitation links + +:::warning + +Anyone with an invitation link can join your Flagsmith organisation at any time. Share these links with caution and +regenerate them if they are compromised. + +::: -#### Creating a Role +Direct links to join your organisation can be found in the **Team Members** section of this page. One direct link is +available for each built-in role that users will have when joining your organisation. -You can create a Role in the Organisation Settings page. +## Provision permissions -#### Add Permissions to a Role +If a user joins your organisation with the built-in _User_ role, they will not have any permissions to view or change +anything in your Flagsmith organisation. You can provide default fine-grained permissions to users with any of these +options: -Once the role is created you can assign the corresponding permissions. +- Add users by default to a group. When creating or editing a group, select the **Add new users by default** option. + When a user logs in for the first time to your organisation, they will automatically be added to all groups that have + this option enabled. +- [Use existing groups from your enterprise identity provider](/system-administration/authentication/SAML/#using-groups-from-your-saml-idp). + Any time a user logs in using single sign-on, they will be made a member of any groups with matching external IDs. -**E.g. Add Project permission:** +## Deprecated features -- Choose a Role. -- Go to the Projects tab. -- Select a Project and enable the relevant permissions. +Groups can grant permissions directly to their members in the same way that roles do. This functionality was deprecated +in Flagsmith 2.137.0. To grant permissions to all members of a group, create a role with the desired permissions and +assign it to the group instead. -### Assign Role to Users or Groups +Assigning roles to groups has several benefits over assigning permissions directly to a group: -After creating the Role, you can assign it to Users or Groups. +- Roles can be assigned to Admin API keys, but Admin API keys cannot belong to groups. +- If you need multiple groups or users with similar permissions, the common permissions can be defined in a role and + assigned to multiple groups or users instead of being duplicated. +- Having roles as the single place where permissions are defined makes auditing permissions easier. -**E.g. Assign role to a user:** +## Permissions reference -- Choose a role. -- Go to the Members tab. -- Select the Users tab. -- Click assign role to user button and select a user. +Permissions can be assigned at four levels: user group, organisation, project, and environment. -## Permissions +### User group -Permissions can be assigned at 3 levels: Organisation, Project, and Environment. +| Permission | Ability | +| ----------- | ------------------------------------------------ | +| Group Admin | Allows adding or removing users from this group. | ### Organisation -| **Permission** | **Ability** | -| ------------------ | --------------------------------------------------------------------------- | -| Create Project | Allows the user to create Projects in the given Organisation | -| Manage User Groups | Allows the user to manage the Groups in the Organisation and their members. | +| Permission | Ability | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | +| Create Project | Allows creating projects in the organisation. Users are automatically granted Administrator permissions on any projects they create. | +| Manage User Groups | Allows adding or removing users from any group. | ### Project -| **Permission** | **Ability** | -| ------------------ | ------------------------------------------------------------------------------------------ | -| Administrator | Full Read/Write over all Environments, Feature Flag, Remote Config, Segment and Tag values | -| View Project | Can view the Project within their account | -| Create Environment | Can create new Environments within the Project | -| Create Feature | Can create a new Feature / Remote Config | -| Delete Feature | Can remove an existing Feature / Remote Config entirely from the Project | -| Manage Segments | Can create, delete and edit Segments within the Project | -| View audit log | Allows the user to view the audit logs for this Project. | +| Permission | Ability | +| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | +| Administrator | Grants full read and write access to all environments, features and segments. | +| View Project | Allows viewing this project. The project is hidden from users without this permission. | +| Create Environment | Allows creating new environments in this project. Users are automatically granted Administrator permissions on any environments they create. | +| Create Feature | Allows creating new features in all environments. | +| Delete Feature | Allows deleting features from all environments. | +| Manage Segments | Grants write access to segments in this project. | +| View audit log | Allows viewing all audit log entries for this project. | ### Environment -| **Permission** | **Ability** | -| ------------------------ | --------------------------------------------------------------- | -| Administrator | Can modify Feature Flag, Remote Config and Segment values | -| View Environment | Can see the Environment within their account | -| Update Feature State | Update the state or value for a given feature | -| Manage Identities | View and update Identities | -| Manage Segment Overrides | Permission to manage segment overrides in the given environment | -| Create Change Request | Creating a new Change Request | -| Approve Change Request | Approving or denying existing Change Requests | -| View Identities | Viewing Identities | +| Permission | Ability | +| ------------------------ | ----------------------------------------------------------------------------------------------------------------------- | +| Administrator | Grants full read and write access to all feature states, overrides, identities and change requests in this environment. | +| View Environment | Allows viewing this environment. The environment is hidden from users without this permission. | +| Update Feature State | Allows updating updating any feature state or values in this environment. | +| Manage Identities | Grants read and write access to identities in this environment. | +| Manage Segment Overrides | Grants write access to segment overrides in this environment. | +| Create Change Request | Allows creating change requests for features in this environment. | +| Approve Change Request | Allows approving or denying change requests in this environment. | +| View Identities | Grants read-only access to identities in this environment. | diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index fc921b527bc0..69bbabd26d13 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -12,6 +12,11 @@ const config = { onBrokenLinks: 'throw', onBrokenMarkdownLinks: 'warn', + markdown: { + mermaid: true, + }, + themes: ['@docusaurus/theme-mermaid'], + themeConfig: /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ ({ diff --git a/docs/package-lock.json b/docs/package-lock.json index 2dd9af1290ce..85cf48e9f44f 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -11,6 +11,7 @@ "@docusaurus/core": "^3.4.0", "@docusaurus/plugin-google-tag-manager": "^3.4.0", "@docusaurus/preset-classic": "^3.4.0", + "@docusaurus/theme-mermaid": "^3.4.0", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "prism-react-renderer": "^2.3.0", @@ -2182,6 +2183,12 @@ "node": ">=6.9.0" } }, + "node_modules/@braintree/sanitize-url": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz", + "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==", + "license": "MIT" + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -2678,6 +2685,28 @@ "react-dom": "^18.0.0" } }, + "node_modules/@docusaurus/theme-mermaid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-mermaid/-/theme-mermaid-3.4.0.tgz", + "integrity": "sha512-3w5QW0HEZ2O6x2w6lU3ZvOe1gNXP2HIoKDMJBil1VmLBc9PmpAG17VmfhI/p3L2etNmOiVs5GgniUqvn8AFEGQ==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.4.0", + "@docusaurus/module-type-aliases": "3.4.0", + "@docusaurus/theme-common": "3.4.0", + "@docusaurus/types": "3.4.0", + "@docusaurus/utils-validation": "3.4.0", + "mermaid": "^10.4.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/@docusaurus/theme-search-algolia": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.4.0.tgz", @@ -3475,6 +3504,27 @@ "@types/node": "*" } }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz", + "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==", + "license": "MIT" + }, + "node_modules/@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==", + "license": "MIT" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -5248,6 +5298,15 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", @@ -5587,514 +5646,561 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, - "node_modules/debounce": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", - "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" + "node_modules/cytoscape": { + "version": "3.30.2", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.30.2.tgz", + "integrity": "sha512-oICxQsjW8uSaRmn4UK/jkczKOqTrVqt5/1WL0POiJUT2EKNc9STM4hYFHv917yu55aTBMFNRzymlJhVAiWPCxw==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" }, "engines": { - "node": ">=6.0" + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "engines": { + "node": ">=12" } }, - "node_modules/decko": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decko/-/decko-1.2.0.tgz", - "integrity": "sha512-m8FnyHXV1QX+S1cl+KPFDIl6NMkxtKsy6+U/aYyjrOqWMuwAwYWu7ePqrsUHtDR5Y8Yk2pi/KIDSgF+vT4cPOQ==" + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } }, - "node_modules/decode-named-character-reference": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", - "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", "dependencies": { - "character-entities": "^2.0.0" + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "engines": { + "node": ">=12" } }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", "dependencies": { - "mimic-response": "^3.1.0" + "d3-path": "1 - 3" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/decompress-response/node_modules/mimic-response": { + "node_modules/d3-color": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", "engines": { - "node": ">=10" + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=12" } }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, "engines": { - "node": ">=4.0.0" + "node": ">=12" } }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", "engines": { - "node": ">=0.10.0" + "node": ">=12" } }, - "node_modules/default-gateway": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", - "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", "dependencies": { - "execa": "^5.0.0" + "d3-dispatch": "1 - 3", + "d3-selection": "3" }, "engines": { - "node": ">= 10" + "node": ">=12" } }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, "engines": { - "node": ">=10" + "node": ">=12" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" + "d3-dsv": "1 - 3" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=12" } }, - "node_modules/del": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", - "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", "dependencies": { - "globby": "^11.0.1", - "graceful-fs": "^4.2.4", - "is-glob": "^4.0.1", - "is-path-cwd": "^2.2.0", - "is-path-inside": "^3.0.2", - "p-map": "^4.0.0", - "rimraf": "^3.0.2", - "slash": "^3.0.0" + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", "engines": { - "node": ">= 0.8" + "node": ">=12" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, "engines": { - "node": ">=6" + "node": ">=12" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" - }, - "node_modules/detect-port": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.5.1.tgz", - "integrity": "sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ==", - "dependencies": { - "address": "^1.0.1", - "debug": "4" - }, - "bin": { - "detect": "bin/detect-port.js", - "detect-port": "bin/detect-port.js" + "node": ">=12" } }, - "node_modules/detect-port-alt": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", - "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", "dependencies": { - "address": "^1.0.1", - "debug": "^2.6.0" - }, - "bin": { - "detect": "bin/detect-port", - "detect-port": "bin/detect-port" + "d3-color": "1 - 3" }, "engines": { - "node": ">= 4.2.1" + "node": ">=12" } }, - "node_modules/detect-port-alt/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" } }, - "node_modules/detect-port-alt/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" } }, - "node_modules/dir-glob": { + "node_modules/d3-quadtree": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dependencies": { - "path-type": "^4.0.0" - }, + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==" + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } }, - "node_modules/dns-packet": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", - "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", "dependencies": { - "@leichtgewicht/ip-codec": "^2.0.1" - }, - "engines": { - "node": ">=6" + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" } }, - "node_modules/dom-converter": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", - "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", "dependencies": { - "utila": "~0.4" + "internmap": "^1.0.0" } }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + "d3-path": "1" } }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", "dependencies": { - "domelementtype": "^2.3.0" + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" }, "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" + "node": ">=12" } }, - "node_modules/dompurify": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.7.tgz", - "integrity": "sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ==" - }, - "node_modules/domutils": { + "node_modules/d3-scale-chromatic": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" + "engines": { + "node": ">=12" } }, - "node_modules/dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" } }, - "node_modules/dot-prop": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", - "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", "dependencies": { - "is-obj": "^2.0.0" + "d3-path": "^3.1.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/dot-prop/node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/electron-to-chromium": { - "version": "1.4.722", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.722.tgz", - "integrity": "sha512-5nLE0TWFFpZ80Crhtp4pIp8LXCztjYX41yUcV6b+bKR2PqzjskTMOOlBi1VjBHlvHwS+4gar7kNKOrsbsewEZQ==" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" - }, - "node_modules/emojilib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", - "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==" - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, "engines": { - "node": ">= 4" + "node": ">=12" } }, - "node_modules/emoticon": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/emoticon/-/emoticon-4.0.1.tgz", - "integrity": "sha512-dqx7eA9YaqyvYtUhJwT4rC1HIp82j5ybS1/vQ42ur+jBe17dJMwZE4+gvL1XadSFfxaPFFGt3Xsw+Y8akThDlw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" } }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, "engines": { - "node": ">= 0.8" + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" } }, - "node_modules/enhanced-resolve": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", - "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" }, "engines": { - "node": ">=10.13.0" + "node": ">=12" } }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "node_modules/dagre-d3-es": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz", + "integrity": "sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==", + "license": "MIT", + "dependencies": { + "d3": "^7.8.2", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dayjs": { + "version": "1.11.12", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.12.tgz", + "integrity": "sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==", + "license": "MIT" + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, "engines": { - "node": ">=0.12" + "node": ">=6.0" }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "node_modules/decko": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decko/-/decko-1.2.0.tgz", + "integrity": "sha512-m8FnyHXV1QX+S1cl+KPFDIl6NMkxtKsy6+U/aYyjrOqWMuwAwYWu7ePqrsUHtDR5Y8Yk2pi/KIDSgF+vT4cPOQ==" + }, + "node_modules/decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", "dependencies": { - "is-arrayish": "^0.2.1" + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dependencies": { - "get-intrinsic": "^1.2.4" + "mimic-response": "^3.1.0" }, "engines": { - "node": ">= 0.4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", - "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==" - }, - "node_modules/es6-promise": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", - "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==" - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-goat": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", - "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "engines": { "node": ">=10" }, @@ -6102,272 +6208,171 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "engines": { - "node": ">=8.0.0" + "node": ">=4.0.0" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", "dependencies": { - "estraverse": "^5.2.0" + "execa": "^5.0.0" }, "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "engines": { - "node": ">=4.0" + "node": ">= 10" } }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", "engines": { - "node": ">=4.0" + "node": ">=10" } }, - "node_modules/estree-util-attach-comments": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", - "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { - "@types/estree": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-build-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", - "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "estree-walker": "^3.0.0" + "engines": { + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-is-identifier-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", - "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/estree-util-to-js": { + "node_modules/define-lazy-prop": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", - "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "astring": "^1.8.0", - "source-map": "^0.7.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "engines": { + "node": ">=8" } }, - "node_modules/estree-util-value-to-estree": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.1.1.tgz", - "integrity": "sha512-5mvUrF2suuv5f5cGDnDphIy4/gW86z82kl5qG6mM9z04SEQI4FB5Apmaw/TGEf3l55nLtMs5s51dmhUzvAHQCA==", + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dependencies": { - "@types/estree": "^1.0.0", - "is-plain-obj": "^4.0.0" + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/remcohaszing" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/estree-util-visit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", - "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "node_modules/del": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", + "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/unist": "^3.0.0" + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", "dependencies": { - "@types/estree": "^1.0.0" + "robust-predicates": "^3.0.2" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "engines": { - "node": ">=0.10.0" + "node": ">= 0.8" } }, - "node_modules/eta": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/eta/-/eta-2.2.0.tgz", - "integrity": "sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g==", + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "engines": { - "node": ">=6.0.0" - }, - "funding": { - "url": "https://github.com/eta-dev/eta?sponsor=1" + "node": ">=6" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "engines": { - "node": ">= 0.6" + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/eval": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/eval/-/eval-0.1.8.tgz", - "integrity": "sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==", - "dependencies": { - "@types/node": "*", - "require-like": ">= 0.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "engines": { - "node": ">=0.8.x" - } + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "node_modules/detect-port": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.5.1.tgz", + "integrity": "sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ==", "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" + "address": "^1.0.1", + "debug": "4" }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" + "bin": { + "detect": "bin/detect-port.js", + "detect-port": "bin/detect-port.js" } }, - "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "node_modules/detect-port-alt": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", + "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.2", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "address": "^1.0.1", + "debug": "^2.6.0" }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/express/node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" + "bin": { + "detect": "bin/detect-port", + "detect-port": "bin/detect-port" }, "engines": { - "node": ">= 0.6" + "node": ">= 4.2.1" } }, - "node_modules/express/node_modules/debug": { + "node_modules/detect-port-alt/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", @@ -6375,834 +6380,1411 @@ "ms": "2.0.0" } }, - "node_modules/express/node_modules/ms": { + "node_modules/detect-port-alt/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, - "node_modules/express/node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "license": "BSD-3-Clause", "engines": { - "node": ">= 0.6" + "node": ">=0.3.1" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dependencies": { - "is-extendable": "^0.1.0" + "path-type": "^4.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "node_modules/dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==" }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "@leichtgewicht/ip-codec": "^2.0.1" }, "engines": { - "node": ">=8.6.0" + "node": ">=6" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" - }, - "node_modules/fast-url-parser": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", - "integrity": "sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==", + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", "dependencies": { - "punycode": "^1.3.2" + "utila": "~0.4" } }, - "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dependencies": { - "reusify": "^1.0.4" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "node_modules/fault": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", - "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dependencies": { - "format": "^0.2.0" + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "node_modules/dompurify": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.7.tgz", + "integrity": "sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ==" + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", "dependencies": { - "websocket-driver": ">=0.5.1" + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" }, - "engines": { - "node": ">=0.8.0" + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/feed": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz", - "integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==", + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", "dependencies": { - "xml-js": "^1.6.11" - }, - "engines": { - "node": ">=0.4.0" + "no-case": "^3.0.4", + "tslib": "^2.0.3" } }, - "node_modules/file-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "node_modules/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" + "is-obj": "^2.0.0" }, "engines": { - "node": ">= 10.13.0" + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/file-loader/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, + "node_modules/dot-prop/node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/electron-to-chromium": { + "version": "1.4.722", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.722.tgz", + "integrity": "sha512-5nLE0TWFFpZ80Crhtp4pIp8LXCztjYX41yUcV6b+bKR2PqzjskTMOOlBi1VjBHlvHwS+4gar7kNKOrsbsewEZQ==" + }, + "node_modules/elkjs": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", + "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==", + "license": "EPL-2.0" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/emoticon": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/emoticon/-/emoticon-4.0.1.tgz", + "integrity": "sha512-dqx7eA9YaqyvYtUhJwT4rC1HIp82j5ybS1/vQ42ur+jBe17dJMwZE4+gvL1XadSFfxaPFFGt3Xsw+Y8akThDlw==", "funding": { "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/file-loader/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "peerDependencies": { - "ajv": "^6.9.1" + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" } }, - "node_modules/file-loader/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "node_modules/file-loader/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "node_modules/enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=10.13.0" } }, - "node_modules/filesize": { - "version": "8.0.7", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", - "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "engines": { - "node": ">= 0.4.0" + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" + "is-arrayish": "^0.2.1" } }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" + "get-intrinsic": "^1.2.4" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.4" } }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" } }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "node_modules/es-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", + "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==" }, - "node_modules/find-cache-dir": { + "node_modules/es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==" + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-goat": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", - "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", - "dependencies": { - "common-path-prefix": "^3.0.0", - "pkg-dir": "^7.0.0" - }, + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", + "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", "engines": { - "node": ">=14.16" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", - "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" - }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "bin": { - "flat": "cli.js" + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" } }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } + "engines": { + "node": ">=4" } }, - "node_modules/foreach": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz", - "integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==" - }, - "node_modules/fork-ts-checker-webpack-plugin": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz", - "integrity": "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==", + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dependencies": { - "@babel/code-frame": "^7.8.3", - "@types/json-schema": "^7.0.5", - "chalk": "^4.1.0", - "chokidar": "^3.4.2", - "cosmiconfig": "^6.0.0", - "deepmerge": "^4.2.2", - "fs-extra": "^9.0.0", - "glob": "^7.1.6", - "memfs": "^3.1.2", - "minimatch": "^3.0.4", - "schema-utils": "2.7.0", - "semver": "^7.3.2", - "tapable": "^1.0.0" + "estraverse": "^5.2.0" }, "engines": { - "node": ">=10", - "yarn": ">=1.0.0" - }, - "peerDependencies": { - "eslint": ">= 6", - "typescript": ">= 2.7", - "vue-template-compiler": "*", - "webpack": ">= 4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - }, - "vue-template-compiler": { - "optional": true - } + "node": ">=4.0" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "peerDependencies": { - "ajv": "^6.9.1" + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "engines": { + "node": ">=4.0" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" + "@types/estree": "^1.0.0" }, - "engines": { - "node": ">=8" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" }, - "engines": { - "node": ">=10" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", - "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", "dependencies": { - "@types/json-schema": "^7.0.4", - "ajv": "^6.12.2", - "ajv-keywords": "^3.4.1" - }, - "engines": { - "node": ">= 8.9.0" + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/webpack" + "url": "https://opencollective.com/unified" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "engines": { - "node": ">=6" + "node_modules/estree-util-value-to-estree": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.1.1.tgz", + "integrity": "sha512-5mvUrF2suuv5f5cGDnDphIy4/gW86z82kl5qG6mM9z04SEQI4FB5Apmaw/TGEf3l55nLtMs5s51dmhUzvAHQCA==", + "dependencies": { + "@types/estree": "^1.0.0", + "is-plain-obj": "^4.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" } }, - "node_modules/form-data-encoder": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", - "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", - "engines": { - "node": ">= 14.17" + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/format": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", - "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", - "engines": { - "node": ">=0.4.x" + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dependencies": { + "@types/estree": "^1.0.0" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "engines": { - "node": ">= 0.6" + "node": ">=0.10.0" } }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "node_modules/eta": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-2.2.0.tgz", + "integrity": "sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g==", "engines": { - "node": "*" + "node": ">=6.0.0" }, "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" + "url": "https://github.com/eta-dev/eta?sponsor=1" } }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "engines": { "node": ">= 0.6" } }, - "node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "node_modules/eval": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eval/-/eval-0.1.8.tgz", + "integrity": "sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "@types/node": "*", + "require-like": ">= 0.1.1" }, "engines": { - "node": ">=14.14" - } - }, - "node_modules/fs-monkey": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz", - "integrity": "sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.8" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "engines": { - "node": ">=6.9.0" - } + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "engines": { - "node": "6.* || 8.* || >= 10.*" + "node": ">=0.8.x" } }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-own-enumerable-property-symbols": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", - "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==" - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/github-slugger": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", - "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==" - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" }, "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">= 0.10.0" } }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/express/node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/express/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dependencies": { - "is-glob": "^4.0.1" + "safe-buffer": "5.2.1" }, "engines": { - "node": ">= 6" + "node": ">= 0.6" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" - }, - "node_modules/global-dirs": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", - "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dependencies": { - "ini": "2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "ms": "2.0.0" } }, - "node_modules/global-dirs/node_modules/ini": { + "node_modules/express/node_modules/ms": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", - "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/express/node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "engines": { - "node": ">=10" + "node": ">= 0.6" } }, - "node_modules/global-modules": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "dependencies": { - "global-prefix": "^3.0.0" + "is-extendable": "^0.1.0" }, "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dependencies": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" }, "engines": { - "node": ">=6" + "node": ">=8.6.0" } }, - "node_modules/global-prefix/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, + "node_modules/fast-url-parser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", + "integrity": "sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==", + "dependencies": { + "punycode": "^1.3.2" } }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "engines": { - "node": ">=4" + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dependencies": { + "reusify": "^1.0.4" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "node_modules/fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" + "format": "^0.2.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", "dependencies": { - "get-intrinsic": "^1.1.3" + "websocket-driver": ">=0.5.1" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=0.8.0" } }, - "node_modules/got": { - "version": "12.6.1", - "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", - "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", + "node_modules/feed": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz", + "integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==", "dependencies": { - "@sindresorhus/is": "^5.2.0", - "@szmarczak/http-timer": "^5.0.1", - "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.8", - "decompress-response": "^6.0.0", - "form-data-encoder": "^2.1.2", - "get-stream": "^6.0.1", - "http2-wrapper": "^2.1.10", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^3.0.0", - "responselike": "^3.0.0" + "xml-js": "^1.6.11" }, "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" + "node": ">=0.4.0" } }, - "node_modules/got/node_modules/@sindresorhus/is": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", - "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, "engines": { - "node": ">=14.16" + "node": ">= 10.13.0" }, "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, - "node_modules/gray-matter": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", - "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "node_modules/file-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dependencies": { - "js-yaml": "^3.13.1", - "kind-of": "^6.0.2", - "section-matter": "^1.0.0", - "strip-bom-string": "^1.0.0" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, - "engines": { - "node": ">=6.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/gray-matter/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dependencies": { - "sprintf-js": "~1.0.2" + "node_modules/file-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "peerDependencies": { + "ajv": "^6.9.1" } }, - "node_modules/gray-matter/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } + "node_modules/file-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, - "node_modules/gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "node_modules/file-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dependencies": { - "duplexer": "^0.1.2" + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" }, "engines": { - "node": ">=10" + "node": ">= 10.13.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/filesize": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz", + "integrity": "sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ==", "engines": { - "node": ">=8" + "node": ">= 0.4.0" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { - "es-define-property": "^1.0.0" + "to-regex-range": "^5.0.1" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 0.8" } }, - "node_modules/has-yarn": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-3.0.0.tgz", - "integrity": "sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" } }, - "node_modules/hasown": { + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreach": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz", + "integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==" + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.3.tgz", + "integrity": "sha512-SbH/l9ikmMWycd5puHJKTkZJKddF4iRLyW3DeZ08HTI7NGyLS38MXd/KGgeWumQO7YNQbW2u/NtPT2YowbPaGQ==", + "dependencies": { + "@babel/code-frame": "^7.8.3", + "@types/json-schema": "^7.0.5", + "chalk": "^4.1.0", + "chokidar": "^3.4.2", + "cosmiconfig": "^6.0.0", + "deepmerge": "^4.2.2", + "fs-extra": "^9.0.0", + "glob": "^7.1.6", + "memfs": "^3.1.2", + "minimatch": "^3.0.4", + "schema-utils": "2.7.0", + "semver": "^7.3.2", + "tapable": "^1.0.0" + }, + "engines": { + "node": ">=10", + "yarn": ">=1.0.0" + }, + "peerDependencies": { + "eslint": ">= 6", + "typescript": ">= 2.7", + "vue-template-compiler": "*", + "webpack": ">= 4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + }, + "vue-template-compiler": { + "optional": true + } + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz", + "integrity": "sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==", + "dependencies": { + "@types/json-schema": "^7.0.4", + "ajv": "^6.12.2", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/tapable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "engines": { + "node": ">= 14.17" + } + }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz", + "integrity": "sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==" + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/github-slugger": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", + "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-dirs/node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "12.6.1", + "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", + "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/got/node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-yarn": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-3.0.0.tgz", + "integrity": "sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasown": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", @@ -7814,6 +8396,15 @@ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/interpret": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", @@ -8263,6 +8854,31 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/katex": { + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.11.tgz", + "integrity": "sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -8271,6 +8887,11 @@ "json-buffer": "3.0.1" } }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -8287,355 +8908,733 @@ "node": ">=6" } }, - "node_modules/latest-version": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", - "integrity": "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==", + "node_modules/latest-version": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", + "integrity": "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==", + "dependencies": { + "package-json": "^8.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/launch-editor": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", + "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/load-script": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz", + "integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==" + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==" + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==" + }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-table": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", + "integrity": "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/mdast-util-directive": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.0.0.tgz", + "integrity": "sha512-JUpYOqKI4mM3sZcNxmF/ox04XYFFkNwr0CFlrQIkCwbvH0xzMCqkMqAde9wRd80VAhaUrwFwKm2nxretdT1h7Q==", "dependencies": { - "package-json": "^8.1.0" - }, - "engines": { - "node": ">=14.16" + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-visit-parents": "^6.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/launch-editor": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", - "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz", + "integrity": "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==", "dependencies": { - "picocolors": "^1.0.0", - "shell-quote": "^1.8.1" + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "engines": { - "node": ">=6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lilconfig": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", - "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", - "engines": { - "node": ">=14" + "node_modules/mdast-util-from-markdown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.1.tgz", + "integrity": "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" }, "funding": { - "url": "https://github.com/sponsors/antonk52" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + "node_modules/mdast-util-from-markdown/node_modules/micromark-util-symbol": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", + "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] }, - "node_modules/load-script": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz", - "integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==" + "node_modules/mdast-util-frontmatter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz", + "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "escape-string-regexp": "^5.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "node_modules/mdast-util-frontmatter/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "engines": { - "node": ">=6.11.5" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "node_modules/mdast-util-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz", + "integrity": "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==", "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, - "engines": { - "node": ">=8.9.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.0.tgz", + "integrity": "sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg==", "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" - }, - "node_modules/longest-streak": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-character": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", + "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-symbol": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", + "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz", + "integrity": "sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==", "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" }, - "bin": { - "loose-envify": "cli.js" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/lowercase-keys": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", - "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", "dependencies": { - "yallist": "^3.0.2" + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/lunr": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", - "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==" - }, - "node_modules/mark.js": { - "version": "8.11.1", - "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", - "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==" - }, - "node_modules/markdown-extensions": { + "node_modules/mdast-util-gfm-task-list-item": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", - "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", - "engines": { - "node": ">=16" + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/markdown-table": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", - "integrity": "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==", + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/marked": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", - "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", - "bin": { - "marked": "bin/marked.js" + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.0.tgz", + "integrity": "sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, - "engines": { - "node": ">= 12" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mdast-util-directive": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.0.0.tgz", - "integrity": "sha512-JUpYOqKI4mM3sZcNxmF/ox04XYFFkNwr0CFlrQIkCwbvH0xzMCqkMqAde9wRd80VAhaUrwFwKm2nxretdT1h7Q==", + "node_modules/mdast-util-mdx-jsx": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.2.tgz", + "integrity": "sha512-eKMQDeywY2wlHc97k5eD8VC+9ASMjN8ItEZQNGwJ6E0XWKiW/Z0V5/H8pvoXUf+y+Mj0VIgeRRbujBmFn4FTyA==", "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", - "devlop": "^1.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", - "unist-util-visit-parents": "^6.0.0" + "unist-util-remove-position": "^5.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/mdast-util-find-and-replace": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz", - "integrity": "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==", + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", - "escape-string-regexp": "^5.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "engines": { - "node": ">=12" + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/mdast-util-from-markdown": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.1.tgz", - "integrity": "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==", + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", "dependencies": { + "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "decode-named-character-reference": "^1.0.0", + "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", - "micromark": "^4.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-stringify-position": "^4.0.0" + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/mdast-util-from-markdown/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/mdast-util-frontmatter": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz", - "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==", + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "escape-string-regexp": "^5.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "micromark-extension-frontmatter": "^2.0.0" + "@types/mdast": "^4.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/mdast-util-frontmatter/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 8" } }, - "node_modules/mdast-util-gfm": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz", - "integrity": "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==", + "node_modules/mermaid": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.9.1.tgz", + "integrity": "sha512-Mx45Obds5W1UkW1nv/7dHRsbfMM1aOKA2+Pxs/IGHNonygDHwmng8xTHyS9z4KWVi0rbko8gjiBmuwwXQ7tiNA==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^6.0.1", + "@types/d3-scale": "^4.0.3", + "@types/d3-scale-chromatic": "^3.0.0", + "cytoscape": "^3.28.1", + "cytoscape-cose-bilkent": "^4.1.0", + "d3": "^7.4.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.10", + "dayjs": "^1.11.7", + "dompurify": "^3.0.5", + "elkjs": "^0.9.0", + "katex": "^0.16.9", + "khroma": "^2.0.0", + "lodash-es": "^4.17.21", + "mdast-util-from-markdown": "^1.3.0", + "non-layered-tidy-tree-layout": "^2.0.2", + "stylis": "^4.1.3", + "ts-dedent": "^2.2.0", + "uuid": "^9.0.0", + "web-worker": "^1.2.0" + } + }, + "node_modules/mermaid/node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/mermaid/node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==", + "license": "MIT" + }, + "node_modules/mermaid/node_modules/dompurify": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz", + "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==", + "license": "(MPL-2.0 OR Apache-2.0)" + }, + "node_modules/mermaid/node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "license": "MIT", "dependencies": { - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-gfm-autolink-literal": "^2.0.0", - "mdast-util-gfm-footnote": "^2.0.0", - "mdast-util-gfm-strikethrough": "^2.0.0", - "mdast-util-gfm-table": "^2.0.0", - "mdast-util-gfm-task-list-item": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/mdast-util-gfm-autolink-literal": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.0.tgz", - "integrity": "sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg==", + "node_modules/mermaid/node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0", - "ccount": "^2.0.0", - "devlop": "^1.0.0", - "mdast-util-find-and-replace": "^3.0.0", - "micromark-util-character": "^2.0.0" + "@types/mdast": "^3.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" } }, - "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "node_modules/mermaid/node_modules/micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", "funding": [ { "type": "GitHub Sponsors", @@ -8646,15 +9645,31 @@ "url": "https://opencollective.com/unified" } ], + "license": "MIT", "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" } }, - "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "node_modules/mermaid/node_modules/micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", "funding": [ { "type": "GitHub Sponsors", @@ -8664,252 +9679,368 @@ "type": "OpenCollective", "url": "https://opencollective.com/unified" } - ] - }, - "node_modules/mdast-util-gfm-footnote": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz", - "integrity": "sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==", + ], + "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" } }, - "node_modules/mdast-util-gfm-strikethrough": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", - "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "node_modules/mermaid/node_modules/micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" } }, - "node_modules/mdast-util-gfm-table": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", - "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "node_modules/mermaid/node_modules/micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "markdown-table": "^3.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" } }, - "node_modules/mdast-util-gfm-task-list-item": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", - "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "node_modules/mermaid/node_modules/micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" } }, - "node_modules/mdast-util-mdx": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", - "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "node_modules/mermaid/node_modules/micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", "dependencies": { - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" } }, - "node_modules/mdast-util-mdx-expression": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.0.tgz", - "integrity": "sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw==", + "node_modules/mermaid/node_modules/micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "micromark-util-symbol": "^1.0.0" } }, - "node_modules/mdast-util-mdx-jsx": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.2.tgz", - "integrity": "sha512-eKMQDeywY2wlHc97k5eD8VC+9ASMjN8ItEZQNGwJ6E0XWKiW/Z0V5/H8pvoXUf+y+Mj0VIgeRRbujBmFn4FTyA==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-remove-position": "^5.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "node_modules/mermaid/node_modules/micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" } }, - "node_modules/mdast-util-mdxjs-esm": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", - "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "node_modules/mermaid/node_modules/micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" } }, - "node_modules/mdast-util-phrasing": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", - "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "node_modules/mermaid/node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "micromark-util-symbol": "^1.0.0" } }, - "node_modules/mdast-util-to-hast": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", - "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "node_modules/mermaid/node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@ungap/structured-clone": "^1.0.0", - "devlop": "^1.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "trim-lines": "^3.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" } }, - "node_modules/mdast-util-to-markdown": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", - "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "node_modules/mermaid/node_modules/micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mermaid/node_modules/micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mermaid/node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^4.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark-util-decode-string": "^2.0.0", - "unist-util-visit": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "micromark-util-symbol": "^1.0.0" } }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "node_modules/mermaid/node_modules/micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "micromark-util-types": "^1.0.0" } }, - "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" + "node_modules/mermaid/node_modules/micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" } }, - "node_modules/memfs": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", - "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "node_modules/mermaid/node_modules/micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", "dependencies": { - "fs-monkey": "^1.0.4" - }, - "engines": { - "node": ">= 4.0.0" + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" } }, - "node_modules/memoize-one": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "node_modules/mermaid/node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + "node_modules/mermaid/node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "engines": { - "node": ">= 8" + "node_modules/mermaid/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" } }, "node_modules/methods": { @@ -10749,6 +11880,15 @@ } } }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/mrmime": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", @@ -10878,6 +12018,12 @@ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, + "node_modules/non-layered-tidy-tree-layout": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", + "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==", + "license": "MIT" + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -13105,6 +14251,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, "node_modules/rtl-detect": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/rtl-detect/-/rtl-detect-1.1.2.tgz", @@ -13146,7 +14298,25 @@ } ], "dependencies": { - "queue-microtask": "^1.2.2" + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" } }, "node_modules/safe-buffer": { @@ -13960,8 +15130,7 @@ "node_modules/stylis": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.0.tgz", - "integrity": "sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==", - "peer": true + "integrity": "sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==" }, "node_modules/supports-color": { "version": "7.2.0", @@ -14261,6 +15430,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -14758,6 +15936,33 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/uvu": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", + "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0", + "diff": "^5.0.0", + "kleur": "^4.0.3", + "sade": "^1.7.3" + }, + "bin": { + "uvu": "bin.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uvu/node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/value-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", @@ -14840,6 +16045,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/web-worker": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.3.0.tgz", + "integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -16928,6 +18139,11 @@ "to-fast-properties": "^2.0.0" } }, + "@braintree/sanitize-url": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz", + "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==" + }, "@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -17294,6 +18510,20 @@ "utility-types": "^3.10.0" } }, + "@docusaurus/theme-mermaid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-mermaid/-/theme-mermaid-3.4.0.tgz", + "integrity": "sha512-3w5QW0HEZ2O6x2w6lU3ZvOe1gNXP2HIoKDMJBil1VmLBc9PmpAG17VmfhI/p3L2etNmOiVs5GgniUqvn8AFEGQ==", + "requires": { + "@docusaurus/core": "3.4.0", + "@docusaurus/module-type-aliases": "3.4.0", + "@docusaurus/theme-common": "3.4.0", + "@docusaurus/types": "3.4.0", + "@docusaurus/utils-validation": "3.4.0", + "mermaid": "^10.4.0", + "tslib": "^2.6.0" + } + }, "@docusaurus/theme-search-algolia": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.4.0.tgz", @@ -17865,6 +19095,24 @@ "@types/node": "*" } }, + "@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "requires": { + "@types/d3-time": "*" + } + }, + "@types/d3-scale-chromatic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz", + "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==" + }, + "@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==" + }, "@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -19254,6 +20502,14 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, + "cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "requires": { + "layout-base": "^1.0.0" + } + }, "cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", @@ -19342,130 +20598,472 @@ "nth-check": "^2.0.1" } }, - "css-to-react-native": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", - "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", - "peer": true, + "css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "peer": true, + "requires": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, + "css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "requires": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==" + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" + }, + "cssnano": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", + "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", + "requires": { + "cssnano-preset-default": "^6.1.2", + "lilconfig": "^3.1.1" + } + }, + "cssnano-preset-advanced": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-6.1.2.tgz", + "integrity": "sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ==", + "requires": { + "autoprefixer": "^10.4.19", + "browserslist": "^4.23.0", + "cssnano-preset-default": "^6.1.2", + "postcss-discard-unused": "^6.0.5", + "postcss-merge-idents": "^6.0.3", + "postcss-reduce-idents": "^6.0.3", + "postcss-zindex": "^6.0.2" + } + }, + "cssnano-preset-default": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", + "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", + "requires": { + "browserslist": "^4.23.0", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^4.0.2", + "postcss-calc": "^9.0.1", + "postcss-colormin": "^6.1.0", + "postcss-convert-values": "^6.1.0", + "postcss-discard-comments": "^6.0.2", + "postcss-discard-duplicates": "^6.0.3", + "postcss-discard-empty": "^6.0.3", + "postcss-discard-overridden": "^6.0.2", + "postcss-merge-longhand": "^6.0.5", + "postcss-merge-rules": "^6.1.1", + "postcss-minify-font-values": "^6.1.0", + "postcss-minify-gradients": "^6.0.3", + "postcss-minify-params": "^6.1.0", + "postcss-minify-selectors": "^6.0.4", + "postcss-normalize-charset": "^6.0.2", + "postcss-normalize-display-values": "^6.0.2", + "postcss-normalize-positions": "^6.0.2", + "postcss-normalize-repeat-style": "^6.0.2", + "postcss-normalize-string": "^6.0.2", + "postcss-normalize-timing-functions": "^6.0.2", + "postcss-normalize-unicode": "^6.1.0", + "postcss-normalize-url": "^6.0.2", + "postcss-normalize-whitespace": "^6.0.2", + "postcss-ordered-values": "^6.0.2", + "postcss-reduce-initial": "^6.1.0", + "postcss-reduce-transforms": "^6.0.2", + "postcss-svgo": "^6.0.3", + "postcss-unique-selectors": "^6.0.4" + } + }, + "cssnano-utils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", + "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", + "requires": {} + }, + "csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "requires": { + "css-tree": "~2.2.0" + }, + "dependencies": { + "css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "requires": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + } + }, + "mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==" + } + } + }, + "csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "cytoscape": { + "version": "3.30.2", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.30.2.tgz", + "integrity": "sha512-oICxQsjW8uSaRmn4UK/jkczKOqTrVqt5/1WL0POiJUT2EKNc9STM4hYFHv917yu55aTBMFNRzymlJhVAiWPCxw==" + }, + "cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "requires": { + "cose-base": "^1.0.0" + } + }, + "d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "requires": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + } + }, + "d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "requires": { + "internmap": "1 - 2" + } + }, + "d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==" + }, + "d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + } + }, + "d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "requires": { + "d3-path": "1 - 3" + } + }, + "d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" + }, + "d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "requires": { + "d3-array": "^3.2.0" + } + }, + "d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "requires": { + "delaunator": "5" + } + }, + "d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==" + }, + "d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + } + }, + "d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "requires": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "dependencies": { + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==" + }, + "d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "requires": { + "d3-dsv": "1 - 3" + } + }, + "d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + } + }, + "d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==" + }, + "d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "requires": { + "d3-array": "2.5.0 - 3" + } + }, + "d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==" + }, + "d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "requires": { + "d3-color": "1 - 3" + } + }, + "d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==" + }, + "d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==" + }, + "d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==" + }, + "d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==" + }, + "d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "requires": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + }, + "dependencies": { + "d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "requires": { + "internmap": "^1.0.0" + } + }, + "d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + }, + "d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "requires": { + "d3-path": "1" + } + }, + "internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==" + } + } + }, + "d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", "requires": { - "camelize": "^1.0.0", - "css-color-keywords": "^1.0.0", - "postcss-value-parser": "^4.0.2" + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" } }, - "css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", "requires": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" } }, - "css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==" - }, - "cssesc": { + "d3-selection": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==" }, - "cssnano": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", - "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", + "d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", "requires": { - "cssnano-preset-default": "^6.1.2", - "lilconfig": "^3.1.1" + "d3-path": "^3.1.0" } }, - "cssnano-preset-advanced": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-6.1.2.tgz", - "integrity": "sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ==", + "d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", "requires": { - "autoprefixer": "^10.4.19", - "browserslist": "^4.23.0", - "cssnano-preset-default": "^6.1.2", - "postcss-discard-unused": "^6.0.5", - "postcss-merge-idents": "^6.0.3", - "postcss-reduce-idents": "^6.0.3", - "postcss-zindex": "^6.0.2" + "d3-array": "2 - 3" } }, - "cssnano-preset-default": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", - "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", + "d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", "requires": { - "browserslist": "^4.23.0", - "css-declaration-sorter": "^7.2.0", - "cssnano-utils": "^4.0.2", - "postcss-calc": "^9.0.1", - "postcss-colormin": "^6.1.0", - "postcss-convert-values": "^6.1.0", - "postcss-discard-comments": "^6.0.2", - "postcss-discard-duplicates": "^6.0.3", - "postcss-discard-empty": "^6.0.3", - "postcss-discard-overridden": "^6.0.2", - "postcss-merge-longhand": "^6.0.5", - "postcss-merge-rules": "^6.1.1", - "postcss-minify-font-values": "^6.1.0", - "postcss-minify-gradients": "^6.0.3", - "postcss-minify-params": "^6.1.0", - "postcss-minify-selectors": "^6.0.4", - "postcss-normalize-charset": "^6.0.2", - "postcss-normalize-display-values": "^6.0.2", - "postcss-normalize-positions": "^6.0.2", - "postcss-normalize-repeat-style": "^6.0.2", - "postcss-normalize-string": "^6.0.2", - "postcss-normalize-timing-functions": "^6.0.2", - "postcss-normalize-unicode": "^6.1.0", - "postcss-normalize-url": "^6.0.2", - "postcss-normalize-whitespace": "^6.0.2", - "postcss-ordered-values": "^6.0.2", - "postcss-reduce-initial": "^6.1.0", - "postcss-reduce-transforms": "^6.0.2", - "postcss-svgo": "^6.0.3", - "postcss-unique-selectors": "^6.0.4" + "d3-time": "1 - 3" } }, - "cssnano-utils": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", - "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", - "requires": {} + "d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==" }, - "csso": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", "requires": { - "css-tree": "~2.2.0" - }, - "dependencies": { - "css-tree": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", - "requires": { - "mdn-data": "2.0.28", - "source-map-js": "^1.0.1" - } - }, - "mdn-data": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==" - } + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" } }, - "csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + "d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + } + }, + "dagre-d3-es": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz", + "integrity": "sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==", + "requires": { + "d3": "^7.8.2", + "lodash-es": "^4.17.21" + } + }, + "dayjs": { + "version": "1.11.12", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.12.tgz", + "integrity": "sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==" }, "debounce": { "version": "1.2.1", @@ -19571,6 +21169,14 @@ "slash": "^3.0.0" } }, + "delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "requires": { + "robust-predicates": "^3.0.2" + } + }, "depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -19632,6 +21238,11 @@ "dequal": "^2.0.0" } }, + "diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==" + }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -19743,6 +21354,11 @@ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.722.tgz", "integrity": "sha512-5nLE0TWFFpZ80Crhtp4pIp8LXCztjYX41yUcV6b+bKR2PqzjskTMOOlBi1VjBHlvHwS+4gar7kNKOrsbsewEZQ==" }, + "elkjs": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", + "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==" + }, "emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -21083,6 +22699,11 @@ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" }, + "internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==" + }, "interpret": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", @@ -21393,6 +23014,21 @@ "universalify": "^2.0.0" } }, + "katex": { + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.11.tgz", + "integrity": "sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==", + "requires": { + "commander": "^8.3.0" + }, + "dependencies": { + "commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==" + } + } + }, "keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -21401,6 +23037,11 @@ "json-buffer": "3.0.1" } }, + "khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -21428,6 +23069,11 @@ "shell-quote": "^1.8.1" } }, + "layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==" + }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -21476,6 +23122,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -21865,6 +23516,281 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" }, + "mermaid": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.9.1.tgz", + "integrity": "sha512-Mx45Obds5W1UkW1nv/7dHRsbfMM1aOKA2+Pxs/IGHNonygDHwmng8xTHyS9z4KWVi0rbko8gjiBmuwwXQ7tiNA==", + "requires": { + "@braintree/sanitize-url": "^6.0.1", + "@types/d3-scale": "^4.0.3", + "@types/d3-scale-chromatic": "^3.0.0", + "cytoscape": "^3.28.1", + "cytoscape-cose-bilkent": "^4.1.0", + "d3": "^7.4.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.10", + "dayjs": "^1.11.7", + "dompurify": "^3.0.5", + "elkjs": "^0.9.0", + "katex": "^0.16.9", + "khroma": "^2.0.0", + "lodash-es": "^4.17.21", + "mdast-util-from-markdown": "^1.3.0", + "non-layered-tidy-tree-layout": "^2.0.2", + "stylis": "^4.1.3", + "ts-dedent": "^2.2.0", + "uuid": "^9.0.0", + "web-worker": "^1.2.0" + }, + "dependencies": { + "@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "requires": { + "@types/unist": "^2" + } + }, + "@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" + }, + "dompurify": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz", + "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==" + }, + "mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "requires": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + } + }, + "mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "requires": { + "@types/mdast": "^3.0.0" + } + }, + "micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "requires": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "requires": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "requires": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "requires": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "requires": { + "micromark-util-symbol": "^1.0.0" + } + }, + "micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "requires": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "requires": { + "micromark-util-symbol": "^1.0.0" + } + }, + "micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "requires": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==" + }, + "micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==" + }, + "micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "requires": { + "micromark-util-symbol": "^1.0.0" + } + }, + "micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "requires": { + "micromark-util-types": "^1.0.0" + } + }, + "micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "requires": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "requires": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==" + }, + "unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "requires": { + "@types/unist": "^2.0.0" + } + }, + "uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" + } + } + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -22826,6 +24752,11 @@ "integrity": "sha512-NkJREyFTSUXR772Qaai51BnE1voWx56LOL80xG7qkZr6vo8vEaLF3sz1JNUVh+rxmUzxYaqOhfuxTfqUh0FXUg==", "requires": {} }, + "mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" + }, "mrmime": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", @@ -22914,6 +24845,11 @@ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, + "non-layered-tidy-tree-layout": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", + "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==" + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -24450,6 +26386,11 @@ "glob": "^7.1.3" } }, + "robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, "rtl-detect": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/rtl-detect/-/rtl-detect-1.1.2.tgz", @@ -24474,6 +26415,19 @@ "queue-microtask": "^1.2.2" } }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, + "sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "requires": { + "mri": "^1.1.0" + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -25107,8 +27061,7 @@ "stylis": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.0.tgz", - "integrity": "sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==", - "peer": true + "integrity": "sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ==" }, "supports-color": { "version": "7.2.0", @@ -25312,6 +27265,11 @@ "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==" }, + "ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==" + }, "tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -25645,6 +27603,24 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, + "uvu": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", + "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", + "requires": { + "dequal": "^2.0.0", + "diff": "^5.0.0", + "kleur": "^4.0.3", + "sade": "^1.7.3" + }, + "dependencies": { + "kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==" + } + } + }, "value-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", @@ -25705,6 +27681,11 @@ "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==" }, + "web-worker": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.3.0.tgz", + "integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==" + }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/docs/package.json b/docs/package.json index 845743eb6158..cb1a64c4956b 100644 --- a/docs/package.json +++ b/docs/package.json @@ -13,11 +13,13 @@ "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", "prettier": "npx prettier --check docs", - "prettier:fix": "npx prettier --write --check docs"}, + "prettier:fix": "npx prettier --write --check docs" + }, "dependencies": { "@docusaurus/core": "^3.4.0", "@docusaurus/plugin-google-tag-manager": "^3.4.0", "@docusaurus/preset-classic": "^3.4.0", + "@docusaurus/theme-mermaid": "^3.4.0", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "prism-react-renderer": "^2.3.0", diff --git a/frontend/common/code-help/create-user/create-user-curl.js b/frontend/common/code-help/create-user/create-user-curl.js index 5f5c3883317d..a13d8edc5224 100644 --- a/frontend/common/code-help/create-user/create-user-curl.js +++ b/frontend/common/code-help/create-user/create-user-curl.js @@ -1,6 +1,6 @@ module.exports = (envId, { USER_ID }, userId) => `// Identify/create user -curl -i 'https://edge.api.flagsmith.com/api/v1/identities/?identifier=${ +curl -i '${Project.flagsmithClientAPI}identities/?identifier=${ userId || USER_ID }' \\ -H 'x-environment-key: ${envId}' diff --git a/frontend/common/code-help/create-user/create-user-flutter.js b/frontend/common/code-help/create-user/create-user-flutter.js index a6f5f5aae9e7..c639c79ed466 100644 --- a/frontend/common/code-help/create-user/create-user-flutter.js +++ b/frontend/common/code-help/create-user/create-user-flutter.js @@ -1,9 +1,14 @@ +import Constants from 'common/constants' module.exports = ( envId, { FEATURE_NAME, FEATURE_NAME_ALT, USER_ID }, userId, ) => `final flagsmithClient = FlagsmithClient( - apiKey: '${envId}' + apiKey: '${envId}',${ + Constants.isCustomFlagsmithUrl + ? `\n baseURI: '${Project.flagsmithClientAPI}',` + : '' +} config: config, seeds: [ Flag.seed('feature', enabled: true), @@ -12,7 +17,11 @@ module.exports = ( //if you prefer async initialization then you should use //final flagsmithClient = await FlagsmithClient.init( -// apiKey: 'YOUR_ENV_API_KEY', +// apiKey: '${envId}',${ + Constants.isCustomFlagsmithUrl + ? `\n// baseURI: '${Project.flagsmithClientAPI}',` + : '' +} // config: config, // seeds: [ // Flag.seed('feature', enabled: true), diff --git a/frontend/common/code-help/create-user/create-user-ios.js b/frontend/common/code-help/create-user/create-user-ios.js index 8fb6add2bd03..c1fbe2958494 100644 --- a/frontend/common/code-help/create-user/create-user-ios.js +++ b/frontend/common/code-help/create-user/create-user-ios.js @@ -1,3 +1,5 @@ +import Constants from 'common/constants' + module.exports = ( envId, { FEATURE_NAME, FEATURE_NAME_ALT, USER_ID }, @@ -8,7 +10,10 @@ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - Flagsmith.shared.apiKey = "${envId}" + Flagsmith.shared.apiKey = "${envId}"${ + Constants.isCustomFlagsmithUrl && + `\n Flagsmith.shared.baseURL = "${Project.flagsmithClientAPI}"\n` +} // This will create a user in the dashboard if they don't already exist // Check for a feature diff --git a/frontend/common/code-help/create-user/create-user-java.js b/frontend/common/code-help/create-user/create-user-java.js index f92a94e3a69d..40809e0af308 100644 --- a/frontend/common/code-help/create-user/create-user-java.js +++ b/frontend/common/code-help/create-user/create-user-java.js @@ -1,10 +1,17 @@ +import Constants from 'common/constants' + module.exports = ( envId, { FEATURE_NAME, FEATURE_NAME_ALT, LIB_NAME, LIB_NAME_JAVA, USER_ID }, userId, ) => `${LIB_NAME_JAVA} ${LIB_NAME} = ${LIB_NAME_JAVA} .newBuilder() - .setApiKey("${envId}") + .setApiKey("${envId}")${ + Constants.isCustomFlagsmithUrl && + `\n .withConfiguration(FlagsmithConfig.builder() + .baseUri("${Project.flagsmithClientAPI}") + .build())` +} .build(); // Identify the user diff --git a/frontend/common/code-help/create-user/create-user-js.js b/frontend/common/code-help/create-user/create-user-js.js index cf1ae6d72b25..eaa8aad92bf1 100644 --- a/frontend/common/code-help/create-user/create-user-js.js +++ b/frontend/common/code-help/create-user/create-user-js.js @@ -1,3 +1,4 @@ +import Constants from 'common/constants' module.exports = ( envId, { @@ -13,7 +14,11 @@ module.exports = ( // Option 1: initialise with an identity and traits ${LIB_NAME}.init({ - environmentID: "${envId}", + environmentID: "${envId}",${ + Constants.isCustomFlagsmithUrl + ? `\n api: "${Project.flagsmithClientAPI}",` + : '' +} identity: "${userId || USER_ID}", traits: { "${TRAIT_NAME}": 21 }, onChange: (oldFlags, params) => { /* ... */ }, diff --git a/frontend/common/code-help/create-user/create-user-next.js b/frontend/common/code-help/create-user/create-user-next.js index 36ee38e9313c..796ace1b04c1 100644 --- a/frontend/common/code-help/create-user/create-user-next.js +++ b/frontend/common/code-help/create-user/create-user-next.js @@ -1,3 +1,4 @@ +import Constants from 'common/constants' module.exports = ( envId, { FEATURE_NAME, FEATURE_NAME_ALT, LIB_NAME, TRAIT_NAME, USER_ID }, @@ -25,7 +26,11 @@ export default function App({ Component, pageProps, flagsmithState } { <FlagsmithProvider serverState={flagsmithState} options={{ - environmentID: '${envId}', + environmentID: "${envId}",${ + Constants.isCustomFlagsmithUrl + ? `\n api: "${Project.flagsmithClientAPI}",` + : '' +} }} flagsmith={flagsmith}> <Component {...pageProps} /> @@ -36,7 +41,11 @@ export default function App({ Component, pageProps, flagsmithState } { MyApp.getInitialProps = async () => { // calls page's \`getInitialProps\` and fills \`appProps.pageProps\` await flagsmith.init({ // fetches flags on the server - environmentID, + environmentID: "${envId}",${ + Constants.isCustomFlagsmithUrl + ? `\n api: "${Project.flagsmithClientAPI}",` + : '' +} preventFetch: true }); await flagsmith.identify('${ diff --git a/frontend/common/code-help/create-user/create-user-node.js b/frontend/common/code-help/create-user/create-user-node.js index ae39aa855eba..46e393cb1a1b 100644 --- a/frontend/common/code-help/create-user/create-user-node.js +++ b/frontend/common/code-help/create-user/create-user-node.js @@ -1,12 +1,18 @@ +import Constants from 'common/constants' + module.exports = ( envId, - { FEATURE_NAME, FEATURE_NAME_ALT, USER_ID }, + { FEATURE_NAME, FEATURE_NAME_ALT, LIB_NAME, NPM_NODE_CLIENT, USER_ID }, userId, -) => `const Flagsmith = require('flagsmith-nodejs'); +) => `import Flagsmith from "${NPM_NODE_CLIENT}"; // Add this line if you're using ${LIB_NAME} via npm -const flagsmith = new Flagsmith( +const ${LIB_NAME} = new Flagsmith({${ + Constants.isCustomFlagsmithUrl && + `\n apiUrl: '${Project.flagsmithClientAPI}',` +} environmentKey: '${envId}' -); +}); + // Identify the user const flags = await flagsmith.getIdentityFlags('${ diff --git a/frontend/common/code-help/create-user/create-user-php.js b/frontend/common/code-help/create-user/create-user-php.js index 7eea0fe838e9..1995d959d0df 100644 --- a/frontend/common/code-help/create-user/create-user-php.js +++ b/frontend/common/code-help/create-user/create-user-php.js @@ -1,10 +1,14 @@ +import Constants from 'common/constants' + module.exports = ( envId, { FEATURE_NAME, FEATURE_NAME_ALT, USER_ID }, userId, ) => `use Flagsmith\\Flagsmith; -$flagsmith = new Flagsmith('${envId}'); +$flagsmith = new Flagsmith('${envId}'${ + Constants.isCustomFlagsmithUrl && `,\n '${Project.flagsmithClientAPI}'\n` +}); // Identify the user $flags = $flagsmith->getIdentityFlags('${userId}', $traits); diff --git a/frontend/common/code-help/create-user/create-user-python.js b/frontend/common/code-help/create-user/create-user-python.js index d8618f97b0a4..82c8bbf9e42a 100644 --- a/frontend/common/code-help/create-user/create-user-python.js +++ b/frontend/common/code-help/create-user/create-user-python.js @@ -1,10 +1,15 @@ +import Constants from 'common/constants' + module.exports = ( envId, { FEATURE_NAME, FEATURE_NAME_ALT, USER_ID }, userId, ) => `from flagsmith import Flagsmith -flagsmith = Flagsmith(environment_key="${envId}") +flagsmith = Flagsmith(environment_key="${envId}"${ + Constants.isCustomFlagsmithUrl && + `,\n api_url="${Project.flagsmithClientAPI}"\n` +}) # Identify the user identity_flags = flagsmith.get_identity_flags(identifier="${ diff --git a/frontend/common/code-help/create-user/create-user-react.js b/frontend/common/code-help/create-user/create-user-react.js index e83fea32b3a4..2b17dd249c76 100644 --- a/frontend/common/code-help/create-user/create-user-react.js +++ b/frontend/common/code-help/create-user/create-user-react.js @@ -1,16 +1,22 @@ +import Constants from 'common/constants' + module.exports = ( envId, - { FEATURE_NAME, FEATURE_NAME_ALT, LIB_NAME, TRAIT_NAME, USER_ID }, + { FEATURE_NAME, FEATURE_NAME_ALT, NPM_CLIENT, TRAIT_NAME, USER_ID }, userId, ) => ` // Option 1: Initialise with an identity -import { FlagsmithProvider } from 'flagsmith/react'; +import { FlagsmithProvider } from '${NPM_CLIENT}/react'; export default function App() { return ( <FlagsmithProvider options={{ - environmentID: '${envId}', + environmentID: '${envId}',${ + Constants.isCustomFlagsmithUrl + ? `\n api: '${Project.flagsmithClientAPI}',` + : '' +} identity: '${userId || USER_ID}', traits: {${TRAIT_NAME}: 21}, }} @@ -22,8 +28,8 @@ export default function App() { // Option 2: Identify after initialising -import flagsmith from '${LIB_NAME}'; -import { useFlags, useFlagsmith } from 'flagsmith/react'; +import flagsmith from '${NPM_CLIENT}'; +import { useFlags, useFlagsmith } from '${NPM_CLIENT}/react'; export default function HomePage() { const flags = useFlags(['${FEATURE_NAME}','${FEATURE_NAME_ALT}']); // only causes re-render if specified flag values / traits change diff --git a/frontend/common/code-help/create-user/create-user-ruby.js b/frontend/common/code-help/create-user/create-user-ruby.js index 65d89dbc09c9..5da2f36aeedd 100644 --- a/frontend/common/code-help/create-user/create-user-ruby.js +++ b/frontend/common/code-help/create-user/create-user-ruby.js @@ -1,3 +1,5 @@ +import Constants from 'common/constants' + module.exports = ( envId, { FEATURE_NAME, FEATURE_NAME_ALT, USER_ID }, @@ -5,8 +7,10 @@ module.exports = ( ) => `require "flagsmith" $flagsmith = Flagsmith::Client.new( - environment_key: '${envId}' -) + environment_key="${envId}"${ + Constants.isCustomFlagsmithUrl && + `,\n api_url="${Project.flagsmithClientAPI}"\n` +}) // Identify the user $flags = $flagsmith.get_identity_flags('${userId || USER_ID}') diff --git a/frontend/common/code-help/create-user/create-user-rust.js b/frontend/common/code-help/create-user/create-user-rust.js index 492951a843eb..93fdd7818f79 100644 --- a/frontend/common/code-help/create-user/create-user-rust.js +++ b/frontend/common/code-help/create-user/create-user-rust.js @@ -1,3 +1,5 @@ +import Constants from 'common/constants' + module.exports = ( envId, { FEATURE_NAME, FEATURE_NAME_ALT, USER_ID }, @@ -5,7 +7,10 @@ module.exports = ( ) => ` use flagsmith::{Flag, Flagsmith, FlagsmithOptions}; -let options = FlagsmithOptions {..Default::default()}; +let options = FlagsmithOptions {${ + Constants.isCustomFlagsmithUrl && + `api_url: "${Project.flagsmithClientAPI}".to_string(),\n` +}..Default::default()}; let flagsmith = Flagsmith::new( "${envId}".to_string(), options, diff --git a/frontend/common/code-help/init/init-curl.js b/frontend/common/code-help/init/init-curl.js index a45093e6d404..5f847440ad67 100644 --- a/frontend/common/code-help/init/init-curl.js +++ b/frontend/common/code-help/init/init-curl.js @@ -1,4 +1,4 @@ module.exports = (envId) => ` -curl -i 'https://edge.api.flagsmith.com/api/v1/flags/' \\ +curl -i '${Project.flagsmithClientAPI}flags/' \\ -H 'x-environment-key: ${envId}' ` diff --git a/frontend/common/code-help/init/init-flutter.js b/frontend/common/code-help/init/init-flutter.js index 295807e415de..bcd3c531b0df 100644 --- a/frontend/common/code-help/init/init-flutter.js +++ b/frontend/common/code-help/init/init-flutter.js @@ -1,10 +1,15 @@ +import Constants from 'common/constants' module.exports = ( envId, { FEATURE_NAME, FEATURE_NAME_ALT }, ) => `//In your application, initialise the Flagsmith client with your API key: final flagsmithClient = FlagsmithClient( - apiKey: '${envId}' + apiKey: '${envId}',${ + Constants.isCustomFlagsmithUrl + ? `\n baseURI: '${Project.flagsmithClientAPI}',` + : '' +} config: config, seeds: [ Flag.seed('feature', enabled: true), @@ -13,7 +18,11 @@ final flagsmithClient = FlagsmithClient( //if you prefer async initialization then you should use //final flagsmithClient = await FlagsmithClient.init( -// apiKey: 'YOUR_ENV_API_KEY', +// apiKey: '${envId}',${ + Constants.isCustomFlagsmithUrl + ? `\n// baseURI: '${Project.flagsmithClientAPI}',` + : '' +} // config: config, // seeds: [ // Flag.seed('feature', enabled: true), diff --git a/frontend/common/code-help/init/init-go.js b/frontend/common/code-help/init/init-go.js index 10cf678bad23..3441e307a04b 100644 --- a/frontend/common/code-help/init/init-go.js +++ b/frontend/common/code-help/init/init-go.js @@ -1,9 +1,14 @@ +import Constants from 'common/constants' + module.exports = (envId, { FEATURE_NAME, FEATURE_NAME_ALT }, customFeature) => ` ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Initialise the Flagsmith client -client := flagsmith.NewClient('${envId}', flagsmith.WithContext(ctx)) +client := flagsmith.NewClient('${envId}',${ + Constants.isCustomFlagsmithUrl && + `\nflagsmith.WithBaseURL("${Project.flagsmithClientAPI}"),\n` +}flagsmith.WithContext(ctx)) // The method below triggers a network request flags, _ := client.GetEnvironmentFlags() diff --git a/frontend/common/code-help/init/init-ios.js b/frontend/common/code-help/init/init-ios.js index cd4252154c2f..e983bd0a1e5c 100644 --- a/frontend/common/code-help/init/init-ios.js +++ b/frontend/common/code-help/init/init-ios.js @@ -1,3 +1,5 @@ +import Constants from 'common/constants' + module.exports = ( envId, { FEATURE_NAME, FEATURE_NAME_ALT }, @@ -6,7 +8,10 @@ module.exports = ( func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - Flagsmith.shared.apiKey = "${envId}" + Flagsmith.shared.apiKey = "${envId}"${ + Constants.isCustomFlagsmithUrl && + `\n Flagsmith.shared.baseURL = "${Project.flagsmithClientAPI}"\n` +} // Check for a feature Flagsmith.shared .hasFeatureFlag(withID: "${FEATURE_NAME}", forIdentity: nil) { (result) in diff --git a/frontend/common/code-help/init/init-java.js b/frontend/common/code-help/init/init-java.js index f2d44607a5ac..57954e91d348 100644 --- a/frontend/common/code-help/init/init-java.js +++ b/frontend/common/code-help/init/init-java.js @@ -1,10 +1,16 @@ +import Constants from 'common/constants' module.exports = ( envId, { FEATURE_NAME, FEATURE_NAME_ALT, LIB_NAME, LIB_NAME_JAVA }, customFeature, ) => `${LIB_NAME_JAVA} ${LIB_NAME} = ${LIB_NAME_JAVA} .newBuilder() - .setApiKey("${envId}") + .setApiKey("${envId}")${ + Constants.isCustomFlagsmithUrl && + `\n .withConfiguration(FlagsmithConfig.builder() + .baseUri("${Project.flagsmithClientAPI}") + .build())` +} .build(); Flags flags = flagsmith.getEnvironmentFlags(); diff --git a/frontend/common/code-help/init/init-js.js b/frontend/common/code-help/init/init-js.js index 7dbfa8b11008..def8e9a2867e 100644 --- a/frontend/common/code-help/init/init-js.js +++ b/frontend/common/code-help/init/init-js.js @@ -1,3 +1,4 @@ +import Constants from 'common/constants' module.exports = ( envId, { FEATURE_FUNCTION, FEATURE_NAME, FEATURE_NAME_ALT, LIB_NAME, NPM_CLIENT }, @@ -5,7 +6,7 @@ module.exports = ( ) => `import ${LIB_NAME} from "${NPM_CLIENT}"; // Add this line if you're using ${LIB_NAME} via npm ${LIB_NAME}.init({ - environmentID: "${envId}", + environmentID: "${envId}",${Constants.isCustomFlagsmithUrl ? `\n api: "${Project.flagsmithClientAPI}",` : ''} onChange: (oldFlags, params) => { // Occurs whenever flags are changed // Determines if the update came from the server or local cached storage const { isFromServer } = params; diff --git a/frontend/common/code-help/init/init-next.js b/frontend/common/code-help/init/init-next.js index b5c679c899f9..07e0047ad0e9 100644 --- a/frontend/common/code-help/init/init-next.js +++ b/frontend/common/code-help/init/init-next.js @@ -1,3 +1,4 @@ +import Constants from 'common/constants' module.exports = ( envId, { FEATURE_NAME, FEATURE_NAME_ALT, LIB_NAME, NPM_CLIENT }, @@ -10,7 +11,11 @@ export default function App({ Component, pageProps, flagsmithState } { <FlagsmithProvider serverState={flagsmithState} options={{ - environmentID: '${envId}', + environmentID: "${envId}",${ + Constants.isCustomFlagsmithUrl + ? `\n api: "${Project.flagsmithClientAPI}",` + : '' +} }} flagsmith={flagsmith}> <Component {...pageProps} /> @@ -20,7 +25,11 @@ export default function App({ Component, pageProps, flagsmithState } { App.getInitialProps = async () => { await flagsmith.init({ // fetches flags on the server and passes them to the App - environmentID, + environmentID: "${envId}",${ + Constants.isCustomFlagsmithUrl + ? `\n api: "${Project.flagsmithClientAPI}",` + : '' +} }); return { flagsmithState: flagsmith.getState() } } diff --git a/frontend/common/code-help/init/init-node.js b/frontend/common/code-help/init/init-node.js index cc81b604a1de..4c49926e9a06 100644 --- a/frontend/common/code-help/init/init-node.js +++ b/frontend/common/code-help/init/init-node.js @@ -1,10 +1,15 @@ +import Constants from 'common/constants' + module.exports = ( envId, { FEATURE_NAME, FEATURE_NAME_ALT, LIB_NAME, NPM_NODE_CLIENT }, customFeature, ) => `import Flagsmith from "${NPM_NODE_CLIENT}"; // Add this line if you're using ${LIB_NAME} via npm -const ${LIB_NAME} = new Flagsmith({ +const ${LIB_NAME} = new Flagsmith({${ + Constants.isCustomFlagsmithUrl && + `\n apiUrl: '${Project.flagsmithClientAPI}',` +} environmentKey: '${envId}' }); diff --git a/frontend/common/code-help/init/init-php.js b/frontend/common/code-help/init/init-php.js index 9c7168f3effd..9d27c31711a4 100644 --- a/frontend/common/code-help/init/init-php.js +++ b/frontend/common/code-help/init/init-php.js @@ -1,9 +1,12 @@ +import Constants from 'common/constants' module.exports = ( envId, { FEATURE_NAME, FEATURE_NAME_ALT }, ) => `use Flagsmith\\Flagsmith; -$flagsmith = new Flagsmith('${envId}'); +$flagsmith = new Flagsmith('${envId}'${ + Constants.isCustomFlagsmithUrl && `,\n '${Project.flagsmithClientAPI}'\n` +}); // Check for a feature $${FEATURE_NAME} = $flags->isFeatureEnabled('${FEATURE_NAME}'); diff --git a/frontend/common/code-help/init/init-python.js b/frontend/common/code-help/init/init-python.js index 2f596b26b410..088300c490e9 100644 --- a/frontend/common/code-help/init/init-python.js +++ b/frontend/common/code-help/init/init-python.js @@ -1,9 +1,14 @@ +import Constants from 'common/constants' + module.exports = ( envId, { FEATURE_NAME, FEATURE_NAME_ALT }, ) => `from flagsmith import Flagsmith -flagsmith = Flagsmith(environment_key="${envId}") +flagsmith = Flagsmith(environment_key="${envId}"${ + Constants.isCustomFlagsmithUrl && + `,\n api_url="${Project.flagsmithClientAPI}"\n` +}) # The method below triggers a network request flags = flagsmith.get_environment_flags() diff --git a/frontend/common/code-help/init/init-react.js b/frontend/common/code-help/init/init-react.js index 41b6ffa79685..f04e0233719f 100644 --- a/frontend/common/code-help/init/init-react.js +++ b/frontend/common/code-help/init/init-react.js @@ -1,3 +1,5 @@ +import Constants from 'common/constants' + module.exports = ( envId, { FEATURE_NAME, FEATURE_NAME_ALT, LIB_NAME, NPM_CLIENT }, @@ -9,7 +11,11 @@ export default function App() { return ( <FlagsmithProvider options={{ - environmentID: '${envId}', + environmentID: '${envId}',${ + Constants.isCustomFlagsmithUrl + ? `\n api: '${Project.flagsmithClientAPI}',` + : '' +} }} flagsmith={flagsmith}> {...Your app} @@ -18,8 +24,8 @@ export default function App() { } // Home Page -import flagsmith from 'flagsmith'; -import { useFlags, useFlagsmith } from 'flagsmith/react'; +import ${LIB_NAME} from '${NPM_CLIENT}'; +import { useFlags, useFlagsmith } from '${NPM_CLIENT}/react'; export default function HomePage() { const flags = useFlags(['${FEATURE_NAME}','${FEATURE_NAME_ALT}']); // only causes re-render if specified flag values / traits change diff --git a/frontend/common/code-help/init/init-ruby.js b/frontend/common/code-help/init/init-ruby.js index 605411d88fb4..2d91a0006fc3 100644 --- a/frontend/common/code-help/init/init-ruby.js +++ b/frontend/common/code-help/init/init-ruby.js @@ -1,3 +1,5 @@ +import Constants from 'common/constants' + module.exports = ( envId, { FEATURE_NAME, FEATURE_NAME_ALT }, @@ -5,8 +7,10 @@ module.exports = ( ) => `require "flagsmith" $flagsmith = Flagsmith::Client.new( - environment_key: '${envId}' -) + environment_key="${envId}"${ + Constants.isCustomFlagsmithUrl && + `,\n api_url="${Project.flagsmithClientAPI}"\n` +}) // Load the environment's flags locally $flags = $flagsmith.get_environment_flags diff --git a/frontend/common/code-help/init/init-rust.js b/frontend/common/code-help/init/init-rust.js index 1ae1d72dcc3e..f547961eded4 100644 --- a/frontend/common/code-help/init/init-rust.js +++ b/frontend/common/code-help/init/init-rust.js @@ -1,7 +1,12 @@ +import Constants from 'common/constants' + module.exports = (envId, { FEATURE_NAME, FEATURE_NAME_ALT }) => ` use flagsmith::{Flag, Flagsmith, FlagsmithOptions}; -let options = FlagsmithOptions {..Default::default()}; +let options = FlagsmithOptions {${ + Constants.isCustomFlagsmithUrl && + `api_url: "${Project.flagsmithClientAPI}".to_string(),\n` +}..Default::default()}; let flagsmith = Flagsmith::new( "${envId}".to_string(), options, diff --git a/frontend/common/code-help/offline_client/offline-client-curl.js b/frontend/common/code-help/offline_client/offline-client-curl.js index e244733554f5..ceee3d366008 100644 --- a/frontend/common/code-help/offline_client/offline-client-curl.js +++ b/frontend/common/code-help/offline_client/offline-client-curl.js @@ -1,4 +1,4 @@ module.exports = (envId) => ` -curl -i 'https://edge.api.flagsmith.com/api/v1/flags/' \\ +curl -i '${Project.flagsmithClientAPI}flags/' \\ -H 'x-environment-key: ${envId}' | tee flagsmith.json ` diff --git a/frontend/common/code-help/offline_server/offline-server-curl.js b/frontend/common/code-help/offline_server/offline-server-curl.js index 36c4b202b4b7..44ceb1f56f61 100644 --- a/frontend/common/code-help/offline_server/offline-server-curl.js +++ b/frontend/common/code-help/offline_server/offline-server-curl.js @@ -1,4 +1,4 @@ module.exports = (serversideEnvironmentKey) => ` -curl -i 'https://edge.api.flagsmith.com/api/v1/environment-document/' \\ +curl -i '${Project.flagsmithClientAPI}environment-document/' \\ -H 'x-environment-key: ${serversideEnvironmentKey}' | tee flagsmith.json ` diff --git a/frontend/common/code-help/traits/traits-curl.js b/frontend/common/code-help/traits/traits-curl.js index c7fecc02b9e8..4837e58c34bb 100644 --- a/frontend/common/code-help/traits/traits-curl.js +++ b/frontend/common/code-help/traits/traits-curl.js @@ -2,7 +2,7 @@ module.exports = ( envId, { TRAIT_NAME, USER_ID }, userId, -) => `curl -i -X POST 'https://edge.api.flagsmith.com/api/v1/identities/' \\ +) => `curl -i -X POST '${Project.flagsmithClientAPI}identities/' \\ -H 'x-environment-key: ${envId}' \\ -H 'Content-Type: application/json; charset=utf-8' \\ -d $'{ diff --git a/frontend/common/code-help/traits/traits-java.js b/frontend/common/code-help/traits/traits-java.js index dc4bd8654e2b..6b4db8cb1815 100644 --- a/frontend/common/code-help/traits/traits-java.js +++ b/frontend/common/code-help/traits/traits-java.js @@ -1,10 +1,17 @@ +import Constants from 'common/constants' + module.exports = ( envId, { LIB_NAME, LIB_NAME_JAVA, TRAIT_NAME }, userId, ) => `${LIB_NAME_JAVA} ${LIB_NAME} = ${LIB_NAME_JAVA} .newBuilder() -.setApiKey("${envId}") +.setApiKey("${envId}")${ + Constants.isCustomFlagsmithUrl && + `\n .withConfiguration(FlagsmithConfig.builder() + .baseUri("${Project.flagsmithClientAPI}") + .build())` +} .build(); Map traits = new HashMap(); diff --git a/frontend/common/code-help/traits/traits-js.js b/frontend/common/code-help/traits/traits-js.js index fb3a03819957..11f8708cf5a3 100644 --- a/frontend/common/code-help/traits/traits-js.js +++ b/frontend/common/code-help/traits/traits-js.js @@ -1,10 +1,16 @@ +import Constants from 'common/constants' + module.exports = ( envId, { LIB_NAME, TRAIT_NAME, USER_ID }, userId, ) => `// Option 1: initialise with an identity and traits ${LIB_NAME}.init({ - environmentID: "${envId}", + environmentID: "${envId}",${ + Constants.isCustomFlagsmithUrl + ? `\n api: "${Project.flagsmithClientAPI}",` + : '' +} identity: "${userId || USER_ID}", traits: { "${TRAIT_NAME}": 21 }, onChange: (oldFlags, params) => { /* ... */ }, diff --git a/frontend/common/code-help/traits/traits-next.js b/frontend/common/code-help/traits/traits-next.js index 9db7062b7884..11618d3b1752 100644 --- a/frontend/common/code-help/traits/traits-next.js +++ b/frontend/common/code-help/traits/traits-next.js @@ -1,3 +1,4 @@ +import Constants from 'common/constants' module.exports = ( envId, { FEATURE_NAME, FEATURE_NAME_ALT, LIB_NAME, TRAIT_NAME }, @@ -33,7 +34,11 @@ export default function App({ Component, pageProps, flagsmithState } { <FlagsmithProvider serverState={flagsmithState} options={{ - environmentID: '${envId}', + environmentID: "${envId}",${ + Constants.isCustomFlagsmithUrl + ? `\n api: "${Project.flagsmithClientAPI}",` + : '' +} }} flagsmith={flagsmith}> <Component {...pageProps} /> @@ -44,7 +49,11 @@ export default function App({ Component, pageProps, flagsmithState } { MyApp.getInitialProps = async () => { // calls page's \`getInitialProps\` and fills \`appProps.pageProps\` await flagsmith.init({ // fetches flags on the server - environmentID, + environmentID: "${envId}",${ + Constants.isCustomFlagsmithUrl + ? `\n api: "${Project.flagsmithClientAPI}",` + : '' +} preventFetch: true }); await flagsmith.identify('${USER_ID}', {${TRAIT_NAME}: 21}); // Will hydrate the app with the user's flags diff --git a/frontend/common/code-help/traits/traits-node.js b/frontend/common/code-help/traits/traits-node.js index f3a0cd8854c6..0692cdd2a963 100644 --- a/frontend/common/code-help/traits/traits-node.js +++ b/frontend/common/code-help/traits/traits-node.js @@ -1,12 +1,17 @@ +import Constants from 'common/constants' + module.exports = ( envId, - { TRAIT_NAME, USER_ID }, -) => `const Flagsmith = require('flagsmith-nodejs'); + { FEATURE_NAME, TRAIT_NAME, LIB_NAME, NPM_NODE_CLIENT, USER_ID }, + userId, +) => `import Flagsmith from "${NPM_NODE_CLIENT}"; // Add this line if you're using ${LIB_NAME} via npm -const flagsmith = new Flagsmith( +const ${LIB_NAME} = new Flagsmith({${ + Constants.isCustomFlagsmithUrl && + `\n apiUrl: '${Project.flagsmithClientAPI}',` +} environmentKey: '${envId}' -); - +}); // Identify a user, set their traits and retrieve the flags const traits = { ${TRAIT_NAME}: 'robin_reliant' }; const flags = await flagsmith.getIdentityFlags('${USER_ID}', traits); diff --git a/frontend/common/code-help/traits/traits-php.js b/frontend/common/code-help/traits/traits-php.js index c433b2e1952d..e889ea288e43 100644 --- a/frontend/common/code-help/traits/traits-php.js +++ b/frontend/common/code-help/traits/traits-php.js @@ -1,6 +1,9 @@ +import Constants from 'common/constants' module.exports = (envId, { TRAIT_NAME }, userId) => `use Flagsmith\\Flagsmith; -$flagsmith = new Flagsmith('${envId}'); +$flagsmith = new Flagsmith('${envId}'${ + Constants.isCustomFlagsmithUrl && `,\n '${Project.flagsmithClientAPI}'\n` +}); $traits = (object) [ '${TRAIT_NAME}' => 42 ]; diff --git a/frontend/common/code-help/traits/traits-python.js b/frontend/common/code-help/traits/traits-python.js index 940338035f52..45f65e79c72f 100644 --- a/frontend/common/code-help/traits/traits-python.js +++ b/frontend/common/code-help/traits/traits-python.js @@ -1,10 +1,15 @@ +import Constants from 'common/constants' + module.exports = ( envId, { TRAIT_NAME }, userId, ) => `from flagsmith import Flagsmith -flagsmith = Flagsmith(environment_key="${envId}") +flagsmith = Flagsmith(environment_key="${envId}"${ + Constants.isCustomFlagsmithUrl && + `,\n api_url="${Project.flagsmithClientAPI}"\n` +}) traits = {"${TRAIT_NAME}": 42} diff --git a/frontend/common/code-help/traits/traits-react.js b/frontend/common/code-help/traits/traits-react.js index b698f68d8dca..dd80794752c0 100644 --- a/frontend/common/code-help/traits/traits-react.js +++ b/frontend/common/code-help/traits/traits-react.js @@ -1,16 +1,22 @@ +import Constants from 'common/constants' + module.exports = ( envId, - { FEATURE_NAME, FEATURE_NAME_ALT, LIB_NAME, TRAIT_NAME, USER_ID }, + { FEATURE_NAME, FEATURE_NAME_ALT, LIB_NAME, NPM_CLIENT, TRAIT_NAME, USER_ID }, userId, ) => ` // Option 1: Initialise with an identity and traits -import { FlagsmithProvider } from 'flagsmith/react'; +import { FlagsmithProvider } from '${NPM_CLIENT}/react'; export default function App() { return ( <FlagsmithProvider options={{ - environmentID: '${envId}', + environmentID: '${envId}',${ + Constants.isCustomFlagsmithUrl + ? `\n api: '${Project.flagsmithClientAPI}',` + : '' +} identity: '${userId || USER_ID}', traits: {${TRAIT_NAME}: 21}, }} @@ -21,8 +27,8 @@ export default function App() { } // Option 2: Set traits / identify after initialising -import flagsmith from '${LIB_NAME}'; -import { useFlags, useFlagsmith } from 'flagsmith/react'; +import flagsmith from '${NPM_CLIENT}'; +import { useFlags, useFlagsmith } from '${NPM_CLIENT}/react'; export default function HomePage() { const flags = useFlags(['${FEATURE_NAME}','${FEATURE_NAME_ALT}']); // only causes re-render if specified flag values / traits change diff --git a/frontend/common/code-help/traits/traits-ruby.js b/frontend/common/code-help/traits/traits-ruby.js index dfb745ddde6f..bde960f914e3 100644 --- a/frontend/common/code-help/traits/traits-ruby.js +++ b/frontend/common/code-help/traits/traits-ruby.js @@ -1,8 +1,12 @@ +import Constants from 'common/constants' + module.exports = (envId, { TRAIT_NAME }, userId) => `require "flagsmith" $flagsmith = Flagsmith::Client.new( - environment_key: '${envId}' -) + environment_key="${envId}"${ + Constants.isCustomFlagsmithUrl && + `,\n api_url="${Project.flagsmithClientAPI}"\n` +}) traits = {"${TRAIT_NAME}": 42} diff --git a/frontend/common/code-help/traits/traits-rust.js b/frontend/common/code-help/traits/traits-rust.js index c563a9857b4a..f2ea797712e7 100644 --- a/frontend/common/code-help/traits/traits-rust.js +++ b/frontend/common/code-help/traits/traits-rust.js @@ -1,9 +1,14 @@ +import Constants from 'common/constants' + module.exports = (envId, { USER_ID }, userId) => ` use flagsmith::{Flag, Flagsmith, FlagsmithOptions}; use flagsmith_flag_engine::types::{FlagsmithValue, FlagsmithValueType}; use flagsmith_flag_engine::identities::Trait; -let options = FlagsmithOptions {..Default::default()}; +let options = FlagsmithOptions {${ + Constants.isCustomFlagsmithUrl && + `api_url: "${Project.flagsmithClientAPI}".to_string(),\n` +}..Default::default()}; let flagsmith = Flagsmith::new( "${envId}".to_string(), options, diff --git a/frontend/common/constants.ts b/frontend/common/constants.ts index 435285f42484..410fcbab3414 100644 --- a/frontend/common/constants.ts +++ b/frontend/common/constants.ts @@ -2,6 +2,7 @@ import { OAuthType } from './types/requests' import { SegmentCondition } from './types/responses' import Utils from './utils/utils' +import Project from './project' const keywords = { FEATURE_FUNCTION: 'myCoolFeature', FEATURE_NAME: 'my_cool_feature', @@ -114,7 +115,7 @@ export default { 'PHP': require('./code-help/init/init-php')(envId, keywords), 'Python': require('./code-help/init/init-python')(envId, keywords), 'React': require('./code-help/init/init-react')(envId, keywords), - 'React Native': require('./code-help/init/init-js')( + 'React Native': require('./code-help/init/init-react')( envId, keywordsReactNative, ), @@ -229,7 +230,6 @@ export default { ), 'iOS': require('./code-help/traits/traits-ios')(envId, keywords, userId), }), - keys: { 'Java': 'java', 'JavaScript': 'javascript', @@ -453,6 +453,8 @@ export default { githubIssue: 'GitHub Issue', githubPR: 'Github PR', }, + isCustomFlagsmithUrl: + Project.flagsmithClientAPI !== 'https://edge.api.flagsmith.com/api/v1/', modals: { 'PAYMENT': 'Payment Modal', }, diff --git a/frontend/common/services/useGithubRepository.ts b/frontend/common/services/useGithubRepository.ts index 40589d4c6af1..618c4adb4d79 100644 --- a/frontend/common/services/useGithubRepository.ts +++ b/frontend/common/services/useGithubRepository.ts @@ -43,7 +43,7 @@ export const githubRepositoryService = service >({ invalidatesTags: [{ id: 'LIST', type: 'GithubRepository' }], query: (query: Req['updateGithubRepository']) => ({ - body: query, + body: query.body, method: 'PUT', url: `organisations/${query.organisation_id}/integrations/github/${query.github_id}/repositories/${query.id}/`, }), diff --git a/frontend/common/stores/account-store.js b/frontend/common/stores/account-store.js index acfefa41ae85..bd847484fcd9 100644 --- a/frontend/common/stores/account-store.js +++ b/frontend/common/stores/account-store.js @@ -202,6 +202,7 @@ const controller = { : `${Project.api}auth/oauth/${type}/`, { ...(_data || {}), + invite_hash: API.getInvite() || undefined, sign_up_type: API.getInviteType(), }, ) @@ -245,6 +246,7 @@ const controller = { password, referrer: API.getReferrer() || '', sign_up_type: API.getInviteType(), + invite_hash: API.getInvite() || undefined, }) .then((res) => { data.setToken(res.key) diff --git a/frontend/common/stores/feature-list-store.ts b/frontend/common/stores/feature-list-store.ts index d833f865aa9a..a47c9ffe27d5 100644 --- a/frontend/common/stores/feature-list-store.ts +++ b/frontend/common/stores/feature-list-store.ts @@ -36,23 +36,6 @@ import { getFeatureStates } from 'common/services/useFeatureState' import { getSegments } from 'common/services/useSegment' let createdFirstFeature = false const PAGE_SIZE = 50 -function recursivePageGet(url, parentRes) { - return data.get(url).then((res) => { - let response - if (parentRes) { - response = { - ...parentRes, - results: parentRes.results.concat(res.results), - } - } else { - response = res - } - if (res.next) { - return recursivePageGet(res.next, response) - } - return Promise.resolve(response) - }) -} const convertSegmentOverrideToFeatureState = ( override, @@ -107,8 +90,11 @@ const controller = { }), project_id: projectId, }) - .then((res) => - Promise.all( + .then((res) => { + if (res.error) { + throw res.error?.error || res.error + } + return Promise.all( (flag.multivariate_options || []).map((v) => data .post( @@ -124,20 +110,20 @@ const controller = { data.get( `${Project.api}projects/${projectId}/features/${res.data.id}/`, ), - ), - ) + ) + }) .then(() => Promise.all([ data.get(`${Project.api}projects/${projectId}/features/`), - data.get( - `${Project.api}environments/${environmentId}/featurestates/`, - ), - ]).then(([features, environmentFeatures]) => { + ]).then(([features]) => { + const environmentFeatures = features.results.map((v) => ({ + ...v.environment_feature_state, + feature: v.id, + })) store.model = { features: features.results, keyedEnvironmentFeatures: - environmentFeatures && - _.keyBy(environmentFeatures.results, 'feature'), + environmentFeatures && _.keyBy(environmentFeatures, 'feature'), } store.model.lastSaved = new Date().valueOf() store.saved({ createdFlag: flag.name }) @@ -339,123 +325,132 @@ const controller = { store.saving() API.trackEvent(Constants.events.EDIT_FEATURE) - segmentOverridesProm.then(() => { - if (mode !== 'VALUE') { - prom = Promise.resolve() - } else if (environmentFlag) { - prom = data - .get( - `${Project.api}environments/${environmentId}/featurestates/${environmentFlag.id}/`, - ) - .then((environmentFeatureStates) => { - const multivariate_feature_state_values = - environmentFeatureStates.multivariate_feature_state_values && - environmentFeatureStates.multivariate_feature_state_values.map( - (v) => { - const matching = - environmentFlag.multivariate_feature_state_values.find( - (m) => m.id === v.multivariate_feature_option, - ) || {} - return { - ...v, - percentage_allocation: - matching.default_percentage_allocation, - } - }, - ) - environmentFlag.multivariate_feature_state_values = - multivariate_feature_state_values - return data.put( + segmentOverridesProm + .then(() => { + if (mode !== 'VALUE') { + prom = Promise.resolve() + } else if (environmentFlag) { + prom = data + .get( `${Project.api}environments/${environmentId}/featurestates/${environmentFlag.id}/`, - Object.assign({}, environmentFlag, { - enabled: flag.default_enabled, - feature_state_value: Utils.getTypedValue( - flag.initial_value, - undefined, - true, - ), - }), ) - }) - } else { - prom = data.post( - `${Project.api}environments/${environmentId}/featurestates/`, - Object.assign({}, flag, { - enabled: false, - environment: environmentId, - feature: projectFlag, - }), - ) - } - - const segmentOverridesRequest = - mode === 'SEGMENT' && segmentOverrides - ? (segmentOverrides.length - ? updateSegmentPriorities( - getStore(), - segmentOverrides.map((override, index) => ({ - id: override.id, - priority: index, - })), + .then((environmentFeatureStates) => { + const multivariate_feature_state_values = + environmentFeatureStates.multivariate_feature_state_values && + environmentFeatureStates.multivariate_feature_state_values.map( + (v) => { + const matching = + environmentFlag.multivariate_feature_state_values.find( + (m) => m.id === v.multivariate_feature_option, + ) || {} + return { + ...v, + percentage_allocation: + matching.default_percentage_allocation, + } + }, ) - : Promise.resolve([]) - ).then(() => - Promise.all( - segmentOverrides.map((override) => - data.put( - `${Project.api}features/featurestates/${override.feature_segment_value.id}/`, - { - ...override.feature_segment_value, - enabled: override.enabled, - feature_state_value: Utils.valueToFeatureState( - override.value, - ), - multivariate_feature_state_values: - override.multivariate_options && - override.multivariate_options.map((o) => { - if (o.multivariate_feature_option) return o - return { - multivariate_feature_option: - environmentFlag.multivariate_feature_state_values[ - o.multivariate_feature_option_index - ].multivariate_feature_option, - percentage_allocation: o.percentage_allocation, - } - }), - }, + environmentFlag.multivariate_feature_state_values = + multivariate_feature_state_values + return data.put( + `${Project.api}environments/${environmentId}/featurestates/${environmentFlag.id}/`, + Object.assign({}, environmentFlag, { + enabled: flag.default_enabled, + feature_state_value: Utils.getTypedValue( + flag.initial_value, + undefined, + true, ), - ), - ), - ) - : Promise.resolve() - - Promise.all([prom, segmentOverridesRequest]).then(([res, segmentRes]) => { - if (store.model) { - store.model.keyedEnvironmentFeatures[projectFlag.id] = res - if (segmentRes) { - const feature = _.find( - store.model.features, - (f) => f.id === projectFlag.id, - ) - if (feature) { - feature.feature_segments = _.map( - segmentRes.feature_segments, - (segment) => ({ - ...segment, - segment: segment.segment.id, }), ) - } - } + }) + } else { + prom = data.post( + `${Project.api}environments/${environmentId}/featurestates/`, + Object.assign({}, flag, { + enabled: false, + environment: environmentId, + feature: projectFlag, + }), + ) } - if (store.model) { - store.model.lastSaved = new Date().valueOf() - } - onComplete && onComplete() - store.saved({}) + const segmentOverridesRequest = + mode === 'SEGMENT' && segmentOverrides + ? (segmentOverrides.length + ? updateSegmentPriorities( + getStore(), + segmentOverrides.map((override, index) => ({ + id: override.id, + priority: index, + })), + ) + : Promise.resolve([]) + ).then(() => + Promise.all( + segmentOverrides.map((override) => + data.put( + `${Project.api}features/featurestates/${override.feature_segment_value.id}/`, + { + ...override.feature_segment_value, + enabled: override.enabled, + feature_state_value: Utils.valueToFeatureState( + override.value, + ), + multivariate_feature_state_values: + override.multivariate_options && + override.multivariate_options.map((o) => { + if (o.multivariate_feature_option) return o + return { + multivariate_feature_option: + environmentFlag + .multivariate_feature_state_values[ + o.multivariate_feature_option_index + ].multivariate_feature_option, + percentage_allocation: o.percentage_allocation, + } + }), + }, + ), + ), + ), + ) + : Promise.resolve() + + Promise.all([prom, segmentOverridesRequest]) + .then(([res, segmentRes]) => { + if (store.model) { + store.model.keyedEnvironmentFeatures[projectFlag.id] = res + if (segmentRes) { + const feature = _.find( + store.model.features, + (f) => f.id === projectFlag.id, + ) + if (feature) { + feature.feature_segments = _.map( + segmentRes.feature_segments, + (segment) => ({ + ...segment, + segment: segment.segment.id, + }), + ) + } + } + } + + if (store.model) { + store.model.lastSaved = new Date().valueOf() + } + onComplete && onComplete() + store.saved({}) + }) + .catch((e) => { + API.ajaxHandler(store, e) + }) + }) + .catch((e) => { + API.ajaxHandler(store, e) }) - }) }, editFeatureStateChangeRequest: async ( projectId: string, @@ -787,13 +782,17 @@ const controller = { ) } - prom.then((res) => { - if (store.model) { - store.model.lastSaved = new Date().valueOf() - } - onComplete && onComplete() - store.saved({}) - }) + prom + .then((res) => { + if (store.model) { + store.model.lastSaved = new Date().valueOf() + } + onComplete && onComplete() + store.saved({}) + }) + .catch((e) => { + API.ajaxHandler(store, e) + }) }, getFeatureUsage(projectId, environmentId, flag, period) { data @@ -863,16 +862,17 @@ const controller = { return Promise.all([ data.get(featuresEndpoint), - recursivePageGet( - `${Project.api}environments/${environmentId}/featurestates/?page_size=${PAGE_SIZE}`, - ), feature ? data.get( `${Project.api}projects/${projectId}/features/${feature}/`, ) : Promise.resolve(), ]) - .then(([features, environmentFeatures, feature]) => { + .then(([features, feature]) => { + const environmentFeatures = features.results.map((v) => ({ + ...v.environment_feature_state, + feature: v.id, + })) if (store.filter !== filter) { //The filter has been changed since, ignore the api response. This will be resolved when moving to RTK. return @@ -904,9 +904,10 @@ const controller = { store.model = { features: features.results.map(controller.parseFlag), - keyedEnvironmentFeatures: - environmentFeatures.results && - _.keyBy(environmentFeatures.results, 'feature'), + keyedEnvironmentFeatures: _.keyBy( + environmentFeatures, + 'feature', + ), } store.loaded() }) diff --git a/frontend/common/types/requests.ts b/frontend/common/types/requests.ts index 6c1dd38321ea..b97b023de8c4 100644 --- a/frontend/common/types/requests.ts +++ b/frontend/common/types/requests.ts @@ -397,6 +397,12 @@ export type Req = { organisation_id: string github_id: string id: string + body: { + project: number + repository_name: string + repository_owner: string + tagging_enabled: boolean + } } deleteGithubRepository: { organisation_id: string diff --git a/frontend/common/types/responses.ts b/frontend/common/types/responses.ts index 15159fee182c..487c55ada915 100644 --- a/frontend/common/types/responses.ts +++ b/frontend/common/types/responses.ts @@ -115,7 +115,7 @@ export type ExternalResource = { url: string type: string project?: number - metadata?: { state?: string; title?: string } + metadata?: { [key: string]: string | number | boolean } feature: number } @@ -160,12 +160,14 @@ export type LaunchDarklyProjectImport = { project: number } -export type GithubResources = { +export type GithubResource = { html_url: string id: number number: number title: string state: string + merged: boolean + draft: boolean } export type GithubPaginatedRepos = { @@ -187,6 +189,7 @@ export type GithubRepository = { project: number repository_owner: string repository_name: string + tagging_enabled: boolean } export type githubIntegration = { @@ -685,7 +688,7 @@ export type Res = { externalResource: PagedResponse githubIntegrations: PagedResponse githubRepository: PagedResponse - githubResources: GitHubPagedResponse + githubResources: GitHubPagedResponse githubRepos: GithubPaginatedRepos segmentPriorities: {} featureSegment: FeatureState['feature_segment'] diff --git a/frontend/common/useInfiniteScroll.ts b/frontend/common/useInfiniteScroll.ts index 1dc1e3d975df..a10967cf4395 100644 --- a/frontend/common/useInfiniteScroll.ts +++ b/frontend/common/useInfiniteScroll.ts @@ -55,8 +55,11 @@ const useInfiniteScroll = < }, throttle) const refresh = useCallback(() => { - setLocalPage(1) - }, []) + queryResponse.refetch().then((newData) => { + setCombinedData(newData as unknown as RES) + setLocalPage(1) + }) + }, [queryResponse]) const loadMore = () => { if (queryResponse?.data?.next) { @@ -70,6 +73,7 @@ const useInfiniteScroll = < isLoading: queryResponse.isLoading, loadMore, loadingCombinedData: loadingCombinedData && queryResponse.isFetching, + // refetchData, refresh, response: queryResponse, searchItems, diff --git a/frontend/web/components/ErrorMessage.js b/frontend/web/components/ErrorMessage.js index 5f0bd1b927be..b3961117ef3f 100644 --- a/frontend/web/components/ErrorMessage.js +++ b/frontend/web/components/ErrorMessage.js @@ -28,7 +28,9 @@ export default class ErrorMessage extends PureComponent { - {typeof error === 'object' ? ( + {error instanceof Error ? ( + error.message + ) : typeof error === 'object' ? (
= ({ + environmentId, featureId, linkedExternalResources, organisationId, @@ -36,8 +40,9 @@ const AddExternalResourceRow: FC = ({ repoOwner, }) => { const [externalResourceType, setExternalResourceType] = useState('') - const [featureExternalResource, setFeatureExternalResource] = - useState('') + const [featureExternalResource, setFeatureExternalResource] = useState< + GithubResource | undefined + >(undefined) const [lastSavedResource, setLastSavedResource] = useState< string | undefined >(undefined) @@ -68,11 +73,13 @@ const AddExternalResourceRow: FC = ({ repoOwner={repoOwner} repoName={repoName} githubResource={ - ( - _.find(_.values(Constants.resourceTypes), { - label: externalResourceType!, - }) as any - ).resourceType || '' + (externalResourceType && + ( + _.find(_.values(Constants.resourceTypes), { + label: externalResourceType!, + }) as any + ).resourceType) || + '' } > = ({ key as keyof typeof Constants.resourceTypes ].label === externalResourceType, ) - createExternalResource({ - body: { - feature: parseInt(featureId), - metadata: {}, - type: type!, - url: featureExternalResource, - }, - feature_id: featureId, - project_id: projectId, - }).then(() => { - toast('External Resource Added') - setLastSavedResource(featureExternalResource) - }) + if (type && featureExternalResource) { + createExternalResource({ + body: { + feature: parseInt(featureId), + metadata: { + 'draft': featureExternalResource.draft, + 'merged': featureExternalResource.merged, + 'state': featureExternalResource.state, + 'title': featureExternalResource.title, + }, + type: type, + url: featureExternalResource.html_url, + }, + feature_id: featureId, + project_id: projectId, + }).then(() => { + toast('External Resource Added') + setLastSavedResource(featureExternalResource.html_url) + AppActions.refreshFeatures(parseInt(projectId), environmentId) + }) + } else { + throw new Error('Invalid External Resource Data') + } }} > Save @@ -118,6 +135,7 @@ const AddExternalResourceRow: FC = ({ } const ExternalResourcesLinkTab: FC = ({ + environmentId, featureId, githubId, organisationId, @@ -157,6 +175,7 @@ const ExternalResourcesLinkTab: FC = ({ /> {repoName && repoOwner && ( void loadingCombinedData: boolean nextPage?: string searchItems: (search: string) => void + refresh: () => void } const GitHubResourceSelectContext = createContext< @@ -34,7 +35,7 @@ export const GitHubResourceSelectProvider: FC< GitHubResourceSelectProviderType > = ({ children, ...props }) => { const [externalResourcesSelect, setExternalResourcesSelect] = - useState() + useState() const throttleDelay = 300 @@ -44,6 +45,7 @@ export const GitHubResourceSelectProvider: FC< isLoading, loadMore, loadingCombinedData, + refresh, searchItems, } = useInfiniteScroll( useGetGithubResourcesQuery, @@ -62,7 +64,7 @@ export const GitHubResourceSelectProvider: FC< useEffect(() => { if (results && props.linkedExternalResources) { setExternalResourcesSelect( - results.filter((i: GithubResources) => { + results.filter((i: GithubResource) => { const same = props.linkedExternalResources?.some( (r) => i.html_url === r.url, ) @@ -84,6 +86,7 @@ export const GitHubResourceSelectProvider: FC< loadMore, loadingCombinedData, nextPage: next, + refresh, searchItems, }} > diff --git a/frontend/web/components/GitHubResourcesSelect.tsx b/frontend/web/components/GitHubResourcesSelect.tsx index cc708fb7a901..8b814ec62fb3 100644 --- a/frontend/web/components/GitHubResourcesSelect.tsx +++ b/frontend/web/components/GitHubResourcesSelect.tsx @@ -1,9 +1,13 @@ import React, { FC, useEffect, useRef, useState } from 'react' -import { GithubResources } from 'common/types/responses' +import { GithubResource } from 'common/types/responses' import Utils from 'common/utils/utils' import { FixedSizeList } from 'react-window' import InfiniteLoader from 'react-window-infinite-loader' import { useGitHubResourceSelectProvider } from './GitHubResourceSelectProvider' +import { components } from 'react-select' +import Button from './base/forms/Button' +import Icon from './Icon' +import Select from 'react-select' type MenuListType = { children: React.ReactNode @@ -106,6 +110,23 @@ const MenuList: FC = ({ ) } +const CustomControl = ({ + children, + ...props +}: { + children: React.ReactNode +}) => { + const { refresh } = useGitHubResourceSelectProvider() + return ( + + {children} + + + ) +} + export type GitHubResourcesSelectType = { onChange: (value: string) => void lastSavedResource: string | undefined @@ -119,15 +140,8 @@ const GitHubResourcesSelect: FC = ({ lastSavedResource, onChange, }) => { - const { - githubResources, - isFetching, - isLoading, - loadMore, - loadingCombinedData, - nextPage, - searchItems, - } = useGitHubResourceSelectProvider() + const { githubResources, isFetching, isLoading, searchItems } = + useGitHubResourceSelectProvider() const [selectedOption, setSelectedOption] = useState(null) const [searchText, setSearchText] = React.useState('') @@ -152,11 +166,10 @@ const GitHubResourcesSelect: FC = ({ onChange(v?.value) }} isClearable={true} - options={githubResources?.map((i: GithubResources) => { + options={githubResources?.map((i: GithubResource) => { return { label: `${i.title} #${i.number}`, - status: i.state, - value: i.html_url, + value: i, } })} noOptionsMessage={() => @@ -171,6 +184,7 @@ const GitHubResourcesSelect: FC = ({ searchItems(Utils.safeParseEventValue(e)) }} components={{ + Control: CustomControl, MenuList, }} data={{ searchText }} diff --git a/frontend/web/components/GithubRepositoriesTable.tsx b/frontend/web/components/GithubRepositoriesTable.tsx index d8fdc7f4eaf0..1898d5bb607c 100644 --- a/frontend/web/components/GithubRepositoriesTable.tsx +++ b/frontend/web/components/GithubRepositoriesTable.tsx @@ -1,9 +1,14 @@ import React, { FC, useEffect } from 'react' -import { useDeleteGithubRepositoryMutation } from 'common/services/useGithubRepository' +import { + useDeleteGithubRepositoryMutation, + useUpdateGithubRepositoryMutation, +} from 'common/services/useGithubRepository' import Button from './base/forms/Button' import Icon from './Icon' import PanelSearch from './PanelSearch' import { GithubRepository } from 'common/types/responses' +import Switch from './Switch' +import Tooltip from './Tooltip' export type GithubRepositoriesTableType = { repos: GithubRepository[] | undefined @@ -16,15 +21,6 @@ const GithubRepositoriesTable: FC = ({ organisationId, repos, }) => { - const [deleteGithubRepository, { isSuccess: isDeleted }] = - useDeleteGithubRepositoryMutation() - - useEffect(() => { - if (isDeleted) { - toast('Repository unlinked to Project') - } - }, [isDeleted]) - return (
= ({ header={ Repository +
+ {' '} + + {'Add Tags and Labels'} + +
+ } + place='top' + > + { + 'If enabled, features will be tagged with the GitHub resource type, and the Issue/PR will have the label "Flagsmith flag"' + } + +
Remove
} renderRow={(repo: GithubRepository) => ( - - -
{`${repo.repository_owner} - ${repo.repository_name}`}
-
-
- -
-
+ )} />
) } +const TableRow: FC<{ + githubId: string + organisationId: string + repo: GithubRepository +}> = ({ githubId, organisationId, repo }) => { + const [deleteGithubRepository, { isSuccess: isDeleted }] = + useDeleteGithubRepositoryMutation() + + useEffect(() => { + if (isDeleted) { + toast('Repository unlinked to Project') + } + }, [isDeleted]) + + const [updateGithubRepository] = useUpdateGithubRepositoryMutation() + const [taggingenEnabled, setTaggingEnabled] = React.useState( + repo.tagging_enabled || false, + ) + return ( + + +
{`${repo.repository_owner} - ${repo.repository_name}`}
+
+
+ { + updateGithubRepository({ + body: { + project: repo.project, + repository_name: repo.repository_name, + repository_owner: repo.repository_owner, + tagging_enabled: !repo.tagging_enabled || false, + }, + github_id: githubId, + id: `${repo.id}`, + organisation_id: organisationId, + }).then(() => { + setTaggingEnabled(!taggingenEnabled) + }) + }} + /> +
+
+ +
+
+ ) +} + export default GithubRepositoriesTable diff --git a/frontend/web/components/Icon.tsx b/frontend/web/components/Icon.tsx index dd4bdd56969d..6f23a68bfd4e 100644 --- a/frontend/web/components/Icon.tsx +++ b/frontend/web/components/Icon.tsx @@ -55,6 +55,12 @@ export type IconName = | 'required' | 'more-vertical' | 'open-external-link' + | 'issue-closed' + | 'issue-linked' + | 'pr-merged' + | 'pr-draft' + | 'pr-linked' + | 'pr-closed' export type IconType = React.DetailedHTMLProps< React.HTMLAttributes, @@ -1232,6 +1238,91 @@ const Icon: FC = ({ fill, fill2, height, name, width, ...rest }) => { ) } + case 'pr-merged': { + return ( + + + + ) + } + case 'issue-closed': { + return ( + + + + + ) + } + case 'issue-linked': { + return ( + + + + + ) + } + case 'pr-linked': { + return ( + + + + ) + } + case 'pr-closed': { + return ( + + + + ) + } + case 'pr-draft': { + return ( + + + + ) + } default: return null } diff --git a/frontend/web/components/PermissionsTabs.tsx b/frontend/web/components/PermissionsTabs.tsx index 34ecf7d95b6b..73d9e535a9e5 100644 --- a/frontend/web/components/PermissionsTabs.tsx +++ b/frontend/web/components/PermissionsTabs.tsx @@ -15,6 +15,7 @@ import RolePermissionsList from './RolePermissionsList' import ProjectFilter from './ProjectFilter' import OrganisationStore from 'common/stores/organisation-store' import PlanBasedAccess from './PlanBasedAccess' +import WarningMessage from './WarningMessage' type PermissionsTabsType = { orgId?: number @@ -56,8 +57,27 @@ const PermissionsTabs: FC = ({ return } + const deprecationMessage = ( +
+ Group-level permissions are deprecated. Assign{' '} + + roles + {' '} + to this group instead.{' '} + + Learn more + + . +
+ ) + return ( + {!!group && } )} diff --git a/frontend/web/components/modals/CreateSAML.tsx b/frontend/web/components/modals/CreateSAML.tsx index b81e6d153334..2e83592ebc0b 100644 --- a/frontend/web/components/modals/CreateSAML.tsx +++ b/frontend/web/components/modals/CreateSAML.tsx @@ -69,7 +69,7 @@ const CreateSAML: FC = ({ organisationId, samlName }) => { return regularExpresion.test(name) } - const convetToXmlFile = (fileName: string, data: string) => { + const convertToXmlFile = (fileName: string, data: string) => { const blob = new Blob([data], { type: 'application/xml' }) const link = document.createElement('a') link.download = `${fileName}.xml` @@ -82,7 +82,7 @@ const CreateSAML: FC = ({ organisationId, samlName }) => { getSamlConfigurationMetadata(getStore(), { name: previousName }) .then((res) => { if (res.error) { - convetToXmlFile(previousName, res.error.data) + convertToXmlFile(previousName, res.error.data) } }) .finally(() => { @@ -92,7 +92,7 @@ const CreateSAML: FC = ({ organisationId, samlName }) => { const downloadIDPMetadata = () => { const name = data?.name || samlName - convetToXmlFile(`IDP metadata ${name!}`, data?.idp_metadata_xml || '') + convertToXmlFile(`IDP metadata ${name!}`, data?.idp_metadata_xml || '') } const Tab1 = ( @@ -123,7 +123,7 @@ const CreateSAML: FC = ({ organisationId, samlName }) => { data-test='frontend-url' tooltip='The base URL of the Flagsmith dashboard' tooltipPlace='right' - value={frontendUrl} + value={data?.frontend_url || frontendUrl} onChange={(event: React.ChangeEvent) => { setFrontendUrl(Utils.safeParseEventValue(event)) }} diff --git a/frontend/web/components/tags/TagContent.tsx b/frontend/web/components/tags/TagContent.tsx index 3652d6ae9e08..106b56a25568 100644 --- a/frontend/web/components/tags/TagContent.tsx +++ b/frontend/web/components/tags/TagContent.tsx @@ -9,6 +9,7 @@ import { getTagColor } from './Tag' import OrganisationStore from 'common/stores/organisation-store' import Utils from 'common/utils/utils' import classNames from 'classnames' +import Icon from 'components/Icon' type TagContent = { tag: Partial } @@ -19,6 +20,44 @@ function escapeHTML(unsafe: string) { ) } +const renderIcon = (tagType: string, tagColor: string, tagLabel: string) => { + switch (tagType) { + case 'STALE': + return ( + + ) + case 'GITHUB': + switch (tagLabel) { + case 'PR Open': + return + case 'PR Merged': + return + case 'PR Closed': + return + case 'PR Draft': + return + case 'Issue Open': + return + case 'Issue Closed': + return + default: + return + } + default: + return ( + + ) + } +} + const getTooltip = (tag: TTag | undefined) => { if (!tag) { return null @@ -86,21 +125,7 @@ const TagContent: FC = ({ tag }) => { })} > {tagLabel} - {tag.type === 'STALE' ? ( - - ) : ( - tag.is_permanent && ( - - ) - )} + {renderIcon(tag.type!, tag.color!, tag.label!)} } > diff --git a/frontend/web/project/api.js b/frontend/web/project/api.js index 1f33a17a6bf4..8ac745d78ec9 100644 --- a/frontend/web/project/api.js +++ b/frontend/web/project/api.js @@ -16,6 +16,11 @@ global.API = { } // Catch coding errors that end up here + if (typeof res === 'string') { + store.error = new Error(res) + store.goneABitWest() + return + } if (res instanceof Error) { console.error(res) store.error = res diff --git a/infrastructure/aws/production/ecs-task-definition-task-processor.json b/infrastructure/aws/production/ecs-task-definition-task-processor.json index 769e2764e6e4..781de824f235 100644 --- a/infrastructure/aws/production/ecs-task-definition-task-processor.json +++ b/infrastructure/aws/production/ecs-task-definition-task-processor.json @@ -139,7 +139,7 @@ }, { "name": "TASK_DELETE_RETENTION_DAYS", - "value": "44" + "value": "7" }, { "name": "TASK_DELETE_BATCH_SIZE", diff --git a/infrastructure/aws/production/ecs-task-definition-web.json b/infrastructure/aws/production/ecs-task-definition-web.json index 32fce8246103..ceed5987901c 100644 --- a/infrastructure/aws/production/ecs-task-definition-web.json +++ b/infrastructure/aws/production/ecs-task-definition-web.json @@ -155,6 +155,10 @@ "name": "CACHE_BAD_ENVIRONMENTS_SECONDS", "value": "60" }, + { + "name": "FEATURE_EVALUATION_CACHE_SECONDS", + "value": "300" + }, { "name": "EDGE_RELEASE_DATETIME", "value": "2022-06-07T10:00:00Z" diff --git a/version.txt b/version.txt index 6be1c8e9ff6e..ac8faeddc8a7 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.134.0 +2.136.0