diff --git a/.env b/.env index b802753a..e27aca46 100644 --- a/.env +++ b/.env @@ -12,6 +12,11 @@ TAXONOMY_EDITOR_EXPOSE=127.0.0.1:8091 # this one is needed only in dev, to tell nginx and fastapi, which port urls should include # it must either start with : or be empty PUBLIC_TAXONOMY_EDITOR_PORT=:8091 +# this one is used to expose the websocket in dev and shoudl match PUBLIC_TAXONOMY_EDITOR_PORT but without leading ":" +WDS_SOCKET_PORT=8091 +# API scheme is useful because, in prod, we have to proxy and already proxied request +# and loose the original scheme +API_SCHEME=http # This is the PAT (Personal Access Token) # to create PRs on openfoodfacts-server github project (must be able to read-write PRs) @@ -24,3 +29,13 @@ REPO_URI=openfoodfacts/openfoodfacts-server # eventually set this to your local user id to avoid permissions errors # USER_UID=1000 # USER_GID=1000 + +# Neo4J configurations +NEO4J_BOLT_EXPOSE=127.0.0.1:7687 +NEO4J_ADMIN_EXPOSE=127.0.0.1:7474 +# note: in prod, heap_initial__size and max__size should match, but it's ok like that for dev +NEO4J_server_memory_heap_initial__size=512M +NEO4J_server_memory_heap_max__size=2G +NEO4J_server_memory_pagecache_size=1G +NEO4J_db_memory_transaction_total_max=512M + diff --git a/.github/workflows/auto-assign-pr.yml b/.github/workflows/auto-assign-pr.yml index 299f76cd..1f2a66b7 100644 --- a/.github/workflows/auto-assign-pr.yml +++ b/.github/workflows/auto-assign-pr.yml @@ -12,4 +12,4 @@ jobs: assign-author: runs-on: ubuntu-latest steps: - - uses: toshimaru/auto-author-assign@v1.6.1 + - uses: toshimaru/auto-author-assign@v2.0.1 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 977f6851..4bf4dec9 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/container-build.yml b/.github/workflows/container-build.yml index a8dc79f5..0fdfc324 100644 --- a/.github/workflows/container-build.yml +++ b/.github/workflows/container-build.yml @@ -3,7 +3,7 @@ name: Docker Image Build CI on: push: branches: - - $default-branch + - main - deploy-* tags: - v*.*.* @@ -17,7 +17,7 @@ jobs: - taxonomy_frontend - taxonomy_api steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 1 @@ -61,7 +61,7 @@ jobs: type=sha,format=long - name: Build and push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v4 with: context: ${{ env.build_context }} push: true diff --git a/.github/workflows/container-deploy.yml b/.github/workflows/container-deploy.yml index 6a5309a9..ca2fb132 100644 --- a/.github/workflows/container-deploy.yml +++ b/.github/workflows/container-deploy.yml @@ -69,7 +69,7 @@ jobs: timeoutSeconds: 600 # 10m - name: Do something if build isn't launched - if: steps.wait-build-frontend.outputs.conclusion == 'does not exist' || steps.wait-build-api.outputs.conclusion == 'does not exist' + if: steps.wait-build-frontend.outputs.conclusion == 'not found' || steps.wait-build-api.outputs.conclusion == 'not found' run: echo job does not exist && true - name: Do something if build fail @@ -134,11 +134,19 @@ jobs: echo "COMPOSE_FILE=docker-compose.yml;docker/prod.yml" >> .env echo "DOCKER_TAG=sha-${{ github.sha }}" >> .env + # Neo4j configuration + echo "NEO4J_server_memory_heap_initial__size=3G" >> .env + echo "NEO4J_server_memory_heap_max__size=3G" >> .env + echo "NEO4J_server_memory_pagecache_size=2G" >> .env + # we don't want transaction to grow too big + echo "NEO4J_db_memory_transaction_total_max=1G" >> .env # App environment variables echo "TAXONOMY_EDITOR_EXPOSE=${{ env.TAXONOMY_EDITOR_EXPOSE }}" >> .env echo "TAXONOMY_EDITOR_DOMAIN=${{ env.TAXONOMY_EDITOR_DOMAIN }}" >> .env # should be blank in production echo "PUBLIC_TAXONOMY_EDITOR_PORT=" >> .env + # we use https + echo "API_SCHEME=https" >> .env # the PAT is environment dependant # and must have write access to PRs on the target repo (see REPO_URI) echo "GITHUB_PAT=${{ secrets.OFF_SERVER_GITHUB_PAT }}" >> .env @@ -212,7 +220,7 @@ jobs: cd ${{ matrix.env }} docker system prune -af - - uses: frankie567/grafana-annotation-action@v1.0.2 + - uses: frankie567/grafana-annotation-action@v1.0.3 if: ${{ always() }} with: apiHost: https://grafana.openfoodfacts.org diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index b0dedc42..4e751977 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -15,6 +15,6 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: 'Dependency Review' uses: actions/dependency-review-action@v3 diff --git a/.github/workflows/github-projects-for-openfoodfacts-design.yml b/.github/workflows/github-projects-for-openfoodfacts-design.yml index 9a85f688..f72074e5 100644 --- a/.github/workflows/github-projects-for-openfoodfacts-design.yml +++ b/.github/workflows/github-projects-for-openfoodfacts-design.yml @@ -16,3 +16,9 @@ jobs: github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} labeled: mockups available, needs mockup label-operator: OR + - uses: actions/add-to-project@main + with: + project-url: https://github.com/orgs/openfoodfacts/projects/25 # Add issue to the documentation project + github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} + labeled: documentation + label-operator: OR diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 0baefa9f..7a41e75d 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -4,40 +4,7 @@ on: pull_request: jobs: - # Parser quality - parser-checks: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: - - "3.10" - - "3.9" - - steps: - #---------------------------------------------- - # check-out repo and set-up python - #---------------------------------------------- - - name: Check out repository - uses: actions/checkout@v3 - - name: build docker - run: | - export USER_UID=$(id -u) - export USER_GID=$(id -g) - DOCKER_BUILDKIT=1 docker-compose build - - name: Set up python - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install Packages - run: | - cd parser - pip install -r requirements-dev.txt - - name: Make checks - run: | - cd parser - make checks - - # taxonomy editor quality + # taxonomy editor quality for frontend and backend taxonomy-editor-checks: runs-on: ubuntu-latest steps: @@ -45,7 +12,7 @@ jobs: # check-out repo #---------------------------------------------- - name: Check out repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Make checks run: | export USER_UID=$(id -u) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e56b6b48..b2d70610 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,7 +55,7 @@ Ready to contribute code? Here's how to set up Taxonomy Editor for local develop ``` git clone git@github.com:your_name_here/taxonomy-editor.git ``` -3. Follow [install documentation](./docs/explanations/docker-compose-setup.md) +3. Follow [install documentation](./doc/introduction/setup-dev.md) 4. code! @@ -96,4 +96,4 @@ Before you submit a pull request, check that it meets these guidelines: 2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring. -This contributing page was adapted from [Pyswarms documentation](https://github.com/ljvmiranda921/pyswarms/blob/master/CONTRIBUTING.rst). \ No newline at end of file +This contributing page was adapted from [Pyswarms documentation](https://github.com/ljvmiranda921/pyswarms/blob/master/CONTRIBUTING.rst). diff --git a/Makefile b/Makefile index 71b8061d..40e6d0cc 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,11 @@ +# to suppress the path translation in Windows +export MSYS_NO_PATHCONV=1 + +ifeq ($(findstring cmd.exe,$(SHELL)),cmd.exe) + $(error "We do not suppport using cmd.exe on Windows, please run in a 'git bash' console") +endif + +# use bash everywhere ! SHELL := /bin/bash ENV_FILE ?= .env @@ -13,22 +21,45 @@ export DOCKER_BUILDKIT=1 export COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_COMPOSE=docker-compose --env-file=${ENV_FILE} -DOCKER_COMPOSE_TEST=COMPOSE_PROJECT_NAME=test_taxonomy docker-compose --env-file=${ENV_FILE} +# tweak some config to avoid port conflicts +DOCKER_COMPOSE_TEST=COMPOSE_PROJECT_NAME=test_taxonomy NEO4J_ADMIN_EXPOSE=127.0.0.1:7475 NEO4J_BOLT_EXPOSE=127.0.0.1:7688 docker-compose --env-file=${ENV_FILE} .PHONY: tests +#------------# +# dev setup # +#------------# + +build: + @echo "🍜 Building docker images" + ${DOCKER_COMPOSE} build + @echo "🍜 Project setup done" + +up: + @echo "🍜 Running project (ctrl+C to stop)" + ${DOCKER_COMPOSE} up + +dev: build up + + #-----------# # dev tools # #-----------# + # lint code -lint: backend_lint +lint: backend_lint frontend_lint backend_lint: @echo "🍜 Linting python code" ${DOCKER_COMPOSE} run --rm taxonomy_api isort . ${DOCKER_COMPOSE} run --rm taxonomy_api black . +frontend_lint: + @echo "🍜 Linting react code" + ${DOCKER_COMPOSE} run --rm taxonomy_node npx prettier -w src/ + + # check code quality quality: backend_quality frontend_quality @@ -40,7 +71,11 @@ backend_quality: frontend_quality: @echo "🍜 Quality checks JS" + ${DOCKER_COMPOSE} run --rm taxonomy_node npx prettier -c src/ ${DOCKER_COMPOSE} run --rm -e CI=true taxonomy_node npm run build +# restore the .empty file (if possible) + git checkout taxonomy-editor-frontend/build/.empty || true + tests: backend_tests @@ -48,7 +83,8 @@ tests: backend_tests backend_tests: @echo "🍜 Running python tests" ${DOCKER_COMPOSE_TEST} up -d neo4j - ${DOCKER_COMPOSE_TEST} run --rm taxonomy_api pytest . /parser + ${DOCKER_COMPOSE_TEST} run --rm taxonomy_api pytest /parser /parser + ${DOCKER_COMPOSE_TEST} run --rm taxonomy_api pytest /code/tests ${DOCKER_COMPOSE_TEST} stop neo4j checks: quality tests @@ -61,4 +97,3 @@ checks: quality tests create_external_volumes: @echo "🍜 Creating external volumes (production only) …" docker volume create ${COMPOSE_PROJECT_NAME}_neo4j-data - diff --git a/README.md b/README.md index ba8b613c..e20ae257 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,86 @@ # Taxonomy Editor - - - + + +

-![License](https://img.shields.io/github/license/openfoodfacts/taxonomy-editor?style=for-the-badge&color=green) -![Github Issues](https://img.shields.io/github/issues/openfoodfacts/taxonomy-editor?style=for-the-badge&color=critical) -![Github Repo Size](https://img.shields.io/github/repo-size/openfoodfacts/taxonomy-editor?style=for-the-badge&color=aqua) -[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge)](http://makeapullrequest.com) +[![Backers on Open Collective](https://opencollective.com/openfoodfacts-server/backers/badge.svg)](#backers) +[![Sponsors on Open Collective](https://opencollective.com/openfoodfacts-server/sponsors/badge.svg)](#sponsors) +![GitHub language count](https://img.shields.io/github/languages/count/openfoodfacts/taxonomy-editor) +![GitHub top language](https://img.shields.io/github/languages/top/openfoodfacts/taxonomy-editor) +![GitHub last commit](https://img.shields.io/github/last-commit/openfoodfacts/taxonomy-editor) +![Github Repo Size](https://img.shields.io/github/repo-size/openfoodfacts/taxonomy-editor) +![License](https://img.shields.io/github/license/openfoodfacts/taxonomy-editor?color=green) +![Github Issues](https://img.shields.io/github/issues/openfoodfacts/taxonomy-editor?color=critical) +![Github Repo Size](https://img.shields.io/github/repo-size/openfoodfacts/taxonomy-editor?color=aqua) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com) + +## What is this project about? + +TLDR: Taxonomies are at the 🧡 of Open Food Facts data structure. This project provides an user-friendly editor for editing taxonomies easily. + +The Open Food Facts database contains a lot of information on food products, such as ingredients, labels, additives etc. + +Because food industry evolves and there can be local peculiarities, we always let people freely enter informations (with suggestions) and we structure afterwards. Structured informations can be more easily exploited (for example to compute the Nutri-Score, detect allergens, etc.). +Taxonomy also brings more informations about products (for example, if an ingredient is vegan, a link to other databases like agribalyse or wikidata…). +Hence, taxonomies are at the heart of data structures in the Open Food Facts database and must be maintained properly. +For more information see the [wiki page about Taxonomies](https://wiki.openfoodfacts.org/Global_taxonomies). + +Currently a taxonomy in Open Food Facts is a raw text file containing a Directed Acyclic Graph (DAG) where each leaf node has one or more parent nodes. +The [taxonomy files present in Open Food Facts](https://github.com/openfoodfacts/openfoodfacts-server/tree/main/taxonomies) are long to read (ingredients.txt taxonomy alone has around 80000 lines!) and cumbersome to edit by contributors. It's also difficult to have a high level overview of a taxonomy or grasp its structure. + +This project aims to provide a web based user-friendly interface for editing taxonomies with ease. + +This tool can help a lot: +* enable searching and navigating the taxonomy +* enable anyone to contribute translations and synonyms, thus enriching the taxonomy +* help spot problems in the taxonomy (missing translations, missing paths, etc.), and get useful statistics about it +* provide helpers to assist power contributors in enriching the taxonomy (eg. find corresponding wikidata entry) +* offer an API to the taxonomy for third party applications (complementing the existing API) + +## Why is this project appealing? + +* Python and ReactJS tech stack (a powerful combo) +* Runs on a easy-to-learn graph database - Neo4J, using which you can do a lot of useful and interesting requests +* not so huge and focused, you can quickly get your hands on it +* it can have a huge impact for Open Food Facts: + * more language support: reaching more countries + * better analysis of ingredients: more allergy detection, potential finer computation of environment scorer + * better classification of products: enable product comparisons, environment score computations, etc. + * enabling more taxonomies: for example on brands to know food producers + +## How to help + +Currently we are focusing on bringing the application to a minimum viable product. See [Move to MVP v2](https://github.com/openfoodfacts/taxonomy-editor/issues/167). +A typescript migration is in progress, and we are also trying to simplify the API as well. + +* [GitHub Project](https://github.com/orgs/openfoodfacts/projects/28/views/1)   +* [Meeting Notes](https://docs.google.com/document/d/1tdYkUmoRU8BxFPdCwtewoUi7PV8PmDlXtExOcPYyu-I/edit#)

+ +

Monthly meetings

+ +- We e-meet [Tuesdays at 17:00 Paris Time](https://dateful.com/convert/paris-france?t=5pm) every first Tuesday of the month. +- ![Google Meet](https://img.shields.io/badge/Google%20Meet-00897B?logo=google-meet&logoColor=white) Video call link: https://meet.google.com/xin-bwzm-xvj +- Join by phone: https://tel.meet/nnw-qswu-hza?pin=2111028061202 +- Add the Event to your Calendar by [adding the Open Food Facts community calendar to your calendar](https://wiki.openfoodfacts.org/Events) +- [Agenda](https://docs.google.com/document/d/1tdYkUmoRU8BxFPdCwtewoUi7PV8PmDlXtExOcPYyu-I): please add the Agenda items as early as you can. Make sure to check the Agenda items in advance of the meeting, so that we have the most informed discussions possible. +- The meeting will handle Agenda items first, and if time permits, collaborative bug triage. +- We strive to timebox the core of the meeting (decision making) to 30 minutes, with an optional free discussion/live debugging afterwards. +- We take comprehensive notes in the Weekly Agenda of agenda item discussions and of decisions taken. +
-Taxonomies are at the 🧡 of Open Food Facts data structure. This project provides an user-friendly editor for editing taxonomies easily. -Please visit the [doc folder](./doc) for more documentation about the Taxonomy Editor. -> This documentation tries to follow as much as possible the documentation system from [Diataxis](https://diataxis.fr/). ## Getting Started - Join us on Slack at https://openfoodfacts.slack.com/ in the channel `#taxonomy-editor`. +- Get an invite to our organization using https://slack.openfoodfacts.org/ +- Check out the Taxonomy Editor in our pre-production environment: + - The UI: https://ui.taxonomy.openfoodfacts.net/ + - The API: https://api.taxonomy.openfoodfacts.net/ - Developer documentation: - [Setup a dev environment](./doc/introduction/setup-dev.md) - [Docker Compose Setup](./doc/how-to-guides/docker-compose-setup.md) @@ -27,13 +88,12 @@ Please visit the [doc folder](./doc) for more documentation about the Taxonomy E - Translate: Use [Crowdin](https://crowdin.com/project/openfoodfacts), project Open Food Facts. - Visit [this link](https://github.com/openfoodfacts/taxonomy-editor/issues) to report issues, give feature requests etc. -## Links +## Documentation -

+Please visit the [doc folder](./doc) for more documentation about the Taxonomy Editor. +This documentation tries to follow as much as possible the documentation system from [Diataxis](https://diataxis.fr/). -[Wiki Page](https://wiki.openfoodfacts.org/GSOC_2022_-_Taxonomy_editor)   -[GitHub Project](https://github.com/orgs/openfoodfacts/projects/28/views/1)   -[Meeting Notes](https://docs.google.com/document/d/1tdYkUmoRU8BxFPdCwtewoUi7PV8PmDlXtExOcPYyu-I/edit#)

+## User interface

Screenshots

@@ -41,11 +101,13 @@ Please visit the [doc folder](./doc) for more documentation about the Taxonomy E - - +
+ +- [![Figma](https://img.shields.io/badge/figma-%23F24E1E.svg?logo=figma&logoColor=white) Mockups on the current design and future plans to discuss](https://www.figma.com/file/7QxD2pOnVntjDPqbHHPGHv/Taxonomy-Editor?t=4YadI2GgSAXcPnlo-0) + ## Contributors diff --git a/backend/editor/api.py b/backend/editor/api.py index d6e9e428..ae236e24 100644 --- a/backend/editor/api.py +++ b/backend/editor/api.py @@ -1,443 +1,525 @@ -""" -Taxonomy Editor Backend API -""" -import logging -import os - -# Required imports -# ----------------------------------------------------------------------------# -from datetime import datetime - -# FastAPI -from fastapi import BackgroundTasks, FastAPI, HTTPException, Request, Response, status -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import FileResponse - -# DB helper imports -from . import graph_db -from .entries import TaxonomyGraph - -# Custom exceptions -from .exceptions import GithubBranchExistsError, GithubUploadError - -# Data model imports -from .models import Footer, Header - -# ----------------------------------------------------------------------------# - -# Setup logs -logging.basicConfig( - handlers=[logging.StreamHandler()], - level=logging.INFO, -) - -app = FastAPI(title="Open Food Facts Taxonomy Editor API") - -# Allow anyone to call the API from their own apps -app.add_middleware( - CORSMiddleware, - # FastAPI doc related to allow_origin (to avoid CORS issues): - # "It's also possible to declare the list as "*" (a "wildcard") to say that all are allowed. - # But that will only allow certain types of communication, excluding everything that involves - # credentials: Cookies, Authorization headers like those used with Bearer Tokens, etc. - # So, for everything to work correctly, it's better to specify explicitly the allowed origins." - # => Workaround: use allow_origin_regex - # Source: https://github.com/tiangolo/fastapi/issues/133#issuecomment-646985050 - allow_origin_regex="https?://.*", - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], - expose_headers=["*"], -) - - -@app.on_event("startup") -async def startup(): - """ - Initialize database - """ - graph_db.initialize_db() - - -@app.on_event("shutdown") -async def shutdown(): - """ - Shutdown database - """ - graph_db.shutdown_db() - - -@app.middleware("http") -async def initialize_neo4j_transactions(request: Request, call_next): - with graph_db.TransactionCtx(): - response = await call_next(request) - return response - - -# Helper methods - - -def check_single(id): - """ - Helper function for checking whether there is only a single entry with given id - """ - if len(id) == 0: - raise HTTPException(status_code=404, detail="Entry not found") - elif len(id) > 1: - raise HTTPException(status_code=500, detail="Multiple entries found") - - -def file_cleanup(filepath): - """ - Helper function to delete a taxonomy file from local storage - """ - try: - os.remove(filepath) - except Exception: - raise HTTPException(status_code=500, detail="Taxonomy file not found for deletion") - - -# Get methods - - -@app.get("/", status_code=status.HTTP_200_OK) -async def hello(): - return {"message": "Hello user! Tip: open /docs or /redoc for documentation"} - - -@app.get("/ping") -async def pong(response: Response): - """ - Check server health - """ - pong = datetime.now() - return {"ping": "pong @ %s" % pong} - - -@app.get("/projects") -async def list_all_projects(response: Response): - """ - List all open projects created in the Taxonomy Editor - """ - # Listing all projects doesn't require a taoxnomy name or branch name - taxonony = TaxonomyGraph("", "") - result = list(taxonony.list_existing_projects()) - return result - - -@app.get("/{taxonomy_name}/{branch}/nodes") -async def find_all_nodes(response: Response, branch: str, taxonomy_name: str): - """ - Get all nodes within taxonomy - """ - taxonomy = TaxonomyGraph(branch, taxonomy_name) - result = taxonomy.get_all_nodes("") - all_nodes = list(result) - return all_nodes - - -@app.get("/{taxonomy_name}/{branch}/rootnodes") -async def find_all_root_nodes(response: Response, branch: str, taxonomy_name: str): - """ - Get all root nodes within taxonomy - """ - taxonomy = TaxonomyGraph(branch, taxonomy_name) - result = taxonomy.get_all_root_nodes() - all_root_nodes = list(result) - return all_root_nodes - - -@app.get("/{taxonomy_name}/{branch}/entry/{entry}") -async def find_one_entry(response: Response, branch: str, taxonomy_name: str, entry: str): - """ - Get entry corresponding to id within taxonomy - """ - taxonomy = TaxonomyGraph(branch, taxonomy_name) - result = taxonomy.get_nodes("ENTRY", entry) - one_entry = list(result) - - check_single(one_entry) - - return one_entry[0] - - -@app.get("/{taxonomy_name}/{branch}/entry/{entry}/parents") -async def find_one_entry_parents(response: Response, branch: str, taxonomy_name: str, entry: str): - """ - Get parents for a entry corresponding to id within taxonomy - """ - taxonomy = TaxonomyGraph(branch, taxonomy_name) - result = taxonomy.get_parents(entry) - one_entry_parents = list(result) - - return one_entry_parents - - -@app.get("/{taxonomy_name}/{branch}/entry/{entry}/children") -async def find_one_entry_children(response: Response, branch: str, taxonomy_name: str, entry: str): - """ - Get children for a entry corresponding to id within taxonomy - """ - taxonomy = TaxonomyGraph(branch, taxonomy_name) - result = taxonomy.get_children(entry) - one_entry_children = list(result) - - return one_entry_children - - -@app.get("/{taxonomy_name}/{branch}/entry") -async def find_all_entries(response: Response, branch: str, taxonomy_name: str): - """ - Get all entries within taxonomy - """ - taxonomy = TaxonomyGraph(branch, taxonomy_name) - result = taxonomy.get_all_nodes("ENTRY") - all_entries = list(result) - return all_entries - - -@app.get("/{taxonomy_name}/{branch}/synonym/{synonym}") -async def find_one_synonym(response: Response, branch: str, taxonomy_name: str, synonym: str): - """ - Get synonym corresponding to id within taxonomy - """ - taxonomy = TaxonomyGraph(branch, taxonomy_name) - result = taxonomy.get_nodes("SYNONYMS", synonym) - one_synonym = list(result) - - check_single(one_synonym) - - return one_synonym[0] - - -@app.get("/{taxonomy_name}/{branch}/synonym") -async def find_all_synonyms(response: Response, branch: str, taxonomy_name: str): - """ - Get all synonyms within taxonomy - """ - taxonomy = TaxonomyGraph(branch, taxonomy_name) - result = taxonomy.get_all_nodes("SYNONYMS") - all_synonyms = list(result) - return all_synonyms - - -@app.get("/{taxonomy_name}/{branch}/stopword/{stopword}") -async def find_one_stopword(response: Response, branch: str, taxonomy_name: str, stopword: str): - """ - Get stopword corresponding to id within taxonomy - """ - taxonomy = TaxonomyGraph(branch, taxonomy_name) - result = taxonomy.get_nodes("STOPWORDS", stopword) - one_stopword = list(result) - - check_single(one_stopword) - - return one_stopword[0] - - -@app.get("/{taxonomy_name}/{branch}/stopword") -async def find_all_stopwords(response: Response, branch: str, taxonomy_name: str): - """ - Get all stopwords within taxonomy - """ - taxonomy = TaxonomyGraph(branch, taxonomy_name) - result = taxonomy.get_all_nodes("STOPWORDS") - all_stopwords = list(result) - return all_stopwords - - -@app.get("/{taxonomy_name}/{branch}/header") -async def find_header(response: Response, branch: str, taxonomy_name: str): - """ - Get __header__ within taxonomy - """ - taxonomy = TaxonomyGraph(branch, taxonomy_name) - result = taxonomy.get_nodes("TEXT", "__header__") - header = list(result) - return header[0] - - -@app.get("/{taxonomy_name}/{branch}/footer") -async def find_footer(response: Response, branch: str, taxonomy_name: str): - """ - Get __footer__ within taxonomy - """ - taxonomy = TaxonomyGraph(branch, taxonomy_name) - result = taxonomy.get_nodes("TEXT", "__footer__") - footer = list(result) - return footer[0] - - -@app.get("/{taxonomy_name}/{branch}/search") -async def search_node(response: Response, branch: str, taxonomy_name: str, query: str): - taxonomy = TaxonomyGraph(branch, taxonomy_name) - result = taxonomy.full_text_search(query) - return result - - -@app.get("/{taxonomy_name}/{branch}/downloadexport") -async def export_to_text_file( - response: Response, branch: str, taxonomy_name: str, background_tasks: BackgroundTasks -): - taxonomy = TaxonomyGraph(branch, taxonomy_name) - file = taxonomy.file_export() - - # Add a background task for removing exported taxonomy file - background_tasks.add_task(file_cleanup, file) - return FileResponse(file) - - -@app.get("/{taxonomy_name}/{branch}/githubexport") -async def export_to_github( - response: Response, branch: str, taxonomy_name: str, background_tasks: BackgroundTasks -): - taxonomy = TaxonomyGraph(branch, taxonomy_name) - try: - url, file = taxonomy.github_export() - # Add a background task for removing exported taxonomy file - background_tasks.add_task(file_cleanup, file) - return url - - except GithubBranchExistsError: - raise HTTPException(status_code=500, detail="The Github branch already exists!") - - except GithubUploadError: - raise HTTPException(status_code=500, detail="Github upload error!") - - -# Post methods - - -@app.post("/{taxonomy_name}/{branch}/import") -async def import_from_github(request: Request, branch: str, taxonomy_name: str): - """ - Get taxonomy from Product Opener GitHub repository - """ - incoming_data = await request.json() - description = incoming_data["description"] - - taxonomy = TaxonomyGraph(branch, taxonomy_name) - if not taxonomy.is_valid_branch_name(): - raise HTTPException(status_code=500, detail="Enter a valid branch name!") - if taxonomy.does_project_exist(): - raise HTTPException(status_code=500, detail="Project already exists!") - if not taxonomy.is_branch_unique(): - raise HTTPException(status_code=500, detail="Branch name should be unique!") - - result = taxonomy.import_from_github(description) - return result - - -@app.post("/{taxonomy_name}/{branch}/nodes") -async def create_node(request: Request, branch: str, taxonomy_name: str): - """ - Creating a new node in a taxonomy - """ - taxonomy = TaxonomyGraph(branch, taxonomy_name) - incoming_data = await request.json() - id = incoming_data["id"] - main_language = incoming_data["main_language"] - if id is None: - raise HTTPException(status_code=400, detail="Invalid id") - if main_language is None: - raise HTTPException(status_code=400, detail="Invalid main language code") - - normalized_id = taxonomy.create_node(taxonomy.get_label(id), id, main_language) - if taxonomy.get_label(id) == "ENTRY": - taxonomy.add_node_to_end(taxonomy.get_label(normalized_id), normalized_id) - else: - taxonomy.add_node_to_beginning(taxonomy.get_label(normalized_id), normalized_id) - - -@app.post("/{taxonomy_name}/{branch}/entry/{entry}") -async def edit_entry(request: Request, branch: str, taxonomy_name: str, entry: str): - """ - Editing an entry in a taxonomy. - New key-value pairs can be added, old key-value pairs can be updated. - URL will be of format '/entry/' - """ - taxonomy = TaxonomyGraph(branch, taxonomy_name) - incoming_data = await request.json() - result = taxonomy.update_nodes("ENTRY", entry, incoming_data) - updated_entry = list(result) - return updated_entry - - -@app.post("/{taxonomy_name}/{branch}/entry/{entry}/children") -async def edit_entry_children(request: Request, branch: str, taxonomy_name: str, entry: str): - """ - Editing an entry's children in a taxonomy. - New children can be added, old children can be removed. - URL will be of format '/entry//children' - """ - taxonomy = TaxonomyGraph(branch, taxonomy_name) - incoming_data = await request.json() - result = taxonomy.update_node_children(entry, incoming_data) - updated_children = list(result) - return updated_children - - -@app.post("/{taxonomy_name}/{branch}/synonym/{synonym}") -async def edit_synonyms(request: Request, branch: str, taxonomy_name: str, synonym: str): - """ - Editing a synonym in a taxonomy. - New key-value pairs can be added, old key-value pairs can be updated. - URL will be of format '/synonym/' - """ - taxonomy = TaxonomyGraph(branch, taxonomy_name) - incoming_data = await request.json() - result = taxonomy.update_nodes("SYNONYMS", synonym, incoming_data) - updated_synonym = list(result) - return updated_synonym - - -@app.post("/{taxonomy_name}/{branch}/stopword/{stopword}") -async def edit_stopwords(request: Request, branch: str, taxonomy_name: str, stopword: str): - """ - Editing a stopword in a taxonomy. - New key-value pairs can be added, old key-value pairs can be updated. - URL will be of format '/stopword/' - """ - taxonomy = TaxonomyGraph(branch, taxonomy_name) - incoming_data = await request.json() - result = taxonomy.update_nodes("STOPWORDS", stopword, incoming_data) - updated_stopword = list(result) - return updated_stopword - - -@app.post("/{taxonomy_name}/{branch}/header") -async def edit_header(incoming_data: Header, branch: str, taxonomy_name: str): - """ - Editing the __header__ in a taxonomy. - """ - taxonomy = TaxonomyGraph(branch, taxonomy_name) - convertedData = incoming_data.dict() - result = taxonomy.update_nodes("TEXT", "__header__", convertedData) - updated_header = list(result) - return updated_header - - -@app.post("/{taxonomy_name}/{branch}/footer") -async def edit_footer(incoming_data: Footer, branch: str, taxonomy_name: str): - """ - Editing the __footer__ in a taxonomy. - """ - taxonomy = TaxonomyGraph(branch, taxonomy_name) - convertedData = incoming_data.dict() - result = taxonomy.update_nodes("TEXT", "__footer__", convertedData) - updated_footer = list(result) - return updated_footer - - -# Delete methods - - -@app.delete("/{taxonomy_name}/{branch}/nodes") -async def delete_node(request: Request, branch: str, taxonomy_name: str): - """ - Deleting given node from a taxonomy - """ - taxonomy = TaxonomyGraph(branch, taxonomy_name) - incoming_data = await request.json() - id = incoming_data["id"] - taxonomy.delete_node(taxonomy.get_label(id), id) +""" +Taxonomy Editor Backend API +""" +import logging +import os +import shutil +import tempfile + +# Required imports +# ------------------------------------------------------------------------------------# +from datetime import datetime +from enum import Enum +from typing import Optional + +# FastAPI +from fastapi import ( + BackgroundTasks, + FastAPI, + Form, + HTTPException, + Request, + Response, + UploadFile, + status, +) +from fastapi.encoders import jsonable_encoder +from fastapi.exceptions import RequestValidationError +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import FileResponse, JSONResponse + +# DB helper imports +from . import graph_db +from .entries import TaxonomyGraph + +# Custom exceptions +from .exceptions import GithubBranchExistsError, GithubUploadError + +# Data model imports +from .models import Footer, Header + +# -----------------------------------------------------------------------------------# + +# Setup logs +logging.basicConfig( + handlers=[logging.StreamHandler()], + level=logging.INFO, +) + +log = logging.getLogger(__name__) + +app = FastAPI(title="Open Food Facts Taxonomy Editor API") + +# Allow anyone to call the API from their own apps +app.add_middleware( + CORSMiddleware, + # FastAPI doc related to allow_origin (to avoid CORS issues): + # "It's also possible to declare the list as "*" (a "wildcard") to say that all are allowed. + # But that will only allow certain types of communication, excluding everything that involves + # credentials: Cookies, Authorization headers like those used with Bearer Tokens, etc. + # So, for everything to work correctly, it's better to specify explicitly the allowed origins." + # => Workaround: use allow_origin_regex + # Source: https://github.com/tiangolo/fastapi/issues/133#issuecomment-646985050 + allow_origin_regex="https?://.*", + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + expose_headers=["*"], +) + + +@app.on_event("startup") +async def startup(): + """ + Initialize database + """ + graph_db.initialize_db() + + +@app.on_event("shutdown") +async def shutdown(): + """ + Shutdown database + """ + graph_db.shutdown_db() + + +@app.middleware("http") +async def initialize_neo4j_transactions(request: Request, call_next): + async with graph_db.TransactionCtx(): + response = await call_next(request) + return response + + +# Helper methods + + +def check_single(id): + """ + Helper function for checking whether there is only a single entry with given id + """ + if len(id) == 0: + raise HTTPException(status_code=404, detail="Entry not found") + elif len(id) > 1: + raise HTTPException(status_code=500, detail="Multiple entries found") + + +def file_cleanup(filepath): + """ + Helper function to delete a taxonomy file from local storage + """ + try: + os.remove(filepath) + except Exception as e: + raise HTTPException(status_code=500, detail="Taxonomy file not found for deletion") from e + + +class StatusFilter(str, Enum): + """ + Enum for project status filter + """ + + OPEN = "OPEN" + CLOSED = "CLOSED" + + +@app.exception_handler(RequestValidationError) +async def validation_exception_handler(request: Request, exc: RequestValidationError): + reformatted_errors = [] + for pydantic_error in exc.errors(): + # Add custom message for status filter + if pydantic_error["loc"] == ("query", "status"): + pydantic_error[ + "msg" + ] = "Status filter must be one of: OPEN, CLOSED or should be omitted" + reformatted_errors.append(pydantic_error) + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content=jsonable_encoder({"detail": "Invalid request", "errors": reformatted_errors}), + ) + + +# Get methods + + +@app.get("/", status_code=status.HTTP_200_OK) +async def hello(): + return {"message": "Hello user! Tip: open /docs or /redoc for documentation"} + + +@app.get("/ping") +async def pong(response: Response): + """ + Check server health + """ + pong = datetime.now() + return {"ping": "pong @ %s" % pong} + + +@app.get("/projects") +async def list_all_projects(response: Response, status: Optional[StatusFilter] = None): + """ + List projects created in the Taxonomy Editor with a status filter + """ + # Listing all projects doesn't require a taxonomy name or branch name + taxonomy = TaxonomyGraph("", "") + result = await taxonomy.list_projects(status) + return result + + +@app.get("{taxonomy_name}/{branch}/set-project-status") +async def set_project_status( + response: Response, branch: str, taxonomy_name: str, status: Optional[StatusFilter] = None +): + """ + Set the status of a Taxonomy Editor project + """ + taxonomy = TaxonomyGraph(branch, taxonomy_name) + result = await taxonomy.set_project_status(status) + return result + + +@app.get("/{taxonomy_name}/{branch}/nodes") +async def find_all_nodes(response: Response, branch: str, taxonomy_name: str): + """ + Get all nodes within taxonomy + """ + taxonomy = TaxonomyGraph(branch, taxonomy_name) + all_nodes = await taxonomy.get_all_nodes("") + return all_nodes + + +@app.get("/{taxonomy_name}/{branch}/rootentries") +async def find_all_root_nodes(response: Response, branch: str, taxonomy_name: str): + """ + Get all root nodes within taxonomy + """ + taxonomy = TaxonomyGraph(branch, taxonomy_name) + all_root_nodes = await taxonomy.get_all_root_nodes() + return all_root_nodes + + +@app.get("/{taxonomy_name}/{branch}/entry/{entry}") +async def find_one_entry(response: Response, branch: str, taxonomy_name: str, entry: str): + """ + Get entry corresponding to id within taxonomy + """ + taxonomy = TaxonomyGraph(branch, taxonomy_name) + one_entry = await taxonomy.get_nodes("ENTRY", entry) + + check_single(one_entry) + + return one_entry[0] + + +@app.get("/{taxonomy_name}/{branch}/entry/{entry}/parents") +async def find_one_entry_parents(response: Response, branch: str, taxonomy_name: str, entry: str): + """ + Get parents for a entry corresponding to id within taxonomy + """ + taxonomy = TaxonomyGraph(branch, taxonomy_name) + one_entry_parents = await taxonomy.get_parents(entry) + + return one_entry_parents + + +@app.get("/{taxonomy_name}/{branch}/entry/{entry}/children") +async def find_one_entry_children(response: Response, branch: str, taxonomy_name: str, entry: str): + """ + Get children for a entry corresponding to id within taxonomy + """ + taxonomy = TaxonomyGraph(branch, taxonomy_name) + one_entry_children = await taxonomy.get_children(entry) + + return one_entry_children + + +@app.get("/{taxonomy_name}/{branch}/entry") +async def find_all_entries(response: Response, branch: str, taxonomy_name: str): + """ + Get all entries within taxonomy + """ + taxonomy = TaxonomyGraph(branch, taxonomy_name) + all_entries = await taxonomy.get_all_nodes("ENTRY") + return all_entries + + +@app.get("/{taxonomy_name}/{branch}/synonym/{synonym}") +async def find_one_synonym(response: Response, branch: str, taxonomy_name: str, synonym: str): + """ + Get synonym corresponding to id within taxonomy + """ + taxonomy = TaxonomyGraph(branch, taxonomy_name) + one_synonym = await taxonomy.get_nodes("SYNONYMS", synonym) + + check_single(one_synonym) + + return one_synonym[0] + + +@app.get("/{taxonomy_name}/{branch}/synonym") +async def find_all_synonyms(response: Response, branch: str, taxonomy_name: str): + """ + Get all synonyms within taxonomy + """ + taxonomy = TaxonomyGraph(branch, taxonomy_name) + all_synonyms = await taxonomy.get_all_nodes("SYNONYMS") + return all_synonyms + + +@app.get("/{taxonomy_name}/{branch}/stopword/{stopword}") +async def find_one_stopword(response: Response, branch: str, taxonomy_name: str, stopword: str): + """ + Get stopword corresponding to id within taxonomy + """ + taxonomy = TaxonomyGraph(branch, taxonomy_name) + one_stopword = await taxonomy.get_nodes("STOPWORDS", stopword) + + check_single(one_stopword) + + return one_stopword[0] + + +@app.get("/{taxonomy_name}/{branch}/stopword") +async def find_all_stopwords(response: Response, branch: str, taxonomy_name: str): + """ + Get all stopwords within taxonomy + """ + taxonomy = TaxonomyGraph(branch, taxonomy_name) + all_stopwords = await taxonomy.get_all_nodes("STOPWORDS") + return all_stopwords + + +@app.get("/{taxonomy_name}/{branch}/header") +async def find_header(response: Response, branch: str, taxonomy_name: str): + """ + Get __header__ within taxonomy + """ + taxonomy = TaxonomyGraph(branch, taxonomy_name) + header = await taxonomy.get_nodes("TEXT", "__header__") + return header[0] + + +@app.get("/{taxonomy_name}/{branch}/footer") +async def find_footer(response: Response, branch: str, taxonomy_name: str): + """ + Get __footer__ within taxonomy + """ + taxonomy = TaxonomyGraph(branch, taxonomy_name) + footer = await taxonomy.get_nodes("TEXT", "__footer__") + return footer[0] + + +@app.get("/{taxonomy_name}/{branch}/parsing_errors") +async def find_all_errors(request: Request, branch: str, taxonomy_name: str): + """ + Get all errors within taxonomy + """ + taxonomy = TaxonomyGraph(branch, taxonomy_name) + result = await taxonomy.get_parsing_errors() + return result + + +@app.get("/{taxonomy_name}/{branch}/search") +async def search_node(response: Response, branch: str, taxonomy_name: str, query: str): + taxonomy = TaxonomyGraph(branch, taxonomy_name) + result = await taxonomy.full_text_search(query) + return result + + +@app.get("/{taxonomy_name}/{branch}/downloadexport") +async def export_to_text_file( + response: Response, branch: str, taxonomy_name: str, background_tasks: BackgroundTasks +): + taxonomy = TaxonomyGraph(branch, taxonomy_name) + file = await taxonomy.file_export() + + # Add a background task for removing exported taxonomy file + background_tasks.add_task(file_cleanup, file) + return FileResponse(file) + + +@app.get("/{taxonomy_name}/{branch}/githubexport") +async def export_to_github( + response: Response, branch: str, taxonomy_name: str, background_tasks: BackgroundTasks +): + taxonomy = TaxonomyGraph(branch, taxonomy_name) + try: + url, file = await taxonomy.github_export() + # Add a background task for removing exported taxonomy file + background_tasks.add_task(file_cleanup, file) + return url + + except GithubBranchExistsError: + raise HTTPException(status_code=500, detail="The Github branch already exists!") + + except GithubUploadError: + raise HTTPException(status_code=500, detail="Github upload error!") + + +# Post methods + + +@app.post("/{taxonomy_name}/{branch}/import") +async def import_from_github(request: Request, branch: str, taxonomy_name: str): + """ + Get taxonomy from Product Opener GitHub repository + """ + incoming_data = await request.json() + description = incoming_data["description"] + + taxonomy = TaxonomyGraph(branch, taxonomy_name) + if not taxonomy.is_valid_branch_name(): + raise HTTPException(status_code=400, detail="branch_name: Enter a valid branch name!") + if await taxonomy.does_project_exist(): + raise HTTPException(status_code=409, detail="Project already exists!") + if not await taxonomy.is_branch_unique(): + raise HTTPException(status_code=409, detail="branch_name: Branch name should be unique!") + + result = await taxonomy.import_from_github(description) + return result + + +@app.post("/{taxonomy_name}/{branch}/upload") +async def upload_taxonomy( + branch: str, taxonomy_name: str, file: UploadFile, description: str = Form(...) +): + """ + Upload taxonomy file to be parsed + """ + # use the file name as the taxonomy name + taxonomy = TaxonomyGraph(branch, taxonomy_name) + if not taxonomy.is_valid_branch_name(): + raise HTTPException(status_code=400, detail="branch_name: Enter a valid branch name!") + if await taxonomy.does_project_exist(): + raise HTTPException(status_code=409, detail="Project already exists!") + if not await taxonomy.is_branch_unique(): + raise HTTPException(status_code=409, detail="branch_name: Branch name should be unique!") + + with tempfile.TemporaryDirectory(prefix="taxonomy-") as tmpdir: + filepath = f"{tmpdir}/{file.filename}" + with open(filepath, "wb") as f: + shutil.copyfileobj(file.file, f) + result = await taxonomy.upload_taxonomy(filepath, description) + + return result + + +@app.post("/{taxonomy_name}/{branch}/nodes") +async def create_node(request: Request, branch: str, taxonomy_name: str): + """ + Creating a new node in a taxonomy + """ + taxonomy = TaxonomyGraph(branch, taxonomy_name) + incoming_data = await request.json() + id = incoming_data["id"] + main_language = incoming_data["main_language"] + if id is None: + raise HTTPException(status_code=400, detail="Invalid id") + if main_language is None: + raise HTTPException(status_code=400, detail="Invalid main language code") + + label = taxonomy.get_label(id) + normalized_id = await taxonomy.create_node(label, id, main_language) + if label == "ENTRY": + await taxonomy.add_node_to_end(label, normalized_id) + else: + await taxonomy.add_node_to_beginning(label, normalized_id) + + +@app.post("/{taxonomy_name}/{branch}/entry/{entry}") +async def edit_entry(request: Request, branch: str, taxonomy_name: str, entry: str): + """ + Editing an entry in a taxonomy. + New key-value pairs can be added, old key-value pairs can be updated. + URL will be of format '/entry/' + """ + taxonomy = TaxonomyGraph(branch, taxonomy_name) + incoming_data = await request.json() + updated_entry = await taxonomy.update_nodes("ENTRY", entry, incoming_data) + return updated_entry + + +@app.post("/{taxonomy_name}/{branch}/entry/{entry}/children") +async def edit_entry_children(request: Request, branch: str, taxonomy_name: str, entry: str): + """ + Editing an entry's children in a taxonomy. + New children can be added, old children can be removed. + URL will be of format '/entry//children' + """ + taxonomy = TaxonomyGraph(branch, taxonomy_name) + incoming_data = await request.json() + updated_children = await taxonomy.update_node_children(entry, incoming_data) + return updated_children + + +@app.post("/{taxonomy_name}/{branch}/synonym/{synonym}") +async def edit_synonyms(request: Request, branch: str, taxonomy_name: str, synonym: str): + """ + Editing a synonym in a taxonomy. + New key-value pairs can be added, old key-value pairs can be updated. + URL will be of format '/synonym/' + """ + taxonomy = TaxonomyGraph(branch, taxonomy_name) + incoming_data = await request.json() + updated_synonym = await taxonomy.update_nodes("SYNONYMS", synonym, incoming_data) + return updated_synonym + + +@app.post("/{taxonomy_name}/{branch}/stopword/{stopword}") +async def edit_stopwords(request: Request, branch: str, taxonomy_name: str, stopword: str): + """ + Editing a stopword in a taxonomy. + New key-value pairs can be added, old key-value pairs can be updated. + URL will be of format '/stopword/' + """ + taxonomy = TaxonomyGraph(branch, taxonomy_name) + incoming_data = await request.json() + updated_stopword = await taxonomy.update_nodes("STOPWORDS", stopword, incoming_data) + return updated_stopword + + +@app.post("/{taxonomy_name}/{branch}/header") +async def edit_header(incoming_data: Header, branch: str, taxonomy_name: str): + """ + Editing the __header__ in a taxonomy. + """ + taxonomy = TaxonomyGraph(branch, taxonomy_name) + convertedData = incoming_data.dict() + updated_header = await taxonomy.update_nodes("TEXT", "__header__", convertedData) + return updated_header + + +@app.post("/{taxonomy_name}/{branch}/footer") +async def edit_footer(incoming_data: Footer, branch: str, taxonomy_name: str): + """ + Editing the __footer__ in a taxonomy. + """ + taxonomy = TaxonomyGraph(branch, taxonomy_name) + convertedData = incoming_data.dict() + updated_footer = await taxonomy.update_nodes("TEXT", "__footer__", convertedData) + return updated_footer + + +# Delete methods + + +@app.delete("/{taxonomy_name}/{branch}/nodes") +async def delete_node(request: Request, branch: str, taxonomy_name: str): + """ + Deleting given node from a taxonomy + """ + taxonomy = TaxonomyGraph(branch, taxonomy_name) + incoming_data = await request.json() + id = incoming_data["id"] + await taxonomy.delete_node(taxonomy.get_label(id), id) + + +@app.delete("/{taxonomy_name}/{branch}/delete") +async def delete_project(response: Response, branch: str, taxonomy_name: str): + """ + Delete a project + """ + taxonomy = TaxonomyGraph(branch, taxonomy_name) + result_data = await taxonomy.delete_taxonomy_project(branch, taxonomy_name) + return {"message": "Deleted {} projects".format(result_data)} diff --git a/backend/editor/entries.py b/backend/editor/entries.py index 988529a7..f3315118 100644 --- a/backend/editor/entries.py +++ b/backend/editor/entries.py @@ -1,552 +1,626 @@ -""" -Database helper functions for API -""" -import re -import tempfile -import urllib.request # Sending requests - -from openfoodfacts_taxonomy_parser import normalizer # Normalizing tags -from openfoodfacts_taxonomy_parser import parser # Parser for taxonomies -from openfoodfacts_taxonomy_parser import unparser # Unparser for taxonomies - -from .exceptions import GithubBranchExistsError # Custom exceptions -from .exceptions import ( - GithubUploadError, - TaxnonomyImportError, - TaxonomyParsingError, - TaxonomyUnparsingError, -) -from .github_functions import GithubOperations # Github functions -from .graph_db import TransactionCtx # Neo4J transactions context manager -from .graph_db import get_current_session # Neo4J transactions helper -from .graph_db import get_current_transaction - - -class TaxonomyGraph: - - """Class for database operations""" - - def __init__(self, branch_name, taxonomy_name): - self.taxonomy_name = taxonomy_name - self.branch_name = branch_name - self.project_name = "p_" + taxonomy_name + "_" + branch_name - - def get_label(self, id): - """ - Helper function for getting the label for a given id - """ - if id.startswith("stopword"): - return "STOPWORDS" - elif id.startswith("synonym"): - return "SYNONYMS" - elif id.startswith("__header__") or id.startswith("__footer__"): - return "TEXT" - else: - return "ENTRY" - - def create_node(self, label, entry, main_language_code): - """ - Helper function used for creating a node with given id and label - """ - params = {"id": entry} - query = [f"""CREATE (n:{self.project_name}:{label})\n"""] - - # Build all basic keys of a node - if label == "ENTRY": - # Normalizing new canonical tag - language_code, canonical_tag = entry.split(":", 1) - normalised_canonical_tag = normalizer.normalizing(canonical_tag, main_language_code) - - # Reconstructing and updation of node ID - params["id"] = language_code + ":" + normalised_canonical_tag - params["main_language_code"] = main_language_code - - query.append( - """ SET n.main_language = $main_language_code """ - ) # Required for only an entry - else: - canonical_tag = "" - - query.append(""" SET n.id = $id """) - query.append(f""" SET n.tags_{main_language_code} = [$canonical_tag] """) - query.append(""" SET n.preceding_lines = [] """) - query.append(""" RETURN n.id """) - - params["canonical_tag"] = canonical_tag - result = get_current_transaction().run(" ".join(query), params) - return result.data()[0]["n.id"] - - def parse_taxonomy(self, filename): - """ - Helper function to call the Open Food Facts Python Taxonomy Parser - """ - # Close current transaction to use the session variable in parser - get_current_transaction().commit() - - # Create parser object and pass current session to it - parser_object = parser.Parser(get_current_session()) - try: - # Parse taxonomy with given file name and branch name - parser_object(filename, self.branch_name, self.taxonomy_name) - return True - except Exception: - raise TaxonomyParsingError() - - def import_from_github(self, description): - """ - Helper function to import a taxonomy from GitHub - """ - base_url = ( - "https://raw.githubusercontent.com/openfoodfacts/openfoodfacts-server" - "/main/taxonomies/" - ) - filename = self.taxonomy_name + ".txt" - base_url += filename - try: - with tempfile.TemporaryDirectory(prefix="taxonomy-") as tmpdir: - - # File to save the downloaded taxonomy - filepath = f"{tmpdir}/{filename}" - - # Downloads and creates taxonomy file in current working directory - urllib.request.urlretrieve(base_url, filepath) - - status = self.parse_taxonomy(filepath) # Parse the taxonomy - - with TransactionCtx(): - self.create_project(description) # Creates a "project node" in neo4j - - return status - except Exception: - raise TaxnonomyImportError() - - def dump_taxonomy(self): - """ - Helper function to create the txt file of a taxonomy - """ - # Create unparser object and pass current session to it - unparser_object = unparser.WriteTaxonomy(get_current_session()) - # Creates a unique file for dumping the taxonomy - filename = self.project_name + ".txt" - try: - # Parse taxonomy with given file name and branch name - unparser_object(filename, self.branch_name, self.taxonomy_name) - return filename - except Exception: - raise TaxonomyUnparsingError() - - def file_export(self): - """Export a taxonomy for download""" - # Close current transaction to use the session variable in unparser - get_current_transaction().commit() - - filepath = self.dump_taxonomy() - return filepath - - def github_export(self): - """Export a taxonomy to Github""" - # Close current transaction to use the session variable in unparser - get_current_transaction().commit() - - filepath = self.dump_taxonomy() - # Create a new transaction context - with TransactionCtx(): - result = self.export_to_github(filepath) - self.close_project() - return result - - def export_to_github(self, filename): - """ - Helper function to export a taxonomy to GitHub - """ - query = """MATCH (n:PROJECT) WHERE n.id = $project_name RETURN n.description""" - result = get_current_transaction().run(query, {"project_name": self.project_name}) - description = result.data()[0]["n.description"] - - github_object = GithubOperations(self.taxonomy_name, self.branch_name) - try: - github_object.checkout_branch() - except Exception as e: - raise GithubBranchExistsError() from e - try: - github_object.update_file(filename) - pr_object = github_object.create_pr(description) - return (pr_object.html_url, filename) - except Exception as e: - raise GithubUploadError() from e - - def does_project_exist(self): - """ - Helper function to check the existence of a project - """ - query = """MATCH (n:PROJECT) WHERE n.id = $project_name RETURN n""" - result = get_current_transaction().run(query, {"project_name": self.project_name}) - if result.data() == []: - return False - else: - return True - - def is_branch_unique(self): - """ - Helper function to check uniqueness of GitHub branch - """ - query = """MATCH (n:PROJECT) WHERE n.branch_name = $branch_name RETURN n""" - result = get_current_transaction().run(query, {"branch_name": self.branch_name}) - - github_object = GithubOperations(self.taxonomy_name, self.branch_name) - current_branches = github_object.list_all_branches() - - if (result.data() == []) and (self.branch_name not in current_branches): - return True - else: - return False - - def is_valid_branch_name(self): - """ - Helper function to check if a branch name is valid - """ - return normalizer.normalizing(self.branch_name, char="_") == self.branch_name - - def create_project(self, description): - """ - Helper function to create a node with label "PROJECT" - """ - query = """ - CREATE (n:PROJECT) - SET n.id = $project_name - SET n.taxonomy_name = $taxonomy_name - SET n.branch_name = $branch_name - SET n.description = $description - SET n.status = $status - SET n.created_at = datetime() - """ - params = { - "project_name": self.project_name, - "taxonomy_name": self.taxonomy_name, - "branch_name": self.branch_name, - "description": description, - "status": "OPEN", - } - get_current_transaction().run(query, params) - - def close_project(self): - """ - Helper function to close a Taxonomy Editor project and updates project status as "CLOSED" - """ - query = """ - MATCH (n:PROJECT) - WHERE n.id = $project_name - SET n.status = $status - """ - params = {"project_name": self.project_name, "status": "CLOSED"} - get_current_transaction().run(query, params) - - def list_existing_projects(self): - """ - Helper function for listing all existing projects created in Taxonomy Editor - """ - query = """ - MATCH (n:PROJECT) - WHERE n.status = "OPEN" RETURN n - ORDER BY n.created_at - """ - result = get_current_transaction().run(query) - return result - - def add_node_to_end(self, label, entry): - """ - Helper function which adds an existing node to end of taxonomy - """ - # Delete relationship between current last node and __footer__ - query = f""" - MATCH (last_node)-[r:is_before]->(footer:{self.project_name}:TEXT) - WHERE footer.id = "__footer__" DELETE r - RETURN last_node - """ - result = get_current_transaction().run(query) - end_node = result.data()[0]["last_node"] - end_node_label = self.get_label(end_node["id"]) # Get current last node ID - - # Rebuild relationships by inserting incoming node at the end - query = [] - query = f""" - MATCH (new_node:{self.project_name}:{label}) WHERE new_node.id = $id - MATCH (last_node:{self.project_name}:{end_node_label}) WHERE last_node.id = $endnodeid - MATCH (footer:{self.project_name}:TEXT) WHERE footer.id = "__footer__" - CREATE (last_node)-[:is_before]->(new_node) - CREATE (new_node)-[:is_before]->(footer) - """ - result = get_current_transaction().run(query, {"id": entry, "endnodeid": end_node["id"]}) - - def add_node_to_beginning(self, label, entry): - """ - Helper function which adds an existing node to beginning of taxonomy - """ - # Delete relationship between current first node and __header__ - query = f""" - MATCH (header:{self.project_name}:TEXT)-[r:is_before]->(first_node) - WHERE header.id = "__header__" DELETE r - RETURN first_node - """ - result = get_current_transaction().run(query) - start_node = result.data()[0]["first_node"] - start_node_label = self.get_label(start_node["id"]) # Get current first node ID - - # Rebuild relationships by inserting incoming node at the beginning - query = f""" - MATCH (new_node:{self.project_name}:{label}) WHERE new_node.id = $id - MATCH (first_node:{self.project_name}:{start_node_label}) - WHERE first_node.id = $startnodeid - MATCH (header:{self.project_name}:TEXT) WHERE header.id = "__header__" - CREATE (new_node)-[:is_before]->(first_node) - CREATE (header)-[:is_before]->(new_node) - """ - result = get_current_transaction().run( - query, {"id": entry, "startnodeid": start_node["id"]} - ) - - def delete_node(self, label, entry): - """ - Helper function used for deleting a node with given id and label - """ - # Finding node to be deleted using node ID - query = f""" - // Find node to be deleted using node ID - MATCH (deleted_node:{self.project_name}:{label})-[:is_before]->(next_node) - WHERE deleted_node.id = $id - MATCH (previous_node)-[:is_before]->(deleted_node) - // Remove node - DETACH DELETE (deleted_node) - // Rebuild relationships after deletion - CREATE (previous_node)-[:is_before]->(next_node) - """ - result = get_current_transaction().run(query, {"id": entry}) - return result - - def get_all_nodes(self, label): - """ - Helper function used for getting all nodes with/without given label - """ - qualifier = f":{label}" if label else "" - query = f""" - MATCH (n:{self.project_name}{qualifier}) RETURN n - """ - result = get_current_transaction().run(query) - return result - - def get_all_root_nodes(self): - """ - Helper function used for getting all root nodes in a taxonomy - """ - query = f""" - MATCH (n:{self.project_name}) WHERE NOT (n)-[:is_child_of]->() RETURN n - """ - result = get_current_transaction().run(query) - return result - - def get_nodes(self, label, entry): - """ - Helper function used for getting the node with given id and label - """ - query = f""" - MATCH (n:{self.project_name}:{label}) WHERE n.id = $id - RETURN n - """ - result = get_current_transaction().run(query, {"id": entry}) - return result - - def get_parents(self, entry): - """ - Helper function used for getting node parents with given id - """ - query = f""" - MATCH (child_node:{self.project_name}:ENTRY)-[r:is_child_of]->(parent) - WHERE child_node.id = $id - RETURN parent.id - """ - result = get_current_transaction().run(query, {"id": entry}) - return result - - def get_children(self, entry): - """ - Helper function used for getting node children with given id - """ - query = f""" - MATCH (child)-[r:is_child_of]->(parent_node:{self.project_name}:ENTRY) - WHERE parent_node.id = $id - RETURN child.id - """ - result = get_current_transaction().run(query, {"id": entry}) - return result - - def update_nodes(self, label, entry, new_node_keys): - """ - Helper function used for updation of node with given id and label - """ - # Sanity check keys - for key in new_node_keys.keys(): - if not re.match(r"^\w+$", key) or key == "id": - raise ValueError("Invalid key: %s", key) - - # Get current node information and deleted keys - curr_node = self.get_nodes(label, entry).data()[0]["n"] - curr_node_keys = list(curr_node.keys()) - deleted_keys = set(curr_node_keys) ^ set(new_node_keys) - - # Check for keys having null/empty values - for key in curr_node_keys: - if (curr_node[key] == []) or (curr_node[key] is None): - deleted_keys.add(key) - - # Build query - query = [f"""MATCH (n:{self.project_name}:{label}) WHERE n.id = $id """] - - # Delete keys removed by user - for key in deleted_keys: - if key == "id": # Doesn't require to be deleted - continue - query.append(f"""\nREMOVE n.{key}\n""") - - # Adding normalized tags ids corresponding to entry tags - normalised_new_node_keys = {} - for keys in new_node_keys.keys(): - if keys.startswith("tags_") and not keys.endswith("_str"): - if "_ids_" not in keys: - keys_language_code = keys.split("_", 1)[1] - normalised_value = [] - for values in new_node_keys[keys]: - normalised_value.append(normalizer.normalizing(values, keys_language_code)) - normalised_new_node_keys[keys] = normalised_value - normalised_new_node_keys["tags_ids_" + keys_language_code] = normalised_value - else: - pass # We generate tags_ids, and ignore the one sent - else: - # No need to normalise - normalised_new_node_keys[keys] = new_node_keys[keys] - - # Update keys - for key in normalised_new_node_keys.keys(): - query.append(f"""\nSET n.{key} = ${key}\n""") - - query.append("""RETURN n""") - - params = dict(normalised_new_node_keys, id=entry) - result = get_current_transaction().run(" ".join(query), params) - return result - - def update_node_children(self, entry, new_children_ids): - """ - Helper function used for updation of node children with given id - """ - # Parse node ids from Neo4j Record object - current_children = [record["child.id"] for record in list(self.get_children(entry))] - deleted_children = set(current_children) - set(new_children_ids) - added_children = set(new_children_ids) - set(current_children) - - # Delete relationships - for child in deleted_children: - query = f""" - MATCH - (deleted_child:{self.project_name}:ENTRY) - -[rel:is_child_of]-> - (parent:{self.project_name}:ENTRY) - WHERE parent.id = $id AND deleted_child.id = $child - DELETE rel - """ - get_current_transaction().run(query, {"id": entry, "child": child}) - - # Create non-existing nodes - query = f""" - MATCH (child:{self.project_name}:ENTRY) - WHERE child.id in $ids RETURN child.id - """ - existing_ids = [ - record["child.id"] - for record in get_current_transaction().run(query, ids=list(added_children)) - ] - to_create = added_children - set(existing_ids) - - # Normalising new children node ID - created_child_ids = [] - - for child in to_create: - main_language_code = child.split(":", 1)[0] - created_node_id = self.create_node("ENTRY", child, main_language_code) - created_child_ids.append(created_node_id) - - # TODO: We would prefer to add the node just after its parent entry - self.add_node_to_end("ENTRY", created_node_id) - - # Stores result of last query executed - result = [] - for child in created_child_ids: - # Create new relationships if it doesn't exist - query = f""" - MATCH (parent:{self.project_name}:ENTRY), (new_child:{self.project_name}:ENTRY) - WHERE parent.id = $id AND new_child.id = $child - MERGE (new_child)-[r:is_child_of]->(parent) - """ - result = get_current_transaction().run(query, {"id": entry, "child": child}) - - return result - - def full_text_search(self, text): - """ - Helper function used for searching a taxonomy - """ - # Escape special characters - normalized_text = re.sub(r"[^A-Za-z0-9_]", r" ", text) - normalized_id_text = normalizer.normalizing(text) - - # If normalized text is empty, no searches are found - if normalized_text.strip() == "": - return [] - - id_index = self.project_name + "_SearchIds" - tags_index = self.project_name + "_SearchTags" - - text_query_exact = "*" + normalized_text + "*" - text_query_fuzzy = normalized_text + "~" - text_id_query_fuzzy = normalized_id_text + "~" - text_id_query_exact = "*" + normalized_id_text + "*" - params = { - "id_index": id_index, - "tags_index": tags_index, - "text_query_fuzzy": text_query_fuzzy, - "text_query_exact": text_query_exact, - "text_id_query_fuzzy": text_id_query_fuzzy, - "text_id_query_exact": text_id_query_exact, - } - - # Fuzzy search and wildcard (*) search on two indexes - # Fuzzy search has more priority, since it matches more close strings - # IDs are given slightly lower priority than tags in fuzzy search - query = """ - CALL { - CALL db.index.fulltext.queryNodes($id_index, $text_id_query_fuzzy) - yield node, score as score_ - where score_ > 0 - return node, score_ * 3 as score - UNION - CALL db.index.fulltext.queryNodes($tags_index, $text_query_fuzzy) - yield node, score as score_ - where score_ > 0 - return node, score_ * 5 as score - UNION - CALL db.index.fulltext.queryNodes($id_index, $text_id_query_exact) - yield node, score as score_ - where score_ > 0 - return node, score_ as score - UNION - CALL db.index.fulltext.queryNodes($tags_index, $text_query_exact) - yield node, score as score_ - where score_ > 0 - return node, score_ as score - } - with node.id as node, score - RETURN node, sum(score) as score - - ORDER BY score DESC - """ - result = [record["node"] for record in get_current_transaction().run(query, params)] - return result +""" +Database helper functions for API +""" +import re +import tempfile +import urllib.request # Sending requests + +from openfoodfacts_taxonomy_parser import normalizer # Normalizing tags +from openfoodfacts_taxonomy_parser import parser # Parser for taxonomies +from openfoodfacts_taxonomy_parser import unparser # Unparser for taxonomies + +from .exceptions import GithubBranchExistsError # Custom exceptions +from .exceptions import ( + GithubUploadError, + TaxnonomyImportError, + TaxonomyParsingError, + TaxonomyUnparsingError, +) +from .github_functions import GithubOperations # Github functions +from .graph_db import ( # Neo4J transactions context managers + SyncTransactionCtx, + TransactionCtx, + get_current_transaction, +) + + +async def async_list(async_iterable): + return [i async for i in async_iterable] + + +class TaxonomyGraph: + + """Class for database operations""" + + def __init__(self, branch_name, taxonomy_name): + self.taxonomy_name = taxonomy_name + self.branch_name = branch_name + self.project_name = "p_" + taxonomy_name + "_" + branch_name + + def get_label(self, id): + """ + Helper function for getting the label for a given id + """ + if id.startswith("stopword"): + return "STOPWORDS" + elif id.startswith("synonym"): + return "SYNONYMS" + elif id.startswith("__header__") or id.startswith("__footer__"): + return "TEXT" + else: + return "ENTRY" + + async def create_node(self, label, entry, main_language_code): + """ + Helper function used for creating a node with given id and label + """ + params = {"id": entry} + query = [f"""CREATE (n:{self.project_name}:{label})\n"""] + + # Build all basic keys of a node + if label == "ENTRY": + # Normalizing new canonical tag + language_code, canonical_tag = entry.split(":", 1) + normalised_canonical_tag = normalizer.normalizing(canonical_tag, main_language_code) + + # Reconstructing and updation of node ID + params["id"] = language_code + ":" + normalised_canonical_tag + params["main_language_code"] = main_language_code + + query.append( + """ SET n.main_language = $main_language_code """ + ) # Required for only an entry + else: + canonical_tag = "" + + query.append(""" SET n.id = $id """) + query.append(f""" SET n.tags_{main_language_code} = [$canonical_tag] """) + query.append(""" SET n.preceding_lines = [] """) + query.append(""" RETURN n.id """) + + params["canonical_tag"] = canonical_tag + result = await get_current_transaction().run(" ".join(query), params) + return (await result.data())[0]["n.id"] + + async def parse_taxonomy(self, filename): + """ + Helper function to call the Open Food Facts Python Taxonomy Parser + """ + # Close current transaction to use the session variable in parser + await get_current_transaction().commit() + + with SyncTransactionCtx() as session: + # Create parser object and pass current session to it + parser_object = parser.Parser(session) + try: + # Parse taxonomy with given file name and branch name + parser_object(filename, self.branch_name, self.taxonomy_name) + return True + except Exception as e: + raise TaxonomyParsingError() from e + + async def import_from_github(self, description): + """ + Helper function to import a taxonomy from GitHub + """ + base_url = ( + "https://raw.githubusercontent.com/openfoodfacts/openfoodfacts-server" + "/main/taxonomies/" + ) + filename = self.taxonomy_name + ".txt" + base_url += filename + try: + with tempfile.TemporaryDirectory(prefix="taxonomy-") as tmpdir: + + # File to save the downloaded taxonomy + filepath = f"{tmpdir}/{filename}" + + # Downloads and creates taxonomy file in current working directory + urllib.request.urlretrieve(base_url, filepath) + + status = await self.parse_taxonomy(filepath) # Parse the taxonomy + + async with TransactionCtx(): + await self.create_project(description) # Creates a "project node" in neo4j + + return status + except Exception as e: + raise TaxnonomyImportError() from e + + async def upload_taxonomy(self, filepath, description): + """ + Helper function to upload a taxonomy file and create a project node + """ + try: + status = await self.parse_taxonomy(filepath) + async with TransactionCtx(): + await self.create_project(description) + return status + except Exception as e: + raise TaxnonomyImportError() from e + + def dump_taxonomy(self): + """ + Helper function to create the txt file of a taxonomy + """ + # Create unparser object and pass a sync session to it + with SyncTransactionCtx() as session: + unparser_object = unparser.WriteTaxonomy(session) + # Creates a unique file for dumping the taxonomy + filename = self.project_name + ".txt" + try: + # Parse taxonomy with given file name and branch name + unparser_object(filename, self.branch_name, self.taxonomy_name) + return filename + except Exception as e: + raise TaxonomyUnparsingError() from e + + async def file_export(self): + """Export a taxonomy for download""" + # Close current transaction to use the session variable in unparser + await get_current_transaction().commit() + + filepath = self.dump_taxonomy() + return filepath + + async def github_export(self): + """Export a taxonomy to Github""" + # Close current transaction to use the session variable in unparser + await get_current_transaction().commit() + + filepath = self.dump_taxonomy() + # Create a new transaction context + async with TransactionCtx(): + result = await self.export_to_github(filepath) + await self.set_project_status(status="CLOSED") + return result + + async def export_to_github(self, filename): + """ + Helper function to export a taxonomy to GitHub + """ + query = """MATCH (n:PROJECT) WHERE n.id = $project_name RETURN n.description""" + result = await get_current_transaction().run(query, {"project_name": self.project_name}) + description = (await result.data())[0]["n.description"] + + github_object = GithubOperations(self.taxonomy_name, self.branch_name) + try: + github_object.checkout_branch() + except Exception as e: + raise GithubBranchExistsError() from e + try: + github_object.update_file(filename) + pr_object = github_object.create_pr(description) + return (pr_object.html_url, filename) + except Exception as e: + raise GithubUploadError() from e + + async def does_project_exist(self): + """ + Helper function to check the existence of a project + """ + query = """MATCH (n:PROJECT) WHERE n.id = $project_name RETURN n""" + result = await get_current_transaction().run(query, {"project_name": self.project_name}) + if (await result.value()) == []: + return False + else: + return True + + async def is_branch_unique(self): + """ + Helper function to check uniqueness of GitHub branch + """ + query = """MATCH (n:PROJECT) WHERE n.branch_name = $branch_name RETURN n""" + result = await get_current_transaction().run(query, {"branch_name": self.branch_name}) + + github_object = GithubOperations(self.taxonomy_name, self.branch_name) + current_branches = github_object.list_all_branches() + + if (await (result.value()) == []) and (self.branch_name not in current_branches): + return True + else: + return False + + def is_valid_branch_name(self): + """ + Helper function to check if a branch name is valid + """ + return normalizer.normalizing(self.branch_name, char="_") == self.branch_name + + async def create_project(self, description): + """ + Helper function to create a node with label "PROJECT" + """ + query = """ + CREATE (n:PROJECT) + SET n.id = $project_name + SET n.taxonomy_name = $taxonomy_name + SET n.branch_name = $branch_name + SET n.description = $description + SET n.status = $status + SET n.created_at = datetime() + """ + params = { + "project_name": self.project_name, + "taxonomy_name": self.taxonomy_name, + "branch_name": self.branch_name, + "description": description, + "status": "OPEN", + } + await get_current_transaction().run(query, params) + + async def set_project_status(self, status): + """ + Helper function to update a Taxonomy Editor project status + """ + query = """ + MATCH (n:PROJECT) + WHERE n.id = $project_name + SET n.status = $status + """ + params = {"project_name": self.project_name, "status": status} + await get_current_transaction().run(query, params) + + async def list_projects(self, status=None): + """ + Helper function for listing all existing projects created in Taxonomy Editor + includes number of nodes with label ERROR for each project + """ + query = [ + "MATCH (n:PROJECT)", + "OPTIONAL MATCH (error_node:ERRORS {branch_name: n.branch_name, id: n.id})", + ] + + params = {} + if status is not None: + # List only projects matching status + query.append("WHERE n.status = $status") + params["status"] = status + + query.extend( + [ + "WITH n, size(error_node.errors) AS errors_count", + "RETURN n{.*, errors_count: errors_count}", + "ORDER BY n.created_at", + ] + ) + + query_result = await get_current_transaction().run("\n".join(query), params) + + return [item async for result_list in query_result for item in result_list] + + async def add_node_to_end(self, label, entry): + """ + Helper function which adds an existing node to end of taxonomy + """ + # Delete relationship between current last node and __footer__ + query = f""" + MATCH (last_node)-[r:is_before]->(footer:{self.project_name}:TEXT) + WHERE footer.id = "__footer__" DELETE r + RETURN last_node + """ + result = await get_current_transaction().run(query) + end_node = (await result.data())[0]["last_node"] + end_node_label = self.get_label(end_node["id"]) # Get current last node ID + + # Rebuild relationships by inserting incoming node at the end + query = [] + query = f""" + MATCH (new_node:{self.project_name}:{label}) WHERE new_node.id = $id + MATCH (last_node:{self.project_name}:{end_node_label}) WHERE last_node.id = $endnodeid + MATCH (footer:{self.project_name}:TEXT) WHERE footer.id = "__footer__" + CREATE (last_node)-[:is_before]->(new_node) + CREATE (new_node)-[:is_before]->(footer) + """ + await get_current_transaction().run(query, {"id": entry, "endnodeid": end_node["id"]}) + + async def add_node_to_beginning(self, label, entry): + """ + Helper function which adds an existing node to beginning of taxonomy + """ + # Delete relationship between current first node and __header__ + query = f""" + MATCH (header:{self.project_name}:TEXT)-[r:is_before]->(first_node) + WHERE header.id = "__header__" DELETE r + RETURN first_node + """ + result = await get_current_transaction().run(query) + start_node = await (result.data())[0]["first_node"] + start_node_label = self.get_label(start_node["id"]) # Get current first node ID + + # Rebuild relationships by inserting incoming node at the beginning + query = f""" + MATCH (new_node:{self.project_name}:{label}) WHERE new_node.id = $id + MATCH (first_node:{self.project_name}:{start_node_label}) + WHERE first_node.id = $startnodeid + MATCH (header:{self.project_name}:TEXT) WHERE header.id = "__header__" + CREATE (new_node)-[:is_before]->(first_node) + CREATE (header)-[:is_before]->(new_node) + """ + await get_current_transaction().run(query, {"id": entry, "startnodeid": start_node["id"]}) + + async def delete_node(self, label, entry): + """ + Helper function used for deleting a node with given id and label + """ + # Finding node to be deleted using node ID + query = f""" + // Find node to be deleted using node ID + MATCH (deleted_node:{self.project_name}:{label})-[:is_before]->(next_node) + WHERE deleted_node.id = $id + MATCH (previous_node)-[:is_before]->(deleted_node) + // Remove node + DETACH DELETE (deleted_node) + // Rebuild relationships after deletion + CREATE (previous_node)-[:is_before]->(next_node) + """ + result = await get_current_transaction().run(query, {"id": entry}) + return await async_list(result) + + async def get_all_nodes(self, label): + """ + Helper function used for getting all nodes with/without given label + """ + qualifier = f":{label}" if label else "" + query = f""" + MATCH (n:{self.project_name}{qualifier}) RETURN n + """ + result = await get_current_transaction().run(query) + return await async_list(result) + + async def get_all_root_nodes(self): + """ + Helper function used for getting all root nodes in a taxonomy + """ + query = f""" + MATCH (n:{self.project_name}:ENTRY) + WHERE NOT (n)-[:is_child_of]->() + RETURN n + """ + result = await get_current_transaction().run(query) + return await async_list(result) + + async def get_parsing_errors(self): + """ + Helper function used for getting parsing errors in the current project + """ + # During parsing of a taxonomy, all the parsing errors + # are stored in a separate node with the label "ERRORS" + # This function returns all the parsing errors + query = f""" + MATCH ( + error_node:ERRORS + {{branch_name: "{self.branch_name}", id: "{self.project_name}"}} + ) + RETURN error_node + """ + result = await get_current_transaction().run(query) + error_node = (await result.data())[0]["error_node"] + return error_node + + async def get_nodes(self, label, entry): + """ + Helper function used for getting the node with given id and label + """ + query = f""" + MATCH (n:{self.project_name}:{label}) WHERE n.id = $id + RETURN n + """ + result = await get_current_transaction().run(query, {"id": entry}) + return await async_list(result) + + async def get_parents(self, entry): + """ + Helper function used for getting node parents with given id + """ + query = f""" + MATCH (child_node:{self.project_name}:ENTRY)-[r:is_child_of]->(parent) + WHERE child_node.id = $id + RETURN parent.id + """ + query_result = await get_current_transaction().run(query, {"id": entry}) + return [item async for result_list in query_result for item in result_list] + + async def get_children(self, entry): + """ + Helper function used for getting node children with given id + """ + query = f""" + MATCH (child)-[r:is_child_of]->(parent_node:{self.project_name}:ENTRY) + WHERE parent_node.id = $id + RETURN child.id + """ + result = await get_current_transaction().run(query, {"id": entry}) + return await async_list(result) + + async def update_nodes(self, label, entry, new_node_keys): + """ + Helper function used for updation of node with given id and label + """ + # Sanity check keys + for key in new_node_keys.keys(): + if not re.match(r"^\w+$", key) or key == "id": + raise ValueError("Invalid key: %s", key) + + # Get current node information and deleted keys + result = await self.get_nodes(label, entry) + curr_node = result[0]["n"] + curr_node_keys = list(curr_node.keys()) + deleted_keys = set(curr_node_keys) ^ set(new_node_keys) + + # Check for keys having null/empty values + for key in curr_node_keys: + if (curr_node[key] == []) or (curr_node[key] is None): + deleted_keys.add(key) + + # Build query + query = [f"""MATCH (n:{self.project_name}:{label}) WHERE n.id = $id """] + + # Delete keys removed by user + for key in deleted_keys: + if key == "id": # Doesn't require to be deleted + continue + query.append(f"""\nREMOVE n.{key}\n""") + + # Adding normalized tags ids corresponding to entry tags + normalised_new_node_keys = {} + for keys in new_node_keys.keys(): + if keys.startswith("tags_"): + if "_ids_" not in keys: + keys_language_code = keys.split("_", 1)[1] + normalised_value = [] + for values in new_node_keys[keys]: + normalised_value.append(normalizer.normalizing(values, keys_language_code)) + normalised_new_node_keys[keys] = normalised_value + normalised_new_node_keys["tags_ids_" + keys_language_code] = normalised_value + else: + pass # We generate tags_ids, and ignore the one sent + else: + # No need to normalise + normalised_new_node_keys[keys] = new_node_keys[keys] + + # Update keys + for key in normalised_new_node_keys.keys(): + query.append(f"""\nSET n.{key} = ${key}\n""") + + query.append("""RETURN n""") + + params = dict(normalised_new_node_keys, id=entry) + result = await get_current_transaction().run(" ".join(query), params) + return await async_list(result) + + async def update_node_children(self, entry, new_children_ids): + """ + Helper function used for updation of node children with given id + """ + # Parse node ids from Neo4j Record object + current_children = [record["child.id"] for record in list(await self.get_children(entry))] + deleted_children = set(current_children) - set(new_children_ids) + added_children = set(new_children_ids) - set(current_children) + + # Delete relationships + for child in deleted_children: + query = f""" + MATCH + (deleted_child:{self.project_name}:ENTRY) + -[rel:is_child_of]-> + (parent:{self.project_name}:ENTRY) + WHERE parent.id = $id AND deleted_child.id = $child + DELETE rel + """ + await get_current_transaction().run(query, {"id": entry, "child": child}) + + # Create non-existing nodes + query = f""" + MATCH (child:{self.project_name}:ENTRY) + WHERE child.id in $ids RETURN child.id + """ + _result = await get_current_transaction().run(query, ids=list(added_children)) + existing_ids = [record["child.id"] for record in (await _result.data())] + to_create = added_children - set(existing_ids) + + # Normalising new children node ID + created_child_ids = [] + + for child in to_create: + main_language_code = child.split(":", 1)[0] + created_node_id = await self.create_node("ENTRY", child, main_language_code) + created_child_ids.append(created_node_id) + + # TODO: We would prefer to add the node just after its parent entry + await self.add_node_to_end("ENTRY", created_node_id) + + # Stores result of last query executed + result = [] + children_ids = created_child_ids + existing_ids + for child_id in children_ids: + # Create new relationships if it doesn't exist + query = f""" + MATCH (parent:{self.project_name}:ENTRY), (new_child:{self.project_name}:ENTRY) + WHERE parent.id = $id AND new_child.id = $child_id + MERGE (new_child)-[r:is_child_of]->(parent) + """ + _result = await get_current_transaction().run( + query, {"id": entry, "child_id": child_id} + ) + result = list(await _result.value()) + + return result + + async def full_text_search(self, text): + """ + Helper function used for searching a taxonomy + """ + # Escape special characters + normalized_text = re.sub(r"[^A-Za-z0-9_]", r" ", text) + normalized_id_text = normalizer.normalizing(text) + + # If normalized text is empty, no searches are found + if normalized_text.strip() == "": + return [] + + id_index = self.project_name + "_SearchIds" + tags_index = self.project_name + "_SearchTags" + + text_query_exact = "*" + normalized_text + "*" + text_query_fuzzy = normalized_text + "~" + text_id_query_fuzzy = normalized_id_text + "~" + text_id_query_exact = "*" + normalized_id_text + "*" + params = { + "id_index": id_index, + "tags_index": tags_index, + "text_query_fuzzy": text_query_fuzzy, + "text_query_exact": text_query_exact, + "text_id_query_fuzzy": text_id_query_fuzzy, + "text_id_query_exact": text_id_query_exact, + } + + # Fuzzy search and wildcard (*) search on two indexes + # Fuzzy search has more priority, since it matches more close strings + # IDs are given slightly lower priority than tags in fuzzy search + query = """ + CALL { + CALL db.index.fulltext.queryNodes($id_index, $text_id_query_fuzzy) + yield node, score as score_ + where score_ > 0 + return node, score_ * 3 as score + UNION + CALL db.index.fulltext.queryNodes($tags_index, $text_query_fuzzy) + yield node, score as score_ + where score_ > 0 + return node, score_ * 5 as score + UNION + CALL db.index.fulltext.queryNodes($id_index, $text_id_query_exact) + yield node, score as score_ + where score_ > 0 + return node, score_ as score + UNION + CALL db.index.fulltext.queryNodes($tags_index, $text_query_exact) + yield node, score as score_ + where score_ > 0 + return node, score_ as score + } + with node.id as node, score + RETURN node, sum(score) as score + + ORDER BY score DESC + """ + _result = await get_current_transaction().run(query, params) + result = [record["node"] for record in await _result.data()] + return result + + async def delete_taxonomy_project(self, branch, taxonomy_name): + """ + Delete taxonomy projects + """ + + delete_query = """ + MATCH (n:PROJECT {taxonomy_name: $taxonomy_name, branch: $branch}) + DELETE n + """ + result = await get_current_transaction().run( + delete_query, taxonomy_name=taxonomy_name, branch=branch + ) + summary = await result.consume() + count = summary.counters.nodes_deleted + return count diff --git a/backend/editor/graph_db.py b/backend/editor/graph_db.py index bfb7cf75..af6c9f40 100644 --- a/backend/editor/graph_db.py +++ b/backend/editor/graph_db.py @@ -3,13 +3,17 @@ """ import contextlib import contextvars # Used for creation of context vars +import logging -from neo4j import GraphDatabase # Interface with Neo4J +import neo4j # Interface with Neo4J from . import settings # Neo4J settings from .exceptions import SessionMissingError # Custom exceptions from .exceptions import TransactionMissingError +log = logging.getLogger(__name__) + + txn = contextvars.ContextVar("txn") txn.set(None) @@ -17,20 +21,23 @@ session.set(None) -@contextlib.contextmanager -def TransactionCtx(): +@contextlib.asynccontextmanager +async def TransactionCtx(): """ Transaction context will set global transaction "txn" for the code in context. Transactions are automatically rollback if an exception occurs within the context. """ global txn, session - with driver.session() as _session: - with _session.begin_transaction() as _txn: - txn.set(_txn) - session.set(_session) - yield txn, session - txn.set(None) - session.set(None) + try: + async with driver.session() as _session: + txn_manager = await _session.begin_transaction() + async with txn_manager as _txn: + txn.set(_txn) + session.set(_session) + yield txn, session + finally: + txn.set(None) + session.set(None) def initialize_db(): @@ -39,7 +46,7 @@ def initialize_db(): """ global driver uri = settings.uri - driver = GraphDatabase.driver(uri) + driver = neo4j.AsyncGraphDatabase.driver(uri) def shutdown_db(): @@ -67,3 +74,17 @@ def get_current_session(): if curr_session is None: raise SessionMissingError() return curr_session + + +@contextlib.contextmanager +def SyncTransactionCtx(): + """ + Get a non async session + + BEWARE: use it with caution only for edge cases + Normally it should be reserved to background tasks + """ + uri = settings.uri + driver = neo4j.GraphDatabase.driver(uri) + with driver.session() as _session: + yield _session diff --git a/backend/editor/settings.py b/backend/editor/settings.py index 7f5e34b2..c6067462 100644 --- a/backend/editor/settings.py +++ b/backend/editor/settings.py @@ -4,5 +4,5 @@ import os uri = os.environ.get("NEO4J_URI", "bolt://localhost:7687") -access_token = os.environ.get("GITHUB_PAT", "") +access_token = os.environ.get("GITHUB_PAT") repo_uri = os.environ.get("REPO_URI", "openfoodfacts/openfoodfacts-server") diff --git a/backend/requirements.txt b/backend/requirements.txt index cd27575e..bc0c2904 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,20 +1,23 @@ anyio==3.6.1 click==8.1.3 -fastapi==0.79.0 +fastapi==0.89.1 h11==0.13.0 httptools==0.4.0 idna==3.3 -neo4j==4.4.5 +neo4j==5.4.0 pydantic==1.9.1 python-dotenv==0.20.0 pytz==2022.1 PyYAML==6.0 sniffio==1.2.0 -starlette==0.19.1 +starlette==0.22.0 typing-extensions==4.3.0 uvicorn==0.18.2 uvloop==0.16.0 -watchfiles==0.16.0 +watchfiles==0.18.1 websockets==10.3 PyGithub==1.56 +python-multipart==0.0.5 +httpx==0.23.3 +pytest==7.1.2 -e ../parser/ \ No newline at end of file diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py new file mode 100644 index 00000000..7addc9e4 --- /dev/null +++ b/backend/tests/conftest.py @@ -0,0 +1,32 @@ +import os +import time + +import pytest +from fastapi.testclient import TestClient +from neo4j import GraphDatabase +from neo4j.exceptions import ServiceUnavailable + +from editor.api import app + + +@pytest.fixture +def client(): + with TestClient(app) as client: + yield client + + +@pytest.fixture +def neo4j(): + """waiting for neo4j to be ready""" + uri = os.environ.get("NEO4J_URI", "bolt://localhost:7687") + driver = GraphDatabase.driver(uri) + session = driver.session() + connected = False + while not connected: + try: + session.run("MATCH () return 1 limit 1") + except ServiceUnavailable: + time.sleep(1) + else: + connected = True + return driver diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py new file mode 100644 index 00000000..fd7dfd5e --- /dev/null +++ b/backend/tests/test_api.py @@ -0,0 +1,54 @@ +import pytest + + +@pytest.fixture(autouse=True) +def test_setup(neo4j): + # delete all the nodes and relations in the database + query = "MATCH (n) DETACH DELETE n" + neo4j.session().run(query) + query = "DROP INDEX p_test_branch_SearchIds IF EXISTS" + neo4j.session().run(query) + query = "DROP INDEX p_test_branch_SearchTags IF EXISTS" + neo4j.session().run(query) + + +def test_hello(client): + response = client.get("/") + assert response.status_code == 200 + assert response.json() == {"message": "Hello user! Tip: open /docs or /redoc for documentation"} + + +def test_delete_project(neo4j, client): + session = neo4j.session() + create_project = """ + CREATE (n:PROJECT) + SET n.id = 'test_project' + SET n.taxonomy_name = 'test_taxonomy_name' + SET n.branch = 'test_branch' + SET n.description = 'test_description' + SET n.status = 'OPEN' + SET n.project_name = 'p_test_taxonomy_name_test_branch_name' + SET n.created_at = datetime() + """ + + create_project2 = """ + CREATE (n:PROJECT) + SET n.id = 'test_project2' + SET n.taxonomy_name = 'test_taxonomy_name2' + SET n.branch = 'test_branch2' + SET n.description = 'test_description2' + SET n.status = 'OPEN' + SET n.project_name = 'p_test_taxonomy_name_test_branch_name2' + SET n.created_at = datetime() + """ + session.run(create_project) + session.run(create_project2) + + response = client.delete("/test_taxonomy_name/test_branch/delete") + assert response.status_code == 200 + assert response.json() == {"message": "Deleted 1 projects"} + + response = client.delete("/test_taxonomy_name2/test_branch2/delete") + assert response.status_code == 200 + assert response.json() == {"message": "Deleted 1 projects"} + session.close() diff --git a/conf/nginx.conf b/conf/nginx.conf index 3b307bbd..76be053e 100644 --- a/conf/nginx.conf +++ b/conf/nginx.conf @@ -68,6 +68,12 @@ server { index index.html; + location / { + # an unknown path is a path handled by react + try_files $uri $uri/ /index.html; + } + + } @@ -90,7 +96,7 @@ server { proxy_set_header Host $host${PUBLIC_TAXONOMY_EDITOR_PORT}; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Proto ${API_SCHEME}; set $taxonomy_api taxonomy_api; proxy_pass http://$taxonomy_api; } diff --git a/doc/introduction/setup-dev.md b/doc/introduction/setup-dev.md index 4a6f6ab1..de320122 100644 --- a/doc/introduction/setup-dev.md +++ b/doc/introduction/setup-dev.md @@ -1,5 +1,8 @@ # Setup a dev environment +- [frontend doc](../../taxonomy-editor-frontend/README.md) +- [backend doc](../../backend/README.md) + ## Using Docker Docker is the easiest way to install the Taxonomy Editor, play with it, and even modify the code. @@ -37,6 +40,7 @@ The API is exposed at: `http://api.taxonomy.localhost:8091` You can also access the Neo4j Admin Console at `http://localhost:7474/browser/` If you modify any file in the React App, the changes will be taken into account instantly. +However, this feature is not compatible with Windows systems. In order to use live reload on a Windows machine, you will need to install and use [WSL2](https://learn.microsoft.com/en-us/windows/wsl/install). This will allow you to run the development server in a Linux environment, and the live reload feature will work as expected. If you modify any files related to the Python API, you need to restart the `taxonomy_api` container in Docker: `docker-compose restart taxonomy_api` diff --git a/docker-compose.yml b/docker-compose.yml index 7fc58de3..d1857a2d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,15 +3,20 @@ version: "3.9" services: neo4j: restart: ${RESTART_POLICY:-no} - image: neo4j:4.4.8-community + image: neo4j:5.3.0-community ports: # admin console - - "127.0.0.1:7474:7474" + - "${NEO4J_ADMIN_EXPOSE:-127.0.0.1:7474}:7474" # db bolt api - - "127.0.0.1:7687:7687" + - "${NEO4J_BOLT_EXPOSE:-127.0.0.1:7687}:7687" environment: - # we should not expose it publicly + # we should not expose it publicly, so no auth is ok NEO4J_AUTH: none + # memory configuration from .env + NEO4J_server_memory_heap_initial__size: + NEO4J_server_memory_heap_max__size: + NEO4J_server_memory_pagecache_size: + NEO4J_db_memory_transaction_total_max: volumes: # put data in a volume - neo4j-data:/data @@ -29,6 +34,8 @@ services: # taken from .env GITHUB_PAT: REPO_URI: + # make proxy headers be taken into account + FORWARDED_ALLOW_IPS: "*" taxonomy_frontend: # this is the nginx frontend serving react static version or redirecting queries image: ghcr.io/openfoodfacts/taxonomy-editor/taxonomy_frontend:${DOCKER_TAG} @@ -40,6 +47,7 @@ services: DEV_UI_SUFFIX: "-dev" TAXONOMY_EDITOR_DOMAIN: PUBLIC_TAXONOMY_EDITOR_PORT: + API_SCHEME: volumes: # Nginx, we use templates dir to be able to use environment vars - ./conf/nginx.conf:/etc/nginx/templates/default.conf.template diff --git a/docker/dev.yml b/docker/dev.yml index 5d0f0393..5b85115a 100644 --- a/docker/dev.yml +++ b/docker/dev.yml @@ -16,10 +16,14 @@ services: volumes: # in development mode, mount code directory dynamically - ./backend/editor:/code/editor + - ./backend/tests:/code/tests - ./parser:/parser # for linting / checks purpose - ./backend/pyproject.toml:/code/pyproject.toml - ./backend/setup.cfg:/code/setup.cfg + # uvicorn with live reload + command: ["uvicorn", "editor.api:app", "--host", "0.0.0.0", "--port", "80", "--reload", "--reload-dir", "/parser", "--reload-dir", "/code/editor"] + taxonomy_node: image: taxonomy-editor/taxonomy_node:dev build: @@ -32,7 +36,7 @@ services: environment: - REACT_APP_API_URL=//api.${TAXONOMY_EDITOR_DOMAIN}:${TAXONOMY_EDITOR_PORT:-80}/ - WDS_SOCKET_HOST=ui.${TAXONOMY_EDITOR_DOMAIN} - - WDS_SOCKET_PORT=${TAXONOMY_EDITOR_PORT:-80} + - WDS_SOCKET_PORT=${WDS_SOCKET_PORT:-80} - TAXONOMY_EDITOR_DOMAIN - NODE_ENV=development # avoid host check in dev @@ -44,6 +48,8 @@ services: - ./taxonomy-editor-frontend/build:/opt/taxonomy-editor/build - ./taxonomy-editor-frontend/public:/opt/taxonomy-editor/public - ./taxonomy-editor-frontend/src:/opt/taxonomy-editor/src + - ./taxonomy-editor-frontend/tsconfig.js:/opt/taxonomy-editor/tsconfig.js + - ./taxonomy-editor-frontend/webpack.config.js:/opt/taxonomy-editor/webpack.config.js taxonomy_frontend: image: taxonomy-editor/taxonomy_frontend:dev # instruction to build locally @@ -53,9 +59,8 @@ services: USER_UID: ${USER_UID:-1000} USER_GID: ${USER_GID:-1000} environment: - # disabling prod ui config and enabling node one - PROD_UI_SUFFIX: "-static" - DEV_UI_SUFFIX: "" - volumes: - # mount build folder dynamically - - ./taxonomy-editor-frontend/build:/opt/taxonomy-editor/build \ No newline at end of file + # by default, disabling prod ui config and enabling node one + # you can change this settings to try the prod configuration, + # by setting PROD_UI_SUFFIX to "" and DEV_UI_SUFFIX to "-dev" + PROD_UI_SUFFIX: "${PROD_UI_SUFFIX--static}" + DEV_UI_SUFFIX: "${DEV_UI_SUFFIX-}" diff --git a/parser/openfoodfacts_taxonomy_parser/parser.py b/parser/openfoodfacts_taxonomy_parser/parser.py index 40b248b3..cd346928 100644 --- a/parser/openfoodfacts_taxonomy_parser/parser.py +++ b/parser/openfoodfacts_taxonomy_parser/parser.py @@ -20,19 +20,19 @@ def __init__(self): self.parsing_warnings = [] # Stores all warning logs self.parsing_errors = [] # Stores all error logs - def info(self, msg): + def info(self, msg, *args, **kwargs): """Stores all parsing info logs""" - logging.info(msg) + logging.info(msg, *args, **kwargs) - def warning(self, msg): + def warning(self, msg, *args, **kwargs): """Stores all parsing warning logs""" - self.parsing_warnings.append(msg) - logging.warning(msg) + self.parsing_warnings.append(msg % args) + logging.warning(msg, *args, **kwargs) - def error(self, msg): + def error(self, msg, *args, **kwargs): """Stores all parsing error logs""" - self.parsing_errors.append(msg) - logging.error(msg) + self.parsing_errors.append(msg % args) + logging.error(msg, *args, **kwargs) class Parser: @@ -81,7 +81,7 @@ def create_node(self, data, multi_label): entry_query += " SET n." + key + " = $" + key + "\n" query = id_query + entry_query + position_query - self.session.run(query, data, is_before=self.is_before) + self.session.run(query, data) def normalized_filename(self, filename): """Add the .txt extension if it is missing in the filename""" @@ -148,7 +148,7 @@ def get_lc_value(self, line): new_line.append(self.remove_stopwords(lc, normalizing(word, lc))) return lc, new_line - def new_node_data(self): + def new_node_data(self, is_before): """To create an empty dictionary that will be used to create node""" data = { "id": "", @@ -156,6 +156,7 @@ def new_node_data(self): "preceding_lines": [], "parent_tag": [], "src_position": None, + "is_before": is_before, } return data @@ -205,20 +206,21 @@ def remove_separating_line(self, data): To remove the one separating line that is always there, between synonyms part and stopwords part and before each entry """ + is_before = data["is_before"] # first, check if there is at least one preceding line if data["preceding_lines"] and not data["preceding_lines"][0]: if data["id"].startswith("synonyms"): # it's a synonyms block, # if the previous block is a stopwords block, # there is at least one separating line - if "stopwords" in self.is_before: + if "stopwords" in is_before: data["preceding_lines"].pop(0) elif data["id"].startswith("stopwords"): # it's a stopwords block, # if the previous block is a synonyms block, # there is at least one separating line - if "synonyms" in self.is_before: + if "synonyms" in is_before: data["preceding_lines"].pop(0) else: @@ -227,7 +229,8 @@ def remove_separating_line(self, data): return data def harvest(self, filename): - """Transform data from file to dictionary""" + """Transform data from file to dictionary + """ saved_nodes = [] index_stopwords = 0 index_synonyms = 0 @@ -242,24 +245,25 @@ def harvest(self, filename): # header header, next_line = self.header_harvest(filename) yield header - self.is_before = "__header__" # the other entries - data = self.new_node_data() + data = self.new_node_data(is_before="__header__") + data["is_before"] = "__header__" for line_number, line in self.file_iter(filename, next_line): # yield data if block ended if self.entry_end(line, data): if data["id"] in saved_nodes: - msg = f"Entry with same id {data['id']} already created, " - msg += f"duplicate id in file at line {data['src_position']}. " - msg += "Node creation cancelled" - self.parser_logger.error(msg) + msg = ( + "Entry with same id %s already created, " + "duplicate id in file at line %s. " + "Node creation cancelled." + ) + self.parser_logger.error(msg, data['id'], data['src_position']) else: data = self.remove_separating_line(data) yield data # another function will use this dictionary to create a node - self.is_before = data["id"] saved_nodes.append(data["id"]) - data = self.new_node_data() + data = self.new_node_data(is_before=data["id"]) # harvest the line if not (line) or line[0] == "#": @@ -326,7 +330,6 @@ def harvest(self, filename): # in case 2 normalized synonyms are the same tagsids_list.append(word_normalized) data["tags_" + lang] = tags_list - data["tags_" + lang + "_str"] = " ".join(tags_list) data["tags_ids_" + lang] = tagsids_list else: # property definition @@ -369,7 +372,7 @@ def create_nodes(self, filename, multi_label): def create_previous_link(self, multi_label): self.parser_logger.info("Creating 'is_before' links") - query = f"MATCH(n:{multi_label}) WHERE exists(n.is_before) return n.id, n.is_before" + query = f"MATCH(n:{multi_label}) WHERE n.is_before IS NOT NULL return n.id, n.is_before" results = self.session.run(query) for result in results: id = result["n.id"] @@ -435,7 +438,7 @@ def create_fulltext_index(self, taxonomy_name, branch_name): self.session.run("".join(query)) language_codes = [lang.alpha2 for lang in list(iso639.languages) if lang.alpha2 != ""] - tags_prefixed_lc = ["n.tags_" + lc + "_str" for lc in language_codes] + tags_prefixed_lc = ["n.tags_" + lc for lc in language_codes] tags_prefixed_lc = ", ".join(tags_prefixed_lc) query = f"""CREATE FULLTEXT INDEX {project_name+'_SearchTags'} IF NOT EXISTS FOR (n:{project_name}) ON EACH [{tags_prefixed_lc}]""" @@ -443,8 +446,9 @@ def create_fulltext_index(self, taxonomy_name, branch_name): def create_parsing_errors_node(self, taxonomy_name, branch_name): """Create node to list parsing errors""" - query = """ - CREATE (n:ERRORS) + multi_label = self.create_multi_label(taxonomy_name, branch_name) + query = f""" + CREATE (n:{multi_label}:ERRORS) SET n.id = $project_name SET n.branch_name = $branch_name SET n.taxonomy_name = $taxonomy_name diff --git a/parser/requirements-test.txt b/parser/requirements-test.txt index 63a0d85d..8bd7ed57 100644 --- a/parser/requirements-test.txt +++ b/parser/requirements-test.txt @@ -1,6 +1,6 @@ attrs==22.1.0 iniconfig==1.1.1 -neo4j==4.4.5 +neo4j==5.4.0 packaging==21.3 pluggy==1.0.0 py==1.11.0 diff --git a/parser/requirements.txt b/parser/requirements.txt index 4bcfa042..58bd097f 100644 --- a/parser/requirements.txt +++ b/parser/requirements.txt @@ -1,4 +1,4 @@ -neo4j==4.4.5 +neo4j==5.4.0 pytz==2022.1 Unidecode==1.3.4 iso-639==0.4.5 \ No newline at end of file diff --git a/parser/tests/integration/test_parse_unparse_integration.py b/parser/tests/integration/test_parse_unparse_integration.py index 3ababa3a..a21a7456 100644 --- a/parser/tests/integration/test_parse_unparse_integration.py +++ b/parser/tests/integration/test_parse_unparse_integration.py @@ -44,7 +44,7 @@ def test_round_trip(neo4j): query = "MATCH (n:p_test_branch:t_test:b_branch) RETURN COUNT(*)" result = session.run(query) number_of_nodes = result.value()[0] - assert number_of_nodes == 13 + assert number_of_nodes == 14 # dump taxonomy back test_dumper = unparser.WriteTaxonomy(session) @@ -88,12 +88,12 @@ def test_two_branch_round_trip(neo4j): query = "MATCH (n:p_test_branch1:t_test:b_branch1) RETURN COUNT(*)" result = session.run(query) number_of_nodes = result.value()[0] - assert number_of_nodes == 13 + assert number_of_nodes == 14 query = "MATCH (n:p_test_branch2:t_test:b_branch2) RETURN COUNT(*)" result = session.run(query) number_of_nodes = result.value()[0] - assert number_of_nodes == 13 + assert number_of_nodes == 14 # dump taxonomy back test_dumper = unparser.WriteTaxonomy(session) diff --git a/parser/tests/integration/test_parser_integration.py b/parser/tests/integration/test_parser_integration.py index a35606b6..c5223107 100644 --- a/parser/tests/integration/test_parser_integration.py +++ b/parser/tests/integration/test_parser_integration.py @@ -1,4 +1,6 @@ +import logging import pathlib +import textwrap import pytest @@ -164,3 +166,48 @@ def test_calling(neo4j): for pair in created_pairs: assert pair in expected_pairs session.close() + + +def test_error_log(neo4j, tmp_path, caplog): + # error entries with same id + session = neo4j.session() + test_parser = parser.Parser(session) + + taxonomy_txt = textwrap.dedent(""" + # a fake taxonomy + stopwords:fr: aux,au,de,le,du,la,a,et + + # meat + en:meat + + =6.9.0" @@ -86,9 +98,9 @@ } }, "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -139,20 +151,21 @@ } }, "node_modules/@babel/eslint-parser/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/generator": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.9.tgz", - "integrity": "sha512-wt5Naw6lJrL1/SGkipMiFxJjtyczUWTP38deiP1PO60HsBjDeKk08CGC3S8iVuvf0FmTdgKwU1KIXzSKL1G0Ug==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dependencies": { - "@babel/types": "^7.18.9", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "engines": { @@ -213,9 +226,9 @@ } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -272,17 +285,17 @@ } }, "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "engines": { "node": ">=6.9.0" } @@ -299,23 +312,23 @@ } }, "node_modules/@babel/helper-function-name": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz", - "integrity": "sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dependencies": { - "@babel/template": "^7.18.6", - "@babel/types": "^7.18.9" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -435,20 +448,28 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz", - "integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } @@ -489,12 +510,12 @@ } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -502,9 +523,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.9.tgz", - "integrity": "sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "bin": { "parser": "bin/babel-parser.js" }, @@ -1535,9 +1556,9 @@ } }, "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -1747,9 +1768,9 @@ } }, "node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -1828,31 +1849,31 @@ } }, "node_modules/@babel/template": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.6.tgz", - "integrity": "sha512-JoDWzPe+wgBsTTgdnIma3iHNFC7YVJoPssVBDjiHfNlyt4YcunDtcDOUmfVDfCK5MfdsaIoX9PkijPhjH3nYUw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.6", - "@babel/types": "^7.18.6" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.9.tgz", - "integrity": "sha512-LcPAnujXGwBgv3/WHv01pHtb2tihcyW1XuL9wd7jqh1Z8AQkTd+QVjMrMijrln0T7ED3UXLIy36P9Ao7W75rYg==", - "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.18.9", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.18.9", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.18.9", - "@babel/types": "^7.18.9", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -1861,11 +1882,12 @@ } }, "node_modules/@babel/types": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.9.tgz", - "integrity": "sha512-WwMLAg2MvJmt/rKEVQBBhIVffMmnilX4oe0sRe7iPOHIGsqpruFHHdrfj4O1CMMtgMtCU4oPafZjDPCRgO57Wg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -2731,6 +2753,25 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/@jest/expect-utils": { + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.3.1.tgz", + "integrity": "sha512-wlrznINZI5sMjwvUoLVk617ll/UYfGIZNxmbU+Pa7wmkL4vYzhV9R2pwVqUh4NWWuLQWkI8+8mOkxs//prKQ3g==", + "dependencies": { + "jest-get-type": "^29.2.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils/node_modules/jest-get-type": { + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.2.0.tgz", + "integrity": "sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jest/fake-timers": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", @@ -3167,12 +3208,12 @@ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.14", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", - "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@leichtgewicht/ip-codec": { @@ -4376,12 +4417,47 @@ } }, "node_modules/@types/jest": { - "version": "28.1.6", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-28.1.6.tgz", - "integrity": "sha512-0RbGAFMfcBJKOmqRazM8L98uokwuwD5F8rHrv/ZMbrZBwVOWZUyPG6VFNscjYr/vjM3Vu4fRrCPbOs42AfemaQ==", + "version": "29.2.6", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.2.6.tgz", + "integrity": "sha512-XEUC/Tgw3uMh6Ho8GkUtQ2lPhY5Fmgyp3TdlkTJs1W9VgNxs+Ow/x3Elh8lHQKqCbZL0AubQuqWjHVT033Hhrw==", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest/node_modules/@jest/schemas": { + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.0.0.tgz", + "integrity": "sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==", + "dependencies": { + "@sinclair/typebox": "^0.24.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/@jest/types": { + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.3.1.tgz", + "integrity": "sha512-d0S0jmmTpjnhCmNpApgX3jrUZgZ22ivKJRvL2lli5hpCRoNnp1f85r2/wpKfXuYu8E7Jjh1hGfhPyup1NM5AmA==", + "dependencies": { + "@jest/schemas": "^29.0.0", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/@types/yargs": { + "version": "17.0.20", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.20.tgz", + "integrity": "sha512-eknWrTHofQuPk2iuqDm1waA7V6xPlbgBoaaXEgYkClhLOnB0TtbW+srJaOToAgawPxPlHQzwypFA2bhZaUGP5A==", "dependencies": { - "jest-matcher-utils": "^28.0.0", - "pretty-format": "^28.0.0" + "@types/yargs-parser": "*" } }, "node_modules/@types/jest/node_modules/ansi-styles": { @@ -4430,11 +4506,26 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/@types/jest/node_modules/diff-sequences": { - "version": "28.1.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz", - "integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.3.1.tgz", + "integrity": "sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ==", "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/expect": { + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.3.1.tgz", + "integrity": "sha512-gGb1yTgU30Q0O/tQq+z30KBWv24ApkMgFUpvKBkyLUBL68Wv8dHdJxTBZFl/iT8K/bqDHvUYRH6IIN3rToopPA==", + "dependencies": { + "@jest/expect-utils": "^29.3.1", + "jest-get-type": "^29.2.0", + "jest-matcher-utils": "^29.3.1", + "jest-message-util": "^29.3.1", + "jest-util": "^29.3.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@types/jest/node_modules/has-flag": { @@ -4446,53 +4537,87 @@ } }, "node_modules/@types/jest/node_modules/jest-diff": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz", - "integrity": "sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.3.1.tgz", + "integrity": "sha512-vU8vyiO7568tmin2lA3r2DP8oRvzhvRcD4DjpXc6uGveQodyk7CKLhQlCSiwgx3g0pFaE88/KLZ0yaTWMc4Uiw==", "dependencies": { "chalk": "^4.0.0", - "diff-sequences": "^28.1.1", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.3" + "diff-sequences": "^29.3.1", + "jest-get-type": "^29.2.0", + "pretty-format": "^29.3.1" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@types/jest/node_modules/jest-get-type": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz", - "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==", + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.2.0.tgz", + "integrity": "sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA==", "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@types/jest/node_modules/jest-matcher-utils": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz", - "integrity": "sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.3.1.tgz", + "integrity": "sha512-fkRMZUAScup3txIKfMe3AIZZmPEjWEdsPJFK3AIy5qRohWqQFg1qrmKfYXR9qEkNc7OdAu2N4KPHibEmy4HPeQ==", "dependencies": { "chalk": "^4.0.0", - "jest-diff": "^28.1.3", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.3" + "jest-diff": "^29.3.1", + "jest-get-type": "^29.2.0", + "pretty-format": "^29.3.1" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-message-util": { + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.3.1.tgz", + "integrity": "sha512-lMJTbgNcDm5z+6KDxWtqOFWlGQxD6XaYwBqHR8kmpkP+WWWG90I35kdtQHY67Ay5CSuydkTBbJG+tH9JShFCyA==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.3.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.3.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/jest-util": { + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.3.1.tgz", + "integrity": "sha512-7YOVZaiX7RJLv76ZfHt4nbNEzzTRiMW/IiOG7ZOKmTXmoGBxUDefgMAxQubu6WPVqP5zSzAdZG0FfLcC7HOIFQ==", + "dependencies": { + "@jest/types": "^29.3.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@types/jest/node_modules/pretty-format": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz", - "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.3.1.tgz", + "integrity": "sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg==", "dependencies": { - "@jest/schemas": "^28.1.3", - "ansi-regex": "^5.0.1", + "@jest/schemas": "^29.0.0", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@types/jest/node_modules/pretty-format/node_modules/ansi-styles": { @@ -4538,9 +4663,9 @@ "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" }, "node_modules/@types/node": { - "version": "18.6.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.2.tgz", - "integrity": "sha512-KcfkBq9H4PI6Vpu5B/KoPeuVDAbmi+2mDBqGPGUgoL7yXQtcWGu2vJWmmRkneWK3Rh0nIAX192Aa87AqKHYChQ==" + "version": "18.11.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", + "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -4573,9 +4698,9 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" }, "node_modules/@types/react": { - "version": "18.0.15", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.15.tgz", - "integrity": "sha512-iz3BtLuIYH1uWdsv6wXYdhozhqj20oD4/Hk2DNXIn1kFsmp9x8d9QB6FnPhfkbhd2PgEONt9Q1x/ebkwjfFLow==", + "version": "18.0.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.27.tgz", + "integrity": "sha512-3vtRKHgVxu3Jp9t718R9BuzoD4NcQ8YJ5XRzsSKxNDiDonD2MXIT1TmSkenxuCycZJoQT5d2vE8LwWJxBC1gmA==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -4583,9 +4708,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.0.6", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.6.tgz", - "integrity": "sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA==", + "version": "18.0.10", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz", + "integrity": "sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==", "dependencies": { "@types/react": "*" } @@ -5178,6 +5303,19 @@ "node": ">= 6.0.0" } }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -5411,6 +5549,15 @@ "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==" }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/async": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", @@ -5678,9 +5825,9 @@ } }, "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -6160,6 +6307,87 @@ "node": ">=0.10.0" } }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-truncate": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", + "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", + "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -7259,6 +7487,12 @@ "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==", + "dev": true + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -7812,9 +8046,9 @@ } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -7885,9 +8119,9 @@ } }, "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -9410,6 +9644,21 @@ "node": ">=10.17.0" } }, + "node_modules/husky": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", + "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", + "dev": true, + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/i18next": { "version": "21.8.16", "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.16.tgz", @@ -9924,9 +10173,9 @@ } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -12048,9 +12297,9 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, "node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "bin": { "json5": "lib/cli.js" }, @@ -12159,45 +12408,293 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, - "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/lint-staged": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.1.0.tgz", + "integrity": "sha512-pn/sR8IrcF/T0vpWLilih8jmVouMlxqXxKuAojmbiGX5n/gDnz+abdPptlj0vYnbfE0SQNl3CY/HwtM0+yfOVQ==", + "dev": true, + "dependencies": { + "cli-truncate": "^3.1.0", + "colorette": "^2.0.19", + "commander": "^9.4.1", + "debug": "^4.3.4", + "execa": "^6.1.0", + "lilconfig": "2.0.6", + "listr2": "^5.0.5", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-inspect": "^1.12.2", + "pidtree": "^0.6.0", + "string-argv": "^0.3.1", + "yaml": "^2.1.3" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, "engines": { - "node": ">=6.11.5" + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" } }, - "node_modules/loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "node_modules/lint-staged/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/lint-staged/node_modules/execa": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz", + "integrity": "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==", + "dev": true, "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^3.0.1", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" }, "engines": { - "node": ">=8.9.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dependencies": { - "p-locate": "^5.0.0" + "node_modules/lint-staged/node_modules/human-signals": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz", + "integrity": "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==", + "dev": true, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/lint-staged/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, "engines": { - "node": ">=10" + "node": ">=12" }, "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/lint-staged/node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lint-staged/node_modules/yaml": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.1.tgz", + "integrity": "sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/listr2": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-5.0.6.tgz", + "integrity": "sha512-u60KxKBy1BR2uLJNTWNptzWQ1ob/gjMzIJPZffAENzpZqbMZ/5PrXXOomDcevIS/+IB7s1mmCEtSlT2qHWMqag==", + "dev": true, + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.19", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.7", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/listr2/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/listr2/node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "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.debounce": { "version": "4.0.8", @@ -12224,6 +12721,88 @@ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-update/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -12285,9 +12864,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -12868,6 +13447,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-retry": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", @@ -13010,6 +13604,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -14322,6 +14928,21 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.2.tgz", + "integrity": "sha512-BtRV9BcncDyI2tsuS19zzhzoxD8Dh8LiCx7j7tHzrkz8GFXAexeWFdi22mjE1d16dftH2qNaytVxqiRTGlMfpw==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -14680,9 +15301,9 @@ } }, "node_modules/react-dev-utils/node_modules/loader-utils": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.0.tgz", - "integrity": "sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", "engines": { "node": ">= 12.13.0" } @@ -14928,25 +15549,14 @@ } }, "node_modules/recursive-readdir": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz", - "integrity": "sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg==", - "dependencies": { - "minimatch": "3.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/recursive-readdir/node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", + "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", "dependencies": { - "brace-expansion": "^1.1.7" + "minimatch": "^3.0.5" }, "engines": { - "node": "*" + "node": ">=6.0.0" } }, "node_modules/redent": { @@ -15210,6 +15820,19 @@ "node": ">=10" } }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -15227,6 +15850,12 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true + }, "node_modules/rifm": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/rifm/-/rifm-0.12.1.tgz", @@ -15339,6 +15968,15 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz", + "integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -15449,9 +16087,9 @@ } }, "node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -15655,6 +16293,46 @@ "node": ">=8" } }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -15836,6 +16514,15 @@ } ] }, + "node_modules/string-argv": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -16344,6 +17031,12 @@ "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz", "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==" }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", @@ -16435,9 +17128,9 @@ } }, "node_modules/tsconfig-paths/node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dependencies": { "minimist": "^1.2.0" }, @@ -16528,10 +17221,9 @@ } }, "node_modules/typescript": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", - "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", - "peer": true, + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", + "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16799,9 +17491,9 @@ } }, "node_modules/webpack": { - "version": "5.74.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.74.0.tgz", - "integrity": "sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==", + "version": "5.76.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz", + "integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==", "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^0.0.51", @@ -17199,9 +17891,9 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", "engines": { "node": ">=0.10.0" } @@ -17669,11 +18361,12 @@ } }, "@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "requires": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" } }, "@babel/compat-data": { @@ -17704,9 +18397,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" } } }, @@ -17740,19 +18433,20 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" } } }, "@babel/generator": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.9.tgz", - "integrity": "sha512-wt5Naw6lJrL1/SGkipMiFxJjtyczUWTP38deiP1PO60HsBjDeKk08CGC3S8iVuvf0FmTdgKwU1KIXzSKL1G0Ug==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "requires": { - "@babel/types": "^7.18.9", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "dependencies": { @@ -17797,9 +18491,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" } } }, @@ -17840,16 +18534,16 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" } } }, "@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==" + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==" }, "@babel/helper-explode-assignable-expression": { "version": "7.18.6", @@ -17860,20 +18554,20 @@ } }, "@babel/helper-function-name": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz", - "integrity": "sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "requires": { - "@babel/template": "^7.18.6", - "@babel/types": "^7.18.9" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-member-expression-to-functions": { @@ -17960,17 +18654,22 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==" + }, "@babel/helper-validator-identifier": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz", - "integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==" + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" }, "@babel/helper-validator-option": { "version": "7.18.6", @@ -17999,19 +18698,19 @@ } }, "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.9.tgz", - "integrity": "sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg==" + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==" }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.18.6", @@ -18649,9 +19348,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" } } }, @@ -18806,9 +19505,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" } } }, @@ -18865,38 +19564,39 @@ } }, "@babel/template": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.6.tgz", - "integrity": "sha512-JoDWzPe+wgBsTTgdnIma3iHNFC7YVJoPssVBDjiHfNlyt4YcunDtcDOUmfVDfCK5MfdsaIoX9PkijPhjH3nYUw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.6", - "@babel/types": "^7.18.6" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" } }, "@babel/traverse": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.9.tgz", - "integrity": "sha512-LcPAnujXGwBgv3/WHv01pHtb2tihcyW1XuL9wd7jqh1Z8AQkTd+QVjMrMijrln0T7ED3UXLIy36P9Ao7W75rYg==", - "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.18.9", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.18.9", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.18.9", - "@babel/types": "^7.18.9", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.9.tgz", - "integrity": "sha512-WwMLAg2MvJmt/rKEVQBBhIVffMmnilX4oe0sRe7iPOHIGsqpruFHHdrfj4O1CMMtgMtCU4oPafZjDPCRgO57Wg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "requires": { - "@babel/helper-validator-identifier": "^7.18.6", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, @@ -19486,6 +20186,21 @@ "jest-mock": "^27.5.1" } }, + "@jest/expect-utils": { + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.3.1.tgz", + "integrity": "sha512-wlrznINZI5sMjwvUoLVk617ll/UYfGIZNxmbU+Pa7wmkL4vYzhV9R2pwVqUh4NWWuLQWkI8+8mOkxs//prKQ3g==", + "requires": { + "jest-get-type": "^29.2.0" + }, + "dependencies": { + "jest-get-type": { + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.2.0.tgz", + "integrity": "sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA==" + } + } + }, "@jest/fake-timers": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", @@ -19813,12 +20528,12 @@ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" }, "@jridgewell/trace-mapping": { - "version": "0.3.14", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", - "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", + "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "@leichtgewicht/ip-codec": { @@ -20599,14 +21314,43 @@ } }, "@types/jest": { - "version": "28.1.6", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-28.1.6.tgz", - "integrity": "sha512-0RbGAFMfcBJKOmqRazM8L98uokwuwD5F8rHrv/ZMbrZBwVOWZUyPG6VFNscjYr/vjM3Vu4fRrCPbOs42AfemaQ==", + "version": "29.2.6", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.2.6.tgz", + "integrity": "sha512-XEUC/Tgw3uMh6Ho8GkUtQ2lPhY5Fmgyp3TdlkTJs1W9VgNxs+Ow/x3Elh8lHQKqCbZL0AubQuqWjHVT033Hhrw==", "requires": { - "jest-matcher-utils": "^28.0.0", - "pretty-format": "^28.0.0" + "expect": "^29.0.0", + "pretty-format": "^29.0.0" }, "dependencies": { + "@jest/schemas": { + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.0.0.tgz", + "integrity": "sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==", + "requires": { + "@sinclair/typebox": "^0.24.1" + } + }, + "@jest/types": { + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.3.1.tgz", + "integrity": "sha512-d0S0jmmTpjnhCmNpApgX3jrUZgZ22ivKJRvL2lli5hpCRoNnp1f85r2/wpKfXuYu8E7Jjh1hGfhPyup1NM5AmA==", + "requires": { + "@jest/schemas": "^29.0.0", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@types/yargs": { + "version": "17.0.20", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.20.tgz", + "integrity": "sha512-eknWrTHofQuPk2iuqDm1waA7V6xPlbgBoaaXEgYkClhLOnB0TtbW+srJaOToAgawPxPlHQzwypFA2bhZaUGP5A==", + "requires": { + "@types/yargs-parser": "*" + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -20638,9 +21382,21 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "diff-sequences": { - "version": "28.1.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz", - "integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==" + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.3.1.tgz", + "integrity": "sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ==" + }, + "expect": { + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.3.1.tgz", + "integrity": "sha512-gGb1yTgU30Q0O/tQq+z30KBWv24ApkMgFUpvKBkyLUBL68Wv8dHdJxTBZFl/iT8K/bqDHvUYRH6IIN3rToopPA==", + "requires": { + "@jest/expect-utils": "^29.3.1", + "jest-get-type": "^29.2.0", + "jest-matcher-utils": "^29.3.1", + "jest-message-util": "^29.3.1", + "jest-util": "^29.3.1" + } }, "has-flag": { "version": "4.0.0", @@ -20648,39 +21404,67 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "jest-diff": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz", - "integrity": "sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.3.1.tgz", + "integrity": "sha512-vU8vyiO7568tmin2lA3r2DP8oRvzhvRcD4DjpXc6uGveQodyk7CKLhQlCSiwgx3g0pFaE88/KLZ0yaTWMc4Uiw==", "requires": { "chalk": "^4.0.0", - "diff-sequences": "^28.1.1", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.3" + "diff-sequences": "^29.3.1", + "jest-get-type": "^29.2.0", + "pretty-format": "^29.3.1" } }, "jest-get-type": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz", - "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==" + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.2.0.tgz", + "integrity": "sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA==" }, "jest-matcher-utils": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz", - "integrity": "sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.3.1.tgz", + "integrity": "sha512-fkRMZUAScup3txIKfMe3AIZZmPEjWEdsPJFK3AIy5qRohWqQFg1qrmKfYXR9qEkNc7OdAu2N4KPHibEmy4HPeQ==", + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.3.1", + "jest-get-type": "^29.2.0", + "pretty-format": "^29.3.1" + } + }, + "jest-message-util": { + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.3.1.tgz", + "integrity": "sha512-lMJTbgNcDm5z+6KDxWtqOFWlGQxD6XaYwBqHR8kmpkP+WWWG90I35kdtQHY67Ay5CSuydkTBbJG+tH9JShFCyA==", + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.3.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.3.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-util": { + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.3.1.tgz", + "integrity": "sha512-7YOVZaiX7RJLv76ZfHt4nbNEzzTRiMW/IiOG7ZOKmTXmoGBxUDefgMAxQubu6WPVqP5zSzAdZG0FfLcC7HOIFQ==", "requires": { + "@jest/types": "^29.3.1", + "@types/node": "*", "chalk": "^4.0.0", - "jest-diff": "^28.1.3", - "jest-get-type": "^28.0.2", - "pretty-format": "^28.1.3" + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" } }, "pretty-format": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz", - "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.3.1.tgz", + "integrity": "sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg==", "requires": { - "@jest/schemas": "^28.1.3", - "ansi-regex": "^5.0.1", + "@jest/schemas": "^29.0.0", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -20723,9 +21507,9 @@ "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" }, "@types/node": { - "version": "18.6.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.2.tgz", - "integrity": "sha512-KcfkBq9H4PI6Vpu5B/KoPeuVDAbmi+2mDBqGPGUgoL7yXQtcWGu2vJWmmRkneWK3Rh0nIAX192Aa87AqKHYChQ==" + "version": "18.11.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", + "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" }, "@types/parse-json": { "version": "4.0.0", @@ -20758,9 +21542,9 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" }, "@types/react": { - "version": "18.0.15", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.15.tgz", - "integrity": "sha512-iz3BtLuIYH1uWdsv6wXYdhozhqj20oD4/Hk2DNXIn1kFsmp9x8d9QB6FnPhfkbhd2PgEONt9Q1x/ebkwjfFLow==", + "version": "18.0.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.27.tgz", + "integrity": "sha512-3vtRKHgVxu3Jp9t718R9BuzoD4NcQ8YJ5XRzsSKxNDiDonD2MXIT1TmSkenxuCycZJoQT5d2vE8LwWJxBC1gmA==", "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -20768,9 +21552,9 @@ } }, "@types/react-dom": { - "version": "18.0.6", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.6.tgz", - "integrity": "sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA==", + "version": "18.0.10", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz", + "integrity": "sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==", "requires": { "@types/react": "*" } @@ -21227,6 +22011,16 @@ "debug": "4" } }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -21393,6 +22187,12 @@ "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", "integrity": "sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==" }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, "async": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", @@ -21577,9 +22377,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" } } }, @@ -21945,6 +22745,59 @@ } } }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-truncate": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz", + "integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==", + "dev": true, + "requires": { + "slice-ansi": "^5.0.0", + "string-width": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", + "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + } + } + }, "cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -22743,6 +23596,12 @@ "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -23240,9 +24099,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" } } }, @@ -23286,9 +24145,9 @@ } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" } } }, @@ -24309,6 +25168,12 @@ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==" }, + "husky": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", + "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", + "dev": true + }, "i18next": { "version": "21.8.16", "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.8.16.tgz", @@ -24643,9 +25508,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" } } }, @@ -26201,9 +27066,9 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==" + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" }, "jsonfile": { "version": "6.1.0", @@ -26280,15 +27145,178 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "lint-staged": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.1.0.tgz", + "integrity": "sha512-pn/sR8IrcF/T0vpWLilih8jmVouMlxqXxKuAojmbiGX5n/gDnz+abdPptlj0vYnbfE0SQNl3CY/HwtM0+yfOVQ==", + "dev": true, + "requires": { + "cli-truncate": "^3.1.0", + "colorette": "^2.0.19", + "commander": "^9.4.1", + "debug": "^4.3.4", + "execa": "^6.1.0", + "lilconfig": "2.0.6", + "listr2": "^5.0.5", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-inspect": "^1.12.2", + "pidtree": "^0.6.0", + "string-argv": "^0.3.1", + "yaml": "^2.1.3" + }, + "dependencies": { + "commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true + }, + "execa": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz", + "integrity": "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^3.0.1", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + } + }, + "human-signals": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz", + "integrity": "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==", + "dev": true + }, + "is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true + }, + "mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true + }, + "npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "requires": { + "path-key": "^4.0.0" + } + }, + "onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "requires": { + "mimic-fn": "^4.0.0" + } + }, + "path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true + }, + "strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true + }, + "yaml": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.1.tgz", + "integrity": "sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==", + "dev": true + } + } + }, + "listr2": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-5.0.6.tgz", + "integrity": "sha512-u60KxKBy1BR2uLJNTWNptzWQ1ob/gjMzIJPZffAENzpZqbMZ/5PrXXOomDcevIS/+IB7s1mmCEtSlT2qHWMqag==", + "dev": true, + "requires": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.19", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.7", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "requires": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + } + } + }, "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==" }, "loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "requires": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", @@ -26333,6 +27361,66 @@ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" }, + "log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "requires": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -26379,9 +27467,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" } } }, @@ -26792,6 +27880,15 @@ "p-limit": "^3.0.2" } }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, "p-retry": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", @@ -26898,6 +27995,12 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" }, + "pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true + }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -27655,6 +28758,12 @@ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" }, + "prettier": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.2.tgz", + "integrity": "sha512-BtRV9BcncDyI2tsuS19zzhzoxD8Dh8LiCx7j7tHzrkz8GFXAexeWFdi22mjE1d16dftH2qNaytVxqiRTGlMfpw==", + "dev": true + }, "pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -27919,9 +29028,9 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, "loader-utils": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.0.tgz", - "integrity": "sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ==" + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==" }, "supports-color": { "version": "7.2.0", @@ -28095,21 +29204,11 @@ } }, "recursive-readdir": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz", - "integrity": "sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", + "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", "requires": { - "minimatch": "3.0.4" - }, - "dependencies": { - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - } + "minimatch": "^3.0.5" } }, "redent": { @@ -28301,6 +29400,16 @@ "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz", "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==" }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, "retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -28311,6 +29420,12 @@ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" }, + "rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true + }, "rifm": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/rifm/-/rifm-0.12.1.tgz", @@ -28385,6 +29500,15 @@ "queue-microtask": "^1.2.2" } }, + "rxjs": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz", + "integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -28454,9 +29578,9 @@ } }, "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "requires": { "lru-cache": "^6.0.0" } @@ -28630,6 +29754,30 @@ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" }, + "slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true + } + } + }, "sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -28770,6 +29918,12 @@ } } }, + "string-argv": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", + "dev": true + }, "string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -29143,6 +30297,12 @@ "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz", "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==" }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, "thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", @@ -29218,9 +30378,9 @@ }, "dependencies": { "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "requires": { "minimist": "^1.2.0" } @@ -29288,10 +30448,9 @@ } }, "typescript": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", - "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", - "peer": true + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", + "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==" }, "unbox-primitive": { "version": "1.0.2", @@ -29481,9 +30640,9 @@ "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==" }, "webpack": { - "version": "5.74.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.74.0.tgz", - "integrity": "sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==", + "version": "5.76.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz", + "integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==", "requires": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^0.0.51", @@ -29764,9 +30923,9 @@ } }, "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==" }, "workbox-background-sync": { "version": "6.5.4", diff --git a/taxonomy-editor-frontend/package.json b/taxonomy-editor-frontend/package.json index ee2c518b..7209eeee 100644 --- a/taxonomy-editor-frontend/package.json +++ b/taxonomy-editor-frontend/package.json @@ -11,19 +11,26 @@ "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.3.0", "@testing-library/user-event": "^13.5.0", + "@types/jest": "^29.2.6", + "@types/node": "^18.11.18", + "@types/react": "^18.0.27", + "@types/react-dom": "^18.0.10", + "fast-deep-equal": "^3.1.3", "iso-639-1": "^2.1.15", "react": "^18.2.0", "react-dom": "^18.2.0", "react-i18next": "^11.18.3", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", + "typescript": "^4.9.4", "web-vitals": "^2.1.4" }, "scripts": { "start": "GENERATE_SOURCEMAP=false react-scripts start", "build": "react-scripts build", "test": "react-scripts test", - "eject": "react-scripts eject" + "eject": "react-scripts eject", + "prepare": "cd .. && husky install taxonomy-editor-frontend/.husky" }, "proxy": "http://localhost:80", "eslintConfig": { @@ -43,5 +50,13 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "husky": "^8.0.3", + "lint-staged": "^13.1.0", + "prettier": "2.8.2" + }, + "lint-staged": { + "*.{js,css,md}": "prettier --write" } } diff --git a/taxonomy-editor-frontend/public/index.html b/taxonomy-editor-frontend/public/index.html index 306027e7..da15d55c 100644 --- a/taxonomy-editor-frontend/public/index.html +++ b/taxonomy-editor-frontend/public/index.html @@ -5,15 +5,13 @@ - + + + - - -