From a23a461c45ceb46c6e10146fee45f0d11d5169b5 Mon Sep 17 00:00:00 2001 From: JSv4 Date: Sun, 26 May 2024 15:18:54 -0700 Subject: [PATCH 01/68] Initial pass at extract models, filters, queries, mutations and tests. --- .idea/OpenContracts.iml | 2 +- .idea/misc.xml | 2 +- config/graphql/filters.py | 51 +++++ config/graphql/graphene_types.py | 34 +++ config/graphql/mutations.py | 115 +++++++++++ config/graphql/queries.py | 154 +++++++++++++- config/settings/base.py | 1 + opencontractserver/analyzer/models.py | 16 +- opencontractserver/extracts/__init__.py | 0 opencontractserver/extracts/admin.py | 3 + opencontractserver/extracts/apps.py | 13 ++ .../extracts/migrations/__init__.py | 0 opencontractserver/extracts/models.py | 193 ++++++++++++++++++ opencontractserver/extracts/tests.py | 3 + opencontractserver/extracts/views.py | 3 + opencontractserver/llms/__init__.py | 0 opencontractserver/tasks/extract_tasks.py | 79 +++++++ .../tests/test_extract_mutations.py | 145 +++++++++++++ .../tests/test_extract_queries.py | 157 ++++++++++++++ .../tests/test_extract_tasks.py | 64 ++++++ 20 files changed, 1030 insertions(+), 5 deletions(-) create mode 100644 opencontractserver/extracts/__init__.py create mode 100644 opencontractserver/extracts/admin.py create mode 100644 opencontractserver/extracts/apps.py create mode 100644 opencontractserver/extracts/migrations/__init__.py create mode 100644 opencontractserver/extracts/models.py create mode 100644 opencontractserver/extracts/tests.py create mode 100644 opencontractserver/extracts/views.py create mode 100644 opencontractserver/llms/__init__.py create mode 100644 opencontractserver/tasks/extract_tasks.py create mode 100644 opencontractserver/tests/test_extract_mutations.py create mode 100644 opencontractserver/tests/test_extract_queries.py create mode 100644 opencontractserver/tests/test_extract_tasks.py diff --git a/.idea/OpenContracts.iml b/.idea/OpenContracts.iml index 407c5384..d5e15622 100644 --- a/.idea/OpenContracts.iml +++ b/.idea/OpenContracts.iml @@ -5,7 +5,7 @@ - + diff --git a/.idea/misc.xml b/.idea/misc.xml index 49c9c8ad..18b96c1f 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,7 +3,7 @@ - + diff --git a/config/graphql/filters.py b/config/graphql/filters.py index 7d4bf86e..85cd771f 100644 --- a/config/graphql/filters.py +++ b/config/graphql/filters.py @@ -8,6 +8,7 @@ from django_filters import rest_framework as filters from graphql_relay import from_global_id +from opencontractserver.extracts.models import LanguageModel, Fieldset, Column, Extract, Row from opencontractserver.analyzer.models import Analysis, Analyzer, GremlinEngine from opencontractserver.annotations.models import ( Annotation, @@ -379,3 +380,53 @@ class Meta: "description": ["exact", "contains"], "id": ["exact"], } + +class LanguageModelFilter(django_filters.FilterSet): + class Meta: + model = LanguageModel + fields = { + "model": ["exact", "contains"], + } + + +class FieldsetFilter(django_filters.FilterSet): + class Meta: + model = Fieldset + fields = { + "name": ["exact", "contains"], + "description": ["contains"], + } + + +class ColumnFilter(django_filters.FilterSet): + class Meta: + model = Column + fields = { + "query": ["contains"], + "match_text": ["contains"], + "output_type": ["exact"], + "limit_to_label": ["exact"], + "agentic": ["exact"], + } + + +class ExtractFilter(django_filters.FilterSet): + class Meta: + model = Extract + fields = { + "name": ["exact", "contains"], + "created": ["lte", "gte"], + "started": ["lte", "gte"], + "finished": ["lte", "gte"], + } + + +class RowFilter(django_filters.FilterSet): + class Meta: + model = Row + fields = { + "data_definition": ["exact"], + "started": ["lte", "gte"], + "completed": ["lte", "gte"], + "failed": ["lte", "gte"], + } diff --git a/config/graphql/graphene_types.py b/config/graphql/graphene_types.py index e4b2150b..875d3dac 100644 --- a/config/graphql/graphene_types.py +++ b/config/graphql/graphene_types.py @@ -285,3 +285,37 @@ class Meta: model = Analysis interfaces = [relay.Node] connection_class = CountableConnection + + +class LanguageModelType(AnnotatePermissionsForReadMixin, DjangoObjectType): + class Meta: + model = LanguageModel + interfaces = [relay.Node] + connection_class = CountableConnection + +class FieldsetType(AnnotatePermissionsForReadMixin, DjangoObjectType): + class Meta: + model = Fieldset + interfaces = [relay.Node] + connection_class = CountableConnection + + +class ColumnType(AnnotatePermissionsForReadMixin, DjangoObjectType): + class Meta: + model = Column + interfaces = [relay.Node] + connection_class = CountableConnection + + +class ExtractType(AnnotatePermissionsForReadMixin, DjangoObjectType): + class Meta: + model = Extract + interfaces = [relay.Node] + connection_class = CountableConnection + + +class RowType(AnnotatePermissionsForReadMixin, DjangoObjectType): + class Meta: + model = Row + interfaces = [relay.Node] + connection_class = CountableConnection diff --git a/config/graphql/mutations.py b/config/graphql/mutations.py index 2f16f2d6..18a06db0 100644 --- a/config/graphql/mutations.py +++ b/config/graphql/mutations.py @@ -1389,3 +1389,118 @@ class Mutation(graphene.ObjectType): ) # Limited by user.is_usage_capped delete_analysis = DeleteAnalysisMutation.Field() make_analysis_public = MakeAnalysisPublic.Field() + +class CreateLanguageModel(graphene.Mutation): + class Arguments: + model = graphene.String(required=True) + + ok = graphene.Boolean() + language_model = graphene.Field(LanguageModelType) + + @staticmethod + @login_required + def mutate(root, info, model): + language_model = LanguageModel(model=model) + language_model.save() + set_permissions_for_obj_to_user( + info.context.user, language_model, [PermissionTypes.CRUD] + ) + return CreateLanguageModel(ok=True, language_model=language_model) + + +class CreateFieldset(graphene.Mutation): + class Arguments: + name = graphene.String(required=True) + description = graphene.String(required=True) + + ok = graphene.Boolean() + fieldset = graphene.Field(FieldsetType) + + @staticmethod + @login_required + def mutate(root, info, name, description): + fieldset = Fieldset(owner=info.context.user, name=name, description=description) + fieldset.save() + set_permissions_for_obj_to_user( + info.context.user, fieldset, [PermissionTypes.CRUD] + ) + return CreateFieldset(ok=True, fieldset=fieldset) + + +class CreateColumn(graphene.Mutation): + class Arguments: + fieldset_id = graphene.ID(required=True) + query = graphene.String(required=True) + match_text = graphene.String() + output_type = graphene.String(required=True) + limit_to_label = graphene.String() + instructions = graphene.String() + language_model_id = graphene.ID(required=True) + agentic = graphene.Boolean(required=True) + + ok = graphene.Boolean() + column = graphene.Field(ColumnType) + + @staticmethod + @login_required + def mutate( + root, + info, + fieldset_id, + query, + output_type, + language_model_id, + agentic, + match_text=None, + limit_to_label=None, + instructions=None, + ): + fieldset = Fieldset.objects.get(pk=from_global_id(fieldset_id)[1]) + language_model = LanguageModel.objects.get(pk=from_global_id(language_model_id)[1]) + column = Column( + fieldset=fieldset, + query=query, + match_text=match_text, + output_type=output_type, + limit_to_label=limit_to_label, + instructions=instructions, + language_model=language_model, + agentic=agentic + ) + column.save() + set_permissions_for_obj_to_user( + info.context.user, column, [PermissionTypes.CRUD] + ) + return CreateColumn(ok=True, column=column) + + +class StartExtract(graphene.Mutation): + class Arguments: + corpus_id = graphene.ID(required=True) + name = graphene.String(required=True) + fieldset_id = graphene.ID(required=True) + + ok = graphene.Boolean() + extract = graphene.Field(ExtractType) + + @staticmethod + @login_required + def mutate(root, info, corpus_id, name, fieldset_id): + corpus = Corpus.objects.get(pk=from_global_id(corpus_id)[1]) + fieldset = Fieldset.objects.get(pk=from_global_id(fieldset_id)[1]) + + extract = Extract( + corpus=corpus, + name=name, + fieldset=fieldset, + owner=info.context.user + ) + extract.save() + set_permissions_for_obj_to_user( + info.context.user, extract, [PermissionTypes.CRUD] + ) + + # Start celery task to process extract + run_extract.delay(extract.id, info.context.user.id) + + return StartExtract(ok=True, extract=extract) diff --git a/config/graphql/queries.py b/config/graphql/queries.py index 8006a513..fd98f8e1 100644 --- a/config/graphql/queries.py +++ b/config/graphql/queries.py @@ -22,7 +22,7 @@ GremlinEngineFilter, LabelFilter, LabelsetFilter, - RelationshipFilter, + RelationshipFilter, LanguageModelFilter, FieldsetFilter, ColumnFilter, ExtractFilter, RowFilter, ) from config.graphql.graphene_types import ( AnalysisType, @@ -38,7 +38,7 @@ PdfPageInfoType, RelationshipType, UserExportType, - UserImportType, + UserImportType, LanguageModelType, FieldsetType, ColumnType, ExtractType, RowType, ) from opencontractserver.analyzer.models import Analysis, Analyzer, GremlinEngine from opencontractserver.annotations.models import ( @@ -49,6 +49,7 @@ ) from opencontractserver.corpuses.models import Corpus from opencontractserver.documents.models import Document +from opencontractserver.extracts.models import LanguageModel, Fieldset, Column, Extract, Row from opencontractserver.shared.resolvers import resolve_oc_model_queryset from opencontractserver.types.enums import LabelType from opencontractserver.users.models import Assignment, UserExport, UserImport @@ -619,3 +620,152 @@ def resolve_analyses(self, info, **kwargs): return Analysis.objects.filter( Q(creator=info.context.user) | Q(is_public=True) ) + + language_model = relay.Node.Field(LanguageModelType) + + @login_required + def resolve_language_model(self, info, **kwargs): + django_pk = from_global_id(kwargs.get("id", None))[1] + if info.context.user.is_superuser: + return LanguageModel.objects.get(id=django_pk) + elif info.context.user.is_anonymous: + return LanguageModel.objects.get(Q(id=django_pk) & Q(is_public=True)) + else: + return LanguageModel.objects.get( + Q(id=django_pk) & (Q(creator=info.context.user) | Q(is_public=True)) + ) + + language_models = DjangoFilterConnectionField( + LanguageModelType, filterset_class=LanguageModelFilter + ) + + @login_required + def resolve_language_models(self, info, **kwargs): + if info.context.user.is_superuser: + return LanguageModel.objects.all() + elif info.context.user.is_anonymous: + return LanguageModel.objects.filter(Q(is_public=True)) + else: + return LanguageModel.objects.filter( + Q(creator=info.context.user) | Q(is_public=True) + ) + + fieldset = relay.Node.Field(FieldsetType) + + @login_required + def resolve_fieldset(self, info, **kwargs): + django_pk = from_global_id(kwargs.get("id", None))[1] + if info.context.user.is_superuser: + return Fieldset.objects.get(id=django_pk) + elif info.context.user.is_anonymous: + return Fieldset.objects.get(Q(id=django_pk) & Q(is_public=True)) + else: + return Fieldset.objects.get( + Q(id=django_pk) & (Q(owner=info.context.user) | Q(is_public=True)) + ) + + fieldsets = DjangoFilterConnectionField( + FieldsetType, + filterset_class=FieldsetFilter + ) + + @login_required + def resolve_fieldsets(self, info, **kwargs): + if info.context.user.is_superuser: + return Fieldset.objects.all() + elif info.context.user.is_anonymous: + return Fieldset.objects.filter(Q(is_public=True)) + else: + return Fieldset.objects.filter( + Q(owner=info.context.user) | Q(is_public=True) + ) + + column = relay.Node.Field(ColumnType) + + @login_required + def resolve_column(self, info, **kwargs): + django_pk = from_global_id(kwargs.get("id", None))[1] + if info.context.user.is_superuser: + return Column.objects.get(id=django_pk) + elif info.context.user.is_anonymous: + return Column.objects.get(Q(id=django_pk) & Q(is_public=True)) + else: + return Column.objects.get( + Q(id=django_pk) & (Q(fieldset__owner=info.context.user) | Q(is_public=True)) + ) + + columns = DjangoFilterConnectionField( + ColumnType, + filterset_class=ColumnFilter + ) + + @login_required + def resolve_columns(self, info, **kwargs): + if info.context.user.is_superuser: + return Column.objects.all() + elif info.context.user.is_anonymous: + return Column.objects.filter(Q(is_public=True)) + else: + return Column.objects.filter( + Q(fieldset__owner=info.context.user) | Q(is_public=True) + ) + + extract = relay.Node.Field(ExtractType) + + @login_required + def resolve_extract(self, info, **kwargs): + django_pk = from_global_id(kwargs.get("id", None))[1] + if info.context.user.is_superuser: + return Extract.objects.get(id=django_pk) + elif info.context.user.is_anonymous: + return Extract.objects.get(Q(id=django_pk) & Q(is_public=True)) + else: + return Extract.objects.get( + Q(id=django_pk) & (Q(owner=info.context.user) | Q(is_public=True)) + ) + + extracts = DjangoFilterConnectionField( + ExtractType, + filterset_class=ExtractFilter + ) + + @login_required + def resolve_extracts(self, info, **kwargs): + if info.context.user.is_superuser: + return Extract.objects.all() + elif info.context.user.is_anonymous: + return Extract.objects.filter(Q(is_public=True)) + else: + return Extract.objects.filter( + Q(owner=info.context.user) | Q(is_public=True) + ) + + row = relay.Node.Field(RowType) + + @login_required + def resolve_row(self, info, **kwargs): + django_pk = from_global_id(kwargs.get("id", None))[1] + if info.context.user.is_superuser: + return Row.objects.get(id=django_pk) + elif info.context.user.is_anonymous: + return Row.objects.get(Q(id=django_pk) & Q(is_public=True)) + else: + return Row.objects.get( + Q(id=django_pk) & (Q(extract__owner=info.context.user) | Q(is_public=True)) + ) + + rows = DjangoFilterConnectionField( + RowType, + filterset_class=RowFilter + ) + + @login_required + def resolve_rows(self, info, **kwargs): + if info.context.user.is_superuser: + return Row.objects.all() + elif info.context.user.is_anonymous: + return Row.objects.filter(Q(is_public=True)) + else: + return Row.objects.filter( + Q(extract__owner=info.context.user) | Q(is_public=True) + ) diff --git a/config/settings/base.py b/config/settings/base.py index 3e9cd42b..0a07cfbd 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -104,6 +104,7 @@ "opencontractserver.corpuses", "opencontractserver.annotations", "opencontractserver.analyzer", + "opencontractserver.extracts" ] # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps diff --git a/opencontractserver/analyzer/models.py b/opencontractserver/analyzer/models.py index 3411af97..503e5de9 100644 --- a/opencontractserver/analyzer/models.py +++ b/opencontractserver/analyzer/models.py @@ -113,6 +113,17 @@ class AnalyzerGroupObjectPermission(GroupObjectPermissionBase): # Create your models here. class Analysis(BaseOCModel): + """ + Okay, this is duplicative of new Extracts objects... I can probably make this pull double duty + BUT I think the more expeditious approach here is to just start fresh and leave this for now but + Eventually replace it or merge the two concepts. + + For now, the distinction is extracts are not annotating the documents directly but rather tracking where + information is coming from - so we can still jump into the document - but storing extracted information for + export as a csv. + """ + + class Meta: permissions = ( ("create_analysis", "create Analysis"), @@ -134,7 +145,10 @@ class Meta: # Tracking information to tie this back to the OC Analyzer that was used to create it. analyzer = django.db.models.ForeignKey( - Analyzer, null=False, blank=False, on_delete=django.db.models.CASCADE + Analyzer, + null=False, + blank=False, + on_delete=django.db.models.CASCADE ) # For (ok) security on results, the callback for a given analyzer will require a TOKEN header of uuid v4 diff --git a/opencontractserver/extracts/__init__.py b/opencontractserver/extracts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/opencontractserver/extracts/admin.py b/opencontractserver/extracts/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/opencontractserver/extracts/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/opencontractserver/extracts/apps.py b/opencontractserver/extracts/apps.py new file mode 100644 index 00000000..1e8312b4 --- /dev/null +++ b/opencontractserver/extracts/apps.py @@ -0,0 +1,13 @@ +from django.apps import AppConfig + + +class ExtractsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'opencontractserver.extracts' + + def ready(self): + try: + pass + + except ImportError: + pass diff --git a/opencontractserver/extracts/migrations/__init__.py b/opencontractserver/extracts/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/opencontractserver/extracts/models.py b/opencontractserver/extracts/models.py new file mode 100644 index 00000000..1c38c773 --- /dev/null +++ b/opencontractserver/extracts/models.py @@ -0,0 +1,193 @@ +import django +from django.contrib.auth import get_user_model +from django.utils import timezone +from guardian.models import GroupObjectPermissionBase, UserObjectPermissionBase +from pgvector.django import VectorField + +from opencontractserver.corpuses.models import Corpus +from opencontractserver.shared.fields import NullableJSONField +from opencontractserver.shared.Models import BaseOCModel + +User = get_user_model() + + +class LanguageModel(BaseOCModel): + + model = django.db.models.CharField(max_length=256, null=False, blank=False) + + class Meta: + permissions = ( + ("permission_languagemodel", "permission language model"), + ("create_languagemodel", "create language model"), + ("read_languagemodel", "read language model"), + ("update_languagemodel", "update language model"), + ("remove_languagemodel", "delete language model"), + ) + + +class LanguageModelUserObjectPermission(UserObjectPermissionBase): + content_object = django.db.models.ForeignKey( + "LanguageModel", on_delete=django.db.models.CASCADE + ) + + +class LanguageModelGroupObjectPermission(GroupObjectPermissionBase): + content_object = django.db.models.ForeignKey( + "LanguageModel", on_delete=django.db.models.CASCADE + ) + + +class Fieldset(BaseOCModel): + owner = django.db.models.ForeignKey( + User, + related_name="fieldsets", + null=False, + blank=False, + on_delete=django.db.models.CASCADE, + ) + name = django.db.models.CharField(max_length=256, null=False, blank=False) + description = django.db.models.TextField(null=False, blank=False) + + class Meta: + permissions = ( + ("permission_fieldset", "permission fieldset"), + ("create_fieldset", "create fieldset"), + ("read_fieldset", "read fieldset"), + ("update_fieldset", "update fieldset"), + ("remove_fieldset", "delete fieldset"), + ) + + +class FieldsetUserObjectPermission(UserObjectPermissionBase): + content_object = django.db.models.ForeignKey( + "Fieldset", on_delete=django.db.models.CASCADE + ) + + +class FieldsetGroupObjectPermission(GroupObjectPermissionBase): + content_object = django.db.models.ForeignKey( + "Fieldset", on_delete=django.db.models.CASCADE + ) + + +class Column(BaseOCModel): + fieldset = django.db.models.ForeignKey( + "Fieldset", related_name="columns", on_delete=django.db.models.CASCADE + ) + + # TODO - Should set up validations so EITHER of these can be null but not both. + query = django.db.models.TextField(null=False, blank=False) + match_text = django.db.models.TextField(null=False, blank=False) + + output_type = django.db.models.TextField(null=False, blank=False) + limit_to_label = django.db.models.CharField(max_length=512, null=True, blank=True) + instructions = django.db.models.TextField(null=True, blank=True) + language_model = django.db.models.ForeignKey( + "LanguageModel", on_delete=django.db.models.PROTECT, null=False, blank=False + ) + agentic = django.db.models.BooleanField(default=False) + + class Meta: + permissions = ( + ("permission_column", "permission column"), + ("create_column", "create column"), + ("read_column", "read column"), + ("update_column", "update column"), + ("remove_column", "delete column"), + ) + + +class ColumnUserObjectPermission(UserObjectPermissionBase): + content_object = django.db.models.ForeignKey( + "Column", on_delete=django.db.models.CASCADE + ) + + +class ColumnGroupObjectPermission(GroupObjectPermissionBase): + content_object = django.db.models.ForeignKey( + "Column", on_delete=django.db.models.CASCADE + ) + + +class Extract(BaseOCModel): + corpus = django.db.models.ForeignKey( + Corpus, + related_name="extracts", + on_delete=django.db.models.CASCADE, + null=False, + blank=False, + ) + name = django.db.models.CharField(max_length=512, null=False, blank=False) + fieldset = django.db.models.ForeignKey( + "Fieldset", + related_name="extracts", + on_delete=django.db.models.PROTECT, + null=False, + ) + owner = django.db.models.ForeignKey( + django.contrib.auth.get_user_model(), + related_name="owned_extracts", + on_delete=django.db.models.PROTECT, + null=False, + ) + created = django.db.models.DateTimeField(auto_now_add=True) + started = django.db.models.DateTimeField(null=True, blank=True) + finished = django.db.models.DateTimeField(null=True, blank=True) + stacktrace = django.db.models.TextField(null=True, blank=True) + + class Meta: + permissions = ( + ("permission_extract", "permission extract"), + ("create_extract", "create extract"), + ("read_extract", "read extract"), + ("update_extract", "update extract"), + ("remove_extract", "delete extract"), + ) + + +class ExtractUserObjectPermission(UserObjectPermissionBase): + content_object = django.db.models.ForeignKey( + "Extract", on_delete=django.db.models.CASCADE + ) + + +class ExtractGroupObjectPermission(GroupObjectPermissionBase): + content_object = django.db.models.ForeignKey( + "Extract", on_delete=django.db.models.CASCADE + ) + + +class Row(BaseOCModel): + extract = django.db.models.ForeignKey( + "Extract", related_name="rows", on_delete=django.db.models.CASCADE + ) + column = django.db.models.ForeignKey( + "Column", related_name="rows", on_delete=django.db.models.CASCADE + ) + data = NullableJSONField() + data_definition = django.db.models.TextField(null=False, blank=False) + started = django.db.models.DateTimeField(null=True, blank=True) + completed = django.db.models.DateTimeField(null=True, blank=True) + failed = django.db.models.DateTimeField(null=True, blank=True) + stacktrace = django.db.models.TextField(null=True, blank=True) + + class Meta: + permissions = ( + ("permission_row", "permission row"), + ("create_row", "create row"), + ("read_row", "read row"), + ("update_row", "update row"), + ("remove_row", "delete row"), + ) + + +class RowUserObjectPermission(UserObjectPermissionBase): + content_object = django.db.models.ForeignKey( + "Row", on_delete=django.db.models.CASCADE + ) + + +class RowGroupObjectPermission(GroupObjectPermissionBase): + content_object = django.db.models.ForeignKey( + "Row", on_delete=django.db.models.CASCADE + ) diff --git a/opencontractserver/extracts/tests.py b/opencontractserver/extracts/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/opencontractserver/extracts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/opencontractserver/extracts/views.py b/opencontractserver/extracts/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/opencontractserver/extracts/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/opencontractserver/llms/__init__.py b/opencontractserver/llms/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/opencontractserver/tasks/extract_tasks.py b/opencontractserver/tasks/extract_tasks.py new file mode 100644 index 00000000..45d193db --- /dev/null +++ b/opencontractserver/tasks/extract_tasks.py @@ -0,0 +1,79 @@ +import json + +from celery import shared_task +from django.db import transaction + +from opencontractserver.annotations.models import Annotation +from opencontractserver.extracts.models import Extract, Row +from opencontractserver.utils.permissioning import set_permissions_for_obj_to_user +from opencontractserver.types.enums import PermissionTypes + + +# Mock these functions for now +def agent_fetch_my_definitions(annot): + return [] + + +def extract_for_query(annots, query, output_type): + return None + + +@shared_task +def run_extract(extract_id, user_id): + extract = Extract.objects.get(pk=extract_id) + + with transaction.atomic(): + extract.started = timezone.now() + extract.save() + + corpus = extract.corpus + fieldset = extract.fieldset + + document_ids = corpus.documents.values_list('id', flat=True) + + for document_id in document_ids: + for column in fieldset.columns.all(): + with transaction.atomic(): + row = Row.objects.create( + extract=extract, + column=column, + data_definition=column.output_type + ) + set_permissions_for_obj_to_user(user_id, row, [PermissionTypes.CRUD]) + + try: + row.started = timezone.now() + row.save() + + output_type = eval(column.output_type) + + annotations = Annotation.objects.filter( + document_id=document_id, + embedding__isnull=False + ) + + if column.limit_to_label: + annotations = annotations.filter(annotation_label__text=column.limit_to_label) + + match_text = column.match_text or column.query + + if match_text: + # Find closest annotations + annotations = annotations.order_by(L2Distance('embedding', match_text))[:5] + + if column.agentic: + annotations |= agent_fetch_my_definitions(annotations) + + val = extract_for_query( + annotations, + column.query, + output_type + ) + + row.data = json.dumps({"data": val}) + row.completed = timezone.now() + + except Exception as e: + row.stacktrace = f"Error processing: {e}" + row.failed = timezone.now() + row.save() diff --git a/opencontractserver/tests/test_extract_mutations.py b/opencontractserver/tests/test_extract_mutations.py new file mode 100644 index 00000000..61ebb275 --- /dev/null +++ b/opencontractserver/tests/test_extract_mutations.py @@ -0,0 +1,145 @@ +import json +from unittest.mock import patch + +from django.contrib.auth import get_user_model +from django.test import TestCase +from graphene.test import Client +from graphql_relay import to_global_id + +from config.graphql.schema import schema +from opencontractserver.annotations.models import AnnotationLabel +from opencontractserver.corpuses.models import Corpus +from opencontractserver.extracts.models import LanguageModel, Fieldset, Column, Extract, Row +from opencontractserver.tasks.extract_tasks import run_extract + +User = get_user_model() + + +class TestContext: + def __init__(self, user): + self.user = user + +class ExtractsMutationTestCase(TestCase): + def setUp(self): + self.user = User.objects.create_user(username="testuser", password="testpassword") + self.client = Client(schema, context_value=TestContext(self.user)) + + self.corpus = Corpus.objects.create(title="TestCorpus", creator=self.user) + + def test_create_language_model_mutation(self): + mutation = """ + mutation { + createLanguageModel(model: "TestModel") { + ok + languageModel { + id + model + } + } + } + """ + + result = self.client.execute(mutation) + self.assertIsNone(result.get("errors")) + self.assertTrue(result["data"]["createLanguageModel"]["ok"]) + self.assertIsNotNone(result["data"]["createLanguageModel"]["languageModel"]["id"]) + self.assertEqual( + result["data"]["createLanguageModel"]["languageModel"]["model"], "TestModel" + ) + + def test_create_fieldset_mutation(self): + mutation = """ + mutation { + createFieldset(name: "TestFieldset", description: "Test description") { + ok + fieldset { + id + name + description + } + } + } + """ + + result = self.client.execute(mutation) + self.assertIsNone(result.get("errors")) + self.assertTrue(result["data"]["createFieldset"]["ok"]) + self.assertIsNotNone(result["data"]["createFieldset"]["fieldset"]["id"]) + self.assertEqual( + result["data"]["createFieldset"]["fieldset"]["name"], "TestFieldset" + ) + self.assertEqual( + result["data"]["createFieldset"]["fieldset"]["description"], + "Test description", + ) + + def test_create_column_mutation(self): + language_model = LanguageModel.objects.create(model="TestModel") + fieldset = Fieldset.objects.create( + owner=self.user, name="TestFieldset", description="Test description" + ) + + mutation = """ + mutation { + createColumn( + fieldsetId: "%s", + query: "TestQuery", + outputType: "str", + languageModelId: "%s", + agentic: false + ) { + ok + column { + id + query + outputType + agentic + } + } + } + """ % ( + to_global_id("FieldsetType", fieldset.id), + to_global_id("LanguageModelType", language_model.id), + ) + + result = self.client.execute(mutation) + self.assertIsNone(result.get("errors")) + self.assertTrue(result["data"]["createColumn"]["ok"]) + self.assertIsNotNone(result["data"]["createColumn"]["column"]["id"]) + self.assertEqual(result["data"]["createColumn"]["column"]["query"], "TestQuery") + self.assertEqual(result["data"]["createColumn"]["column"]["outputType"], "str") + self.assertEqual(result["data"]["createColumn"]["column"]["agentic"], False) + + def test_start_extract_mutation(self): + fieldset = Fieldset.objects.create( + owner=self.user, name="TestFieldset", description="Test description" + ) + + mutation = """ + mutation { + startExtract( + corpusId: "%s", + name: "TestExtract", + fieldsetId: "%s" + ) { + ok + extract { + id + name + } + } + } + """ % ( + to_global_id("CorpusType", self.corpus.id), + to_global_id("FieldsetType", fieldset.id), + ) + + with patch("opencontractserver.tasks.extract_tasks.run_extract.delay") as mock_task: + result = self.client.execute(mutation) + self.assertIsNone(result.get("errors")) + self.assertTrue(result["data"]["startExtract"]["ok"]) + self.assertIsNotNone(result["data"]["startExtract"]["extract"]["id"]) + self.assertEqual( + result["data"]["startExtract"]["extract"]["name"], "TestExtract" + ) + mock_task.assert_called_once() diff --git a/opencontractserver/tests/test_extract_queries.py b/opencontractserver/tests/test_extract_queries.py new file mode 100644 index 00000000..3d00f5e7 --- /dev/null +++ b/opencontractserver/tests/test_extract_queries.py @@ -0,0 +1,157 @@ +import json +from unittest.mock import patch + +from django.contrib.auth import get_user_model +from django.test import TestCase +from graphene.test import Client +from graphql_relay import to_global_id + +from config.graphql.schema import schema +from opencontractserver.annotations.models import AnnotationLabel +from opencontractserver.corpuses.models import Corpus +from opencontractserver.extracts.models import LanguageModel, Fieldset, Column, Extract, Row +from opencontractserver.tasks.extract_tasks import run_extract + +User = get_user_model() + + +class TestContext: + def __init__(self, user): + self.user = user + + +class ExtractsQueryTestCase(TestCase): + def setUp(self): + self.user = User.objects.create_user(username="testuser", password="testpassword") + self.client = Client(schema, context_value=TestContext(self.user)) + + self.language_model = LanguageModel.objects.create(model="TestModel") + self.fieldset = Fieldset.objects.create( + owner=self.user, name="TestFieldset", description="Test description" + ) + self.column = Column.objects.create( + fieldset=self.fieldset, + query="TestQuery", + output_type="str", + language_model=self.language_model, + agentic=False, + ) + self.corpus = Corpus.objects.create(title="TestCorpus", creator=self.user) + self.extract = Extract.objects.create( + corpus=self.corpus, + name="TestExtract", + fieldset=self.fieldset, + owner=self.user, + ) + self.row = Row.objects.create( + extract=self.extract, + column=self.column, + data={"data": "TestData"}, + data_definition="str", + ) + + def test_language_model_query(self): + query = """ + query { + languageModel(id: "%s") { + id + model + } + } + """ % to_global_id( + "LanguageModelType", self.language_model.id + ) + + result = self.client.execute(query) + self.assertIsNone(result.get("errors")) + self.assertEqual( + result["data"]["languageModel"]["id"], + to_global_id("LanguageModelType", self.language_model.id), + ) + self.assertEqual(result["data"]["languageModel"]["model"], "TestModel") + + def test_fieldset_query(self): + query = """ + query { + fieldset(id: "%s") { + id + name + description + } + } + """ % to_global_id( + "FieldsetType", self.fieldset.id + ) + + result = self.client.execute(query) + self.assertIsNone(result.get("errors")) + self.assertEqual( + result["data"]["fieldset"]["id"], + to_global_id("FieldsetType", self.fieldset.id), + ) + self.assertEqual(result["data"]["fieldset"]["name"], "TestFieldset") + self.assertEqual(result["data"]["fieldset"]["description"], "Test description") + + def test_column_query(self): + query = """ + query { + column(id: "%s") { + id + query + outputType + agentic + } + } + """ % to_global_id( + "ColumnType", self.column.id + ) + + result = self.client.execute(query) + self.assertIsNone(result.get("errors")) + self.assertEqual( + result["data"]["column"]["id"], to_global_id("ColumnType", self.column.id) + ) + self.assertEqual(result["data"]["column"]["query"], "TestQuery") + self.assertEqual(result["data"]["column"]["outputType"], "str") + self.assertEqual(result["data"]["column"]["agentic"], False) + + def test_extract_query(self): + query = """ + query { + extract(id: "%s") { + id + name + } + } + """ % to_global_id( + "ExtractType", self.extract.id + ) + + result = self.client.execute(query) + self.assertIsNone(result.get("errors")) + self.assertEqual( + result["data"]["extract"]["id"], to_global_id("ExtractType", self.extract.id) + ) + self.assertEqual(result["data"]["extract"]["name"], "TestExtract") + + def test_row_query(self): + query = """ + query { + row(id: "%s") { + id + data + dataDefinition + } + } + """ % to_global_id( + "RowType", self.row.id + ) + + result = self.client.execute(query) + self.assertIsNone(result.get("errors")) + self.assertEqual( + result["data"]["row"]["id"], to_global_id("RowType", self.row.id) + ) + self.assertEqual(result["data"]["row"]["data"], {"data": "TestData"}) + self.assertEqual(result["data"]["row"]["dataDefinition"], "str") + diff --git a/opencontractserver/tests/test_extract_tasks.py b/opencontractserver/tests/test_extract_tasks.py new file mode 100644 index 00000000..72a2bcbf --- /dev/null +++ b/opencontractserver/tests/test_extract_tasks.py @@ -0,0 +1,64 @@ +import json +from unittest.mock import patch + +from django.contrib.auth import get_user_model +from django.test import TestCase +from graphene.test import Client +from graphql_relay import to_global_id + +from config.graphql.schema import schema +from opencontractserver.annotations.models import AnnotationLabel +from opencontractserver.corpuses.models import Corpus +from opencontractserver.extracts.models import LanguageModel, Fieldset, Column, Extract, Row +from opencontractserver.tasks.extract_tasks import run_extract + +User = get_user_model() + + +class TestContext: + def __init__(self, user): + self.user = user + +class ExtractsTaskTestCase(TestCase): + def setUp(self): + self.user = User.objects.create_user(username="testuser", password="testpassword") + + self.language_model = LanguageModel.objects.create(model="TestModel") + self.fieldset = Fieldset.objects.create( + owner=self.user, name="TestFieldset", description="Test description" + ) + self.column = Column.objects.create( + fieldset=self.fieldset, + query="TestQuery", + output_type="str", + language_model=self.language_model, + agentic=False, + ) + self.corpus = Corpus.objects.create(title="TestCorpus", creator=self.user) + self.extract = Extract.objects.create( + corpus=self.corpus, + name="TestExtract", + fieldset=self.fieldset, + owner=self.user, + ) + + @patch("opencontractserver.tasks.extract_tasks.agent_fetch_my_definitions") + @patch("opencontractserver.tasks.extract_tasks.extract_for_query") + def test_run_extract_task(self, mock_extract_for_query, mock_agent_fetch_my_definitions): + mock_extract_for_query.return_value = "Mocked extracted data" + mock_agent_fetch_my_definitions.return_value = [] + + run_extract(self.extract.id, self.user.id) + + self.extract.refresh_from_db() + self.assertIsNotNone(self.extract.started) + + row = Row.objects.filter(extract=self.extract, column=self.column).first() + self.assertIsNotNone(row) + self.assertEqual(row.data, json.dumps({"data": "Mocked extracted data"})) + self.assertEqual(row.data_definition, "str") + self.assertIsNotNone(row.started) + self.assertIsNotNone(row.completed) + + mock_extract_for_query.assert_called_once() + mock_agent_fetch_my_definitions.assert_called_once() From 3dd50d8589683083dd9e403d054bdb1ebcef8cab Mon Sep 17 00:00:00 2001 From: JSv4 Date: Sun, 26 May 2024 15:32:20 -0700 Subject: [PATCH 02/68] Ran linter and fixed issues. --- config/graphql/filters.py | 9 +++- config/graphql/graphene_types.py | 9 ++++ config/graphql/mutations.py | 18 ++++--- config/graphql/queries.py | 46 +++++++++------- config/settings/base.py | 2 +- opencontractserver/analyzer/models.py | 6 +-- opencontractserver/extracts/admin.py | 3 +- opencontractserver/extracts/apps.py | 4 +- opencontractserver/extracts/models.py | 2 - opencontractserver/extracts/tests.py | 3 -- opencontractserver/extracts/views.py | 3 -- opencontractserver/tasks/extract_tasks.py | 27 +++++----- .../tests/test_extract_mutations.py | 54 ++++++++++--------- .../tests/test_extract_queries.py | 21 ++++---- .../tests/test_extract_tasks.py | 21 +++++--- 15 files changed, 129 insertions(+), 99 deletions(-) delete mode 100644 opencontractserver/extracts/tests.py delete mode 100644 opencontractserver/extracts/views.py diff --git a/config/graphql/filters.py b/config/graphql/filters.py index 85cd771f..ff391a17 100644 --- a/config/graphql/filters.py +++ b/config/graphql/filters.py @@ -8,7 +8,6 @@ from django_filters import rest_framework as filters from graphql_relay import from_global_id -from opencontractserver.extracts.models import LanguageModel, Fieldset, Column, Extract, Row from opencontractserver.analyzer.models import Analysis, Analyzer, GremlinEngine from opencontractserver.annotations.models import ( Annotation, @@ -18,6 +17,13 @@ ) from opencontractserver.corpuses.models import Corpus from opencontractserver.documents.models import Document +from opencontractserver.extracts.models import ( + Column, + Extract, + Fieldset, + LanguageModel, + Row, +) from opencontractserver.users.models import Assignment, UserExport User = get_user_model() @@ -381,6 +387,7 @@ class Meta: "id": ["exact"], } + class LanguageModelFilter(django_filters.FilterSet): class Meta: model = LanguageModel diff --git a/config/graphql/graphene_types.py b/config/graphql/graphene_types.py index 875d3dac..d0263802 100644 --- a/config/graphql/graphene_types.py +++ b/config/graphql/graphene_types.py @@ -4,6 +4,7 @@ from django.contrib.auth import get_user_model from graphene import relay from graphene.types.generic import GenericScalar +from graphene_django import DjangoObjectType from graphene_django import DjangoObjectType as ModelType from graphene_django.filter import DjangoFilterConnectionField from graphql_relay import from_global_id @@ -20,6 +21,13 @@ ) from opencontractserver.corpuses.models import Corpus from opencontractserver.documents.models import Document +from opencontractserver.extracts.models import ( + Column, + Extract, + Fieldset, + LanguageModel, + Row, +) from opencontractserver.users.models import Assignment, UserExport, UserImport User = get_user_model() @@ -293,6 +301,7 @@ class Meta: interfaces = [relay.Node] connection_class = CountableConnection + class FieldsetType(AnnotatePermissionsForReadMixin, DjangoObjectType): class Meta: model = Fieldset diff --git a/config/graphql/mutations.py b/config/graphql/mutations.py index 18a06db0..a0b1dea8 100644 --- a/config/graphql/mutations.py +++ b/config/graphql/mutations.py @@ -21,9 +21,13 @@ AnalysisType, AnnotationLabelType, AnnotationType, + ColumnType, CorpusType, DocumentType, + ExtractType, + FieldsetType, LabelSetType, + LanguageModelType, RelationInputType, RelationshipType, UserExportType, @@ -45,6 +49,7 @@ ) from opencontractserver.corpuses.models import Corpus, TemporaryFileHandle from opencontractserver.documents.models import Document +from opencontractserver.extracts.models import Column, Extract, Fieldset, LanguageModel from opencontractserver.tasks import ( build_label_lookups_task, burn_doc_annotations, @@ -63,6 +68,7 @@ package_funsd_exports, package_langchain_exports, ) +from opencontractserver.tasks.extract_tasks import run_extract from opencontractserver.tasks.permissioning_tasks import ( make_analysis_public_task, make_corpus_public_task, @@ -1390,6 +1396,7 @@ class Mutation(graphene.ObjectType): delete_analysis = DeleteAnalysisMutation.Field() make_analysis_public = MakeAnalysisPublic.Field() + class CreateLanguageModel(graphene.Mutation): class Arguments: model = graphene.String(required=True) @@ -1456,7 +1463,9 @@ def mutate( instructions=None, ): fieldset = Fieldset.objects.get(pk=from_global_id(fieldset_id)[1]) - language_model = LanguageModel.objects.get(pk=from_global_id(language_model_id)[1]) + language_model = LanguageModel.objects.get( + pk=from_global_id(language_model_id)[1] + ) column = Column( fieldset=fieldset, query=query, @@ -1465,7 +1474,7 @@ def mutate( limit_to_label=limit_to_label, instructions=instructions, language_model=language_model, - agentic=agentic + agentic=agentic, ) column.save() set_permissions_for_obj_to_user( @@ -1490,10 +1499,7 @@ def mutate(root, info, corpus_id, name, fieldset_id): fieldset = Fieldset.objects.get(pk=from_global_id(fieldset_id)[1]) extract = Extract( - corpus=corpus, - name=name, - fieldset=fieldset, - owner=info.context.user + corpus=corpus, name=name, fieldset=fieldset, owner=info.context.user ) extract.save() set_permissions_for_obj_to_user( diff --git a/config/graphql/queries.py b/config/graphql/queries.py index fd98f8e1..ce0de2ce 100644 --- a/config/graphql/queries.py +++ b/config/graphql/queries.py @@ -16,13 +16,18 @@ AnalyzerFilter, AnnotationFilter, AssignmentFilter, + ColumnFilter, CorpusFilter, DocumentFilter, ExportFilter, + ExtractFilter, + FieldsetFilter, GremlinEngineFilter, LabelFilter, LabelsetFilter, - RelationshipFilter, LanguageModelFilter, FieldsetFilter, ColumnFilter, ExtractFilter, RowFilter, + LanguageModelFilter, + RelationshipFilter, + RowFilter, ) from config.graphql.graphene_types import ( AnalysisType, @@ -30,15 +35,20 @@ AnnotationLabelType, AnnotationType, AssignmentType, + ColumnType, CorpusType, DocumentType, + ExtractType, + FieldsetType, GremlinEngineType_READ, LabelSetType, + LanguageModelType, PageAwareAnnotationType, PdfPageInfoType, RelationshipType, + RowType, UserExportType, - UserImportType, LanguageModelType, FieldsetType, ColumnType, ExtractType, RowType, + UserImportType, ) from opencontractserver.analyzer.models import Analysis, Analyzer, GremlinEngine from opencontractserver.annotations.models import ( @@ -49,7 +59,13 @@ ) from opencontractserver.corpuses.models import Corpus from opencontractserver.documents.models import Document -from opencontractserver.extracts.models import LanguageModel, Fieldset, Column, Extract, Row +from opencontractserver.extracts.models import ( + Column, + Extract, + Fieldset, + LanguageModel, + Row, +) from opencontractserver.shared.resolvers import resolve_oc_model_queryset from opencontractserver.types.enums import LabelType from opencontractserver.users.models import Assignment, UserExport, UserImport @@ -665,8 +681,7 @@ def resolve_fieldset(self, info, **kwargs): ) fieldsets = DjangoFilterConnectionField( - FieldsetType, - filterset_class=FieldsetFilter + FieldsetType, filterset_class=FieldsetFilter ) @login_required @@ -691,13 +706,11 @@ def resolve_column(self, info, **kwargs): return Column.objects.get(Q(id=django_pk) & Q(is_public=True)) else: return Column.objects.get( - Q(id=django_pk) & (Q(fieldset__owner=info.context.user) | Q(is_public=True)) + Q(id=django_pk) + & (Q(fieldset__owner=info.context.user) | Q(is_public=True)) ) - columns = DjangoFilterConnectionField( - ColumnType, - filterset_class=ColumnFilter - ) + columns = DjangoFilterConnectionField(ColumnType, filterset_class=ColumnFilter) @login_required def resolve_columns(self, info, **kwargs): @@ -724,10 +737,7 @@ def resolve_extract(self, info, **kwargs): Q(id=django_pk) & (Q(owner=info.context.user) | Q(is_public=True)) ) - extracts = DjangoFilterConnectionField( - ExtractType, - filterset_class=ExtractFilter - ) + extracts = DjangoFilterConnectionField(ExtractType, filterset_class=ExtractFilter) @login_required def resolve_extracts(self, info, **kwargs): @@ -751,13 +761,11 @@ def resolve_row(self, info, **kwargs): return Row.objects.get(Q(id=django_pk) & Q(is_public=True)) else: return Row.objects.get( - Q(id=django_pk) & (Q(extract__owner=info.context.user) | Q(is_public=True)) + Q(id=django_pk) + & (Q(extract__owner=info.context.user) | Q(is_public=True)) ) - rows = DjangoFilterConnectionField( - RowType, - filterset_class=RowFilter - ) + rows = DjangoFilterConnectionField(RowType, filterset_class=RowFilter) @login_required def resolve_rows(self, info, **kwargs): diff --git a/config/settings/base.py b/config/settings/base.py index 0a07cfbd..0aac3201 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -104,7 +104,7 @@ "opencontractserver.corpuses", "opencontractserver.annotations", "opencontractserver.analyzer", - "opencontractserver.extracts" + "opencontractserver.extracts", ] # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps diff --git a/opencontractserver/analyzer/models.py b/opencontractserver/analyzer/models.py index 503e5de9..9c997204 100644 --- a/opencontractserver/analyzer/models.py +++ b/opencontractserver/analyzer/models.py @@ -123,7 +123,6 @@ class Analysis(BaseOCModel): export as a csv. """ - class Meta: permissions = ( ("create_analysis", "create Analysis"), @@ -145,10 +144,7 @@ class Meta: # Tracking information to tie this back to the OC Analyzer that was used to create it. analyzer = django.db.models.ForeignKey( - Analyzer, - null=False, - blank=False, - on_delete=django.db.models.CASCADE + Analyzer, null=False, blank=False, on_delete=django.db.models.CASCADE ) # For (ok) security on results, the callback for a given analyzer will require a TOKEN header of uuid v4 diff --git a/opencontractserver/extracts/admin.py b/opencontractserver/extracts/admin.py index 8c38f3f3..be0528a0 100644 --- a/opencontractserver/extracts/admin.py +++ b/opencontractserver/extracts/admin.py @@ -1,3 +1,2 @@ -from django.contrib import admin - +pass # Register your models here. diff --git a/opencontractserver/extracts/apps.py b/opencontractserver/extracts/apps.py index 1e8312b4..5fa4285b 100644 --- a/opencontractserver/extracts/apps.py +++ b/opencontractserver/extracts/apps.py @@ -2,8 +2,8 @@ class ExtractsConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'opencontractserver.extracts' + default_auto_field = "django.db.models.BigAutoField" + name = "opencontractserver.extracts" def ready(self): try: diff --git a/opencontractserver/extracts/models.py b/opencontractserver/extracts/models.py index 1c38c773..129ffee5 100644 --- a/opencontractserver/extracts/models.py +++ b/opencontractserver/extracts/models.py @@ -1,8 +1,6 @@ import django from django.contrib.auth import get_user_model -from django.utils import timezone from guardian.models import GroupObjectPermissionBase, UserObjectPermissionBase -from pgvector.django import VectorField from opencontractserver.corpuses.models import Corpus from opencontractserver.shared.fields import NullableJSONField diff --git a/opencontractserver/extracts/tests.py b/opencontractserver/extracts/tests.py deleted file mode 100644 index 7ce503c2..00000000 --- a/opencontractserver/extracts/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/opencontractserver/extracts/views.py b/opencontractserver/extracts/views.py deleted file mode 100644 index 91ea44a2..00000000 --- a/opencontractserver/extracts/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. diff --git a/opencontractserver/tasks/extract_tasks.py b/opencontractserver/tasks/extract_tasks.py index 45d193db..c6af8332 100644 --- a/opencontractserver/tasks/extract_tasks.py +++ b/opencontractserver/tasks/extract_tasks.py @@ -2,11 +2,13 @@ from celery import shared_task from django.db import transaction +from django.utils import timezone +from pgvector.django import L2Distance from opencontractserver.annotations.models import Annotation from opencontractserver.extracts.models import Extract, Row -from opencontractserver.utils.permissioning import set_permissions_for_obj_to_user from opencontractserver.types.enums import PermissionTypes +from opencontractserver.utils.permissioning import set_permissions_for_obj_to_user # Mock these functions for now @@ -29,15 +31,13 @@ def run_extract(extract_id, user_id): corpus = extract.corpus fieldset = extract.fieldset - document_ids = corpus.documents.values_list('id', flat=True) + document_ids = corpus.documents.values_list("id", flat=True) for document_id in document_ids: for column in fieldset.columns.all(): with transaction.atomic(): row = Row.objects.create( - extract=extract, - column=column, - data_definition=column.output_type + extract=extract, column=column, data_definition=column.output_type ) set_permissions_for_obj_to_user(user_id, row, [PermissionTypes.CRUD]) @@ -48,27 +48,26 @@ def run_extract(extract_id, user_id): output_type = eval(column.output_type) annotations = Annotation.objects.filter( - document_id=document_id, - embedding__isnull=False + document_id=document_id, embedding__isnull=False ) if column.limit_to_label: - annotations = annotations.filter(annotation_label__text=column.limit_to_label) + annotations = annotations.filter( + annotation_label__text=column.limit_to_label + ) match_text = column.match_text or column.query if match_text: # Find closest annotations - annotations = annotations.order_by(L2Distance('embedding', match_text))[:5] + annotations = annotations.order_by( + L2Distance("embedding", match_text) + )[:5] if column.agentic: annotations |= agent_fetch_my_definitions(annotations) - val = extract_for_query( - annotations, - column.query, - output_type - ) + val = extract_for_query(annotations, column.query, output_type) row.data = json.dumps({"data": val}) row.completed = timezone.now() diff --git a/opencontractserver/tests/test_extract_mutations.py b/opencontractserver/tests/test_extract_mutations.py index 61ebb275..f4f12ac5 100644 --- a/opencontractserver/tests/test_extract_mutations.py +++ b/opencontractserver/tests/test_extract_mutations.py @@ -1,4 +1,3 @@ -import json from unittest.mock import patch from django.contrib.auth import get_user_model @@ -7,10 +6,8 @@ from graphql_relay import to_global_id from config.graphql.schema import schema -from opencontractserver.annotations.models import AnnotationLabel from opencontractserver.corpuses.models import Corpus -from opencontractserver.extracts.models import LanguageModel, Fieldset, Column, Extract, Row -from opencontractserver.tasks.extract_tasks import run_extract +from opencontractserver.extracts.models import Fieldset, LanguageModel User = get_user_model() @@ -19,9 +16,12 @@ class TestContext: def __init__(self, user): self.user = user + class ExtractsMutationTestCase(TestCase): def setUp(self): - self.user = User.objects.create_user(username="testuser", password="testpassword") + self.user = User.objects.create_user( + username="testuser", password="testpassword" + ) self.client = Client(schema, context_value=TestContext(self.user)) self.corpus = Corpus.objects.create(title="TestCorpus", creator=self.user) @@ -42,7 +42,9 @@ def test_create_language_model_mutation(self): result = self.client.execute(mutation) self.assertIsNone(result.get("errors")) self.assertTrue(result["data"]["createLanguageModel"]["ok"]) - self.assertIsNotNone(result["data"]["createLanguageModel"]["languageModel"]["id"]) + self.assertIsNotNone( + result["data"]["createLanguageModel"]["languageModel"]["id"] + ) self.assertEqual( result["data"]["createLanguageModel"]["languageModel"]["model"], "TestModel" ) @@ -80,24 +82,24 @@ def test_create_column_mutation(self): ) mutation = """ - mutation { + mutation {{ createColumn( - fieldsetId: "%s", + fieldsetId: "{}", query: "TestQuery", outputType: "str", - languageModelId: "%s", + languageModelId: "{}", agentic: false - ) { + ) {{ ok - column { + column {{ id query outputType agentic - } - } - } - """ % ( + }} + }} + }} + """.format( to_global_id("FieldsetType", fieldset.id), to_global_id("LanguageModelType", language_model.id), ) @@ -116,25 +118,27 @@ def test_start_extract_mutation(self): ) mutation = """ - mutation { + mutation {{ startExtract( - corpusId: "%s", + corpusId: "{}", name: "TestExtract", - fieldsetId: "%s" - ) { + fieldsetId: "{}" + ) {{ ok - extract { + extract {{ id name - } - } - } - """ % ( + }} + }} + }} + """.format( to_global_id("CorpusType", self.corpus.id), to_global_id("FieldsetType", fieldset.id), ) - with patch("opencontractserver.tasks.extract_tasks.run_extract.delay") as mock_task: + with patch( + "opencontractserver.tasks.extract_tasks.run_extract.delay" + ) as mock_task: result = self.client.execute(mutation) self.assertIsNone(result.get("errors")) self.assertTrue(result["data"]["startExtract"]["ok"]) diff --git a/opencontractserver/tests/test_extract_queries.py b/opencontractserver/tests/test_extract_queries.py index 3d00f5e7..54a636d7 100644 --- a/opencontractserver/tests/test_extract_queries.py +++ b/opencontractserver/tests/test_extract_queries.py @@ -1,16 +1,17 @@ -import json -from unittest.mock import patch - from django.contrib.auth import get_user_model from django.test import TestCase from graphene.test import Client from graphql_relay import to_global_id from config.graphql.schema import schema -from opencontractserver.annotations.models import AnnotationLabel from opencontractserver.corpuses.models import Corpus -from opencontractserver.extracts.models import LanguageModel, Fieldset, Column, Extract, Row -from opencontractserver.tasks.extract_tasks import run_extract +from opencontractserver.extracts.models import ( + Column, + Extract, + Fieldset, + LanguageModel, + Row, +) User = get_user_model() @@ -22,7 +23,9 @@ def __init__(self, user): class ExtractsQueryTestCase(TestCase): def setUp(self): - self.user = User.objects.create_user(username="testuser", password="testpassword") + self.user = User.objects.create_user( + username="testuser", password="testpassword" + ) self.client = Client(schema, context_value=TestContext(self.user)) self.language_model = LanguageModel.objects.create(model="TestModel") @@ -130,7 +133,8 @@ def test_extract_query(self): result = self.client.execute(query) self.assertIsNone(result.get("errors")) self.assertEqual( - result["data"]["extract"]["id"], to_global_id("ExtractType", self.extract.id) + result["data"]["extract"]["id"], + to_global_id("ExtractType", self.extract.id), ) self.assertEqual(result["data"]["extract"]["name"], "TestExtract") @@ -154,4 +158,3 @@ def test_row_query(self): ) self.assertEqual(result["data"]["row"]["data"], {"data": "TestData"}) self.assertEqual(result["data"]["row"]["dataDefinition"], "str") - diff --git a/opencontractserver/tests/test_extract_tasks.py b/opencontractserver/tests/test_extract_tasks.py index 72a2bcbf..e45812d0 100644 --- a/opencontractserver/tests/test_extract_tasks.py +++ b/opencontractserver/tests/test_extract_tasks.py @@ -3,13 +3,15 @@ from django.contrib.auth import get_user_model from django.test import TestCase -from graphene.test import Client -from graphql_relay import to_global_id -from config.graphql.schema import schema -from opencontractserver.annotations.models import AnnotationLabel from opencontractserver.corpuses.models import Corpus -from opencontractserver.extracts.models import LanguageModel, Fieldset, Column, Extract, Row +from opencontractserver.extracts.models import ( + Column, + Extract, + Fieldset, + LanguageModel, + Row, +) from opencontractserver.tasks.extract_tasks import run_extract User = get_user_model() @@ -19,9 +21,12 @@ class TestContext: def __init__(self, user): self.user = user + class ExtractsTaskTestCase(TestCase): def setUp(self): - self.user = User.objects.create_user(username="testuser", password="testpassword") + self.user = User.objects.create_user( + username="testuser", password="testpassword" + ) self.language_model = LanguageModel.objects.create(model="TestModel") self.fieldset = Fieldset.objects.create( @@ -44,7 +49,9 @@ def setUp(self): @patch("opencontractserver.tasks.extract_tasks.agent_fetch_my_definitions") @patch("opencontractserver.tasks.extract_tasks.extract_for_query") - def test_run_extract_task(self, mock_extract_for_query, mock_agent_fetch_my_definitions): + def test_run_extract_task( + self, mock_extract_for_query, mock_agent_fetch_my_definitions + ): mock_extract_for_query.return_value = "Mocked extracted data" mock_agent_fetch_my_definitions.return_value = [] From b9d43a3e6cf8527e8aa8cd8bdeaae86cda23c29d Mon Sep 17 00:00:00 2001 From: JSv4 Date: Sun, 26 May 2024 18:01:18 -0700 Subject: [PATCH 03/68] Added migrations for extract app. --- .../extracts/migrations/0001_initial.py | 276 ++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 opencontractserver/extracts/migrations/0001_initial.py diff --git a/opencontractserver/extracts/migrations/0001_initial.py b/opencontractserver/extracts/migrations/0001_initial.py new file mode 100644 index 00000000..74efcea3 --- /dev/null +++ b/opencontractserver/extracts/migrations/0001_initial.py @@ -0,0 +1,276 @@ +# Generated by Django 3.2.9 on 2024-05-27 01:01 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import opencontractserver.shared.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('corpuses', '0002_initial'), + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='Column', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('backend_lock', models.BooleanField(default=False)), + ('is_public', models.BooleanField(default=False)), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('query', models.TextField()), + ('match_text', models.TextField()), + ('output_type', models.TextField()), + ('limit_to_label', models.CharField(blank=True, max_length=512, null=True)), + ('instructions', models.TextField(blank=True, null=True)), + ('agentic', models.BooleanField(default=False)), + ('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'permissions': (('permission_column', 'permission column'), ('create_column', 'create column'), ('read_column', 'read column'), ('update_column', 'update column'), ('remove_column', 'delete column')), + }, + ), + migrations.CreateModel( + name='Extract', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('backend_lock', models.BooleanField(default=False)), + ('is_public', models.BooleanField(default=False)), + ('modified', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=512)), + ('created', models.DateTimeField(auto_now_add=True)), + ('started', models.DateTimeField(blank=True, null=True)), + ('finished', models.DateTimeField(blank=True, null=True)), + ('stacktrace', models.TextField(blank=True, null=True)), + ('corpus', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extracts', to='corpuses.corpus')), + ('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'permissions': (('permission_extract', 'permission extract'), ('create_extract', 'create extract'), ('read_extract', 'read extract'), ('update_extract', 'update extract'), ('remove_extract', 'delete extract')), + }, + ), + migrations.CreateModel( + name='Fieldset', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('backend_lock', models.BooleanField(default=False)), + ('is_public', models.BooleanField(default=False)), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=256)), + ('description', models.TextField()), + ('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fieldsets', to=settings.AUTH_USER_MODEL)), + ('user_lock', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='locked_fieldset_objects', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'permissions': (('permission_fieldset', 'permission fieldset'), ('create_fieldset', 'create fieldset'), ('read_fieldset', 'read fieldset'), ('update_fieldset', 'update fieldset'), ('remove_fieldset', 'delete fieldset')), + }, + ), + migrations.CreateModel( + name='LanguageModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('backend_lock', models.BooleanField(default=False)), + ('is_public', models.BooleanField(default=False)), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('model', models.CharField(max_length=256)), + ('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('user_lock', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='locked_languagemodel_objects', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'permissions': (('permission_languagemodel', 'permission language model'), ('create_languagemodel', 'create language model'), ('read_languagemodel', 'read language model'), ('update_languagemodel', 'update language model'), ('remove_languagemodel', 'delete language model')), + }, + ), + migrations.CreateModel( + name='Row', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('backend_lock', models.BooleanField(default=False)), + ('is_public', models.BooleanField(default=False)), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('data', opencontractserver.shared.fields.NullableJSONField()), + ('data_definition', models.TextField()), + ('started', models.DateTimeField(blank=True, null=True)), + ('completed', models.DateTimeField(blank=True, null=True)), + ('failed', models.DateTimeField(blank=True, null=True)), + ('stacktrace', models.TextField(blank=True, null=True)), + ('column', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rows', to='extracts.column')), + ('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('extract', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rows', to='extracts.extract')), + ('user_lock', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='locked_row_objects', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'permissions': (('permission_row', 'permission row'), ('create_row', 'create row'), ('read_row', 'read row'), ('update_row', 'update row'), ('remove_row', 'delete row')), + }, + ), + migrations.AddField( + model_name='extract', + name='fieldset', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='extracts', to='extracts.fieldset'), + ), + migrations.AddField( + model_name='extract', + name='owner', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='owned_extracts', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='extract', + name='user_lock', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='locked_extract_objects', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='column', + name='fieldset', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='columns', to='extracts.fieldset'), + ), + migrations.AddField( + model_name='column', + name='language_model', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='extracts.languagemodel'), + ), + migrations.AddField( + model_name='column', + name='user_lock', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='locked_column_objects', to=settings.AUTH_USER_MODEL), + ), + migrations.CreateModel( + name='RowUserObjectPermission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.row')), + ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + 'unique_together': {('user', 'permission', 'content_object')}, + }, + ), + migrations.CreateModel( + name='RowGroupObjectPermission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.row')), + ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.group')), + ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')), + ], + options={ + 'abstract': False, + 'unique_together': {('group', 'permission', 'content_object')}, + }, + ), + migrations.CreateModel( + name='LanguageModelUserObjectPermission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.languagemodel')), + ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + 'unique_together': {('user', 'permission', 'content_object')}, + }, + ), + migrations.CreateModel( + name='LanguageModelGroupObjectPermission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.languagemodel')), + ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.group')), + ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')), + ], + options={ + 'abstract': False, + 'unique_together': {('group', 'permission', 'content_object')}, + }, + ), + migrations.CreateModel( + name='FieldsetUserObjectPermission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.fieldset')), + ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + 'unique_together': {('user', 'permission', 'content_object')}, + }, + ), + migrations.CreateModel( + name='FieldsetGroupObjectPermission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.fieldset')), + ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.group')), + ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')), + ], + options={ + 'abstract': False, + 'unique_together': {('group', 'permission', 'content_object')}, + }, + ), + migrations.CreateModel( + name='ExtractUserObjectPermission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.extract')), + ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + 'unique_together': {('user', 'permission', 'content_object')}, + }, + ), + migrations.CreateModel( + name='ExtractGroupObjectPermission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.extract')), + ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.group')), + ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')), + ], + options={ + 'abstract': False, + 'unique_together': {('group', 'permission', 'content_object')}, + }, + ), + migrations.CreateModel( + name='ColumnUserObjectPermission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.column')), + ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + 'unique_together': {('user', 'permission', 'content_object')}, + }, + ), + migrations.CreateModel( + name='ColumnGroupObjectPermission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.column')), + ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.group')), + ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')), + ], + options={ + 'abstract': False, + 'unique_together': {('group', 'permission', 'content_object')}, + }, + ), + ] From 3f8f64d34bd688721ec544359895ea48a944285b Mon Sep 17 00:00:00 2001 From: JSv4 Date: Sun, 26 May 2024 21:45:40 -0700 Subject: [PATCH 04/68] Number of tests resolved. --- config/graphql/graphene_types.py | 3 + config/graphql/mutations.py | 166 ++++++++++-------- .../migrations/0002_auto_20240527_0445.py | 23 +++ opencontractserver/extracts/models.py | 4 +- .../tests/test_extract_mutations.py | 15 +- .../tests/test_extract_queries.py | 13 +- 6 files changed, 143 insertions(+), 81 deletions(-) create mode 100644 opencontractserver/extracts/migrations/0002_auto_20240527_0445.py diff --git a/config/graphql/graphene_types.py b/config/graphql/graphene_types.py index d0263802..228095eb 100644 --- a/config/graphql/graphene_types.py +++ b/config/graphql/graphene_types.py @@ -324,6 +324,9 @@ class Meta: class RowType(AnnotatePermissionsForReadMixin, DjangoObjectType): + + data = GenericScalar() + class Meta: model = Row interfaces = [relay.Node] diff --git a/config/graphql/mutations.py b/config/graphql/mutations.py index a0b1dea8..e03e4846 100644 --- a/config/graphql/mutations.py +++ b/config/graphql/mutations.py @@ -1282,7 +1282,6 @@ def mutate(root, info, corpus_id, analyzer_id): class DeleteAnalysisMutation(graphene.Mutation): - ok = graphene.Boolean() message = graphene.String() @@ -1329,74 +1328,6 @@ def resolve(cls, root, info, **kwargs): return cls(user=info.context.user) -class Mutation(graphene.ObjectType): - # TOKEN MUTATIONS (IF WE'RE NOT OUTSOURCING JWT CREATION TO AUTH0) ####### - if not settings.USE_AUTH0: - token_auth = ObtainJSONWebTokenWithUser.Field() - else: - token_auth = graphql_jwt.ObtainJSONWebToken.Field() - - verify_token = graphql_jwt.Verify.Field() - refresh_token = graphql_jwt.Refresh.Field() - - # ANNOTATION MUTATIONS ###################################################### - add_annotation = AddAnnotation.Field() - remove_annotation = RemoveAnnotation.Field() - update_annotation = UpdateAnnotation.Field() - add_doc_type_annotation = AddDocTypeAnnotation.Field() - remove_doc_type_annotation = RemoveAnnotation.Field() - - # RELATIONSHIP MUTATIONS ##################################################### - add_relationship = AddRelationship.Field() - remove_relationship = RemoveRelationship.Field() - remove_relationships = RemoveRelationships.Field() - update_relationships = UpdateRelations.Field() - - # LABELSET MUTATIONS ####################################################### - create_labelset = CreateLabelset.Field() - update_labelset = UpdateLabelset.Field() - delete_labelset = DeleteLabelset.Field() - - # LABEL MUTATIONS ########################################################## - create_annotation_label = CreateLabelMutation.Field() - update_annotation_label = UpdateLabelMutation.Field() - delete_annotation_label = DeleteLabelMutation.Field() - delete_multiple_annotation_labels = DeleteMultipleLabelMutation.Field() - create_annotation_label_for_labelset = CreateLabelForLabelsetMutation.Field() - remove_annotation_labels_from_labelset = RemoveLabelsFromLabelsetMutation.Field() - - # DOCUMENT MUTATIONS ####################################################### - upload_document = UploadDocument.Field() # Limited by user.is_usage_capped - update_document = UpdateDocument.Field() - delete_document = DeleteDocument.Field() - export_document = StartDocumentExport.Field() - delete_multiple_documents = DeleteMultipleDocuments.Field() - - # CORPUS MUTATIONS ######################################################### - fork_corpus = StartCorpusFork.Field() - make_corpus_public = MakeCorpusPublic.Field() - create_corpus = CreateCorpusMutation.Field() - update_corpus = UpdateCorpusMutation.Field() - delete_corpus = DeleteCorpusMutation.Field() - link_documents_to_corpus = AddDocumentsToCorpus.Field() - remove_documents_from_corpus = RemoveDocumentsFromCorpus.Field() - - # IMPORT MUTATIONS ######################################################### - import_open_contracts_zip = UploadCorpusImportZip.Field() - import_annotated_doc_to_corpus = UploadAnnotatedDocument.Field() - - # EXPORT MUTATIONS ######################################################### - export_corpus = StartCorpusExport.Field() # Limited by user.is_usage_capped - delete_export = DeleteExport.Field() - - # ANALYSIS MUTATIONS ######################################################### - start_analysis_on_corpus = ( - StartCorpusAnalysisMutation.Field() - ) # Limited by user.is_usage_capped - delete_analysis = DeleteAnalysisMutation.Field() - make_analysis_public = MakeAnalysisPublic.Field() - - class CreateLanguageModel(graphene.Mutation): class Arguments: model = graphene.String(required=True) @@ -1407,7 +1338,10 @@ class Arguments: @staticmethod @login_required def mutate(root, info, model): - language_model = LanguageModel(model=model) + language_model = LanguageModel( + model=model, + creator=info.context.user + ) language_model.save() set_permissions_for_obj_to_user( info.context.user, language_model, [PermissionTypes.CRUD] @@ -1426,7 +1360,12 @@ class Arguments: @staticmethod @login_required def mutate(root, info, name, description): - fieldset = Fieldset(owner=info.context.user, name=name, description=description) + fieldset = Fieldset( + owner=info.context.user, + name=name, + description=description, + creator=info.context.user + ) fieldset.save() set_permissions_for_obj_to_user( info.context.user, fieldset, [PermissionTypes.CRUD] @@ -1462,7 +1401,9 @@ def mutate( limit_to_label=None, instructions=None, ): - fieldset = Fieldset.objects.get(pk=from_global_id(fieldset_id)[1]) + fieldset = Fieldset.objects.get( + pk=from_global_id(fieldset_id)[1] + ) language_model = LanguageModel.objects.get( pk=from_global_id(language_model_id)[1] ) @@ -1475,6 +1416,7 @@ def mutate( instructions=instructions, language_model=language_model, agentic=agentic, + creator=info.context.user ) column.save() set_permissions_for_obj_to_user( @@ -1499,7 +1441,11 @@ def mutate(root, info, corpus_id, name, fieldset_id): fieldset = Fieldset.objects.get(pk=from_global_id(fieldset_id)[1]) extract = Extract( - corpus=corpus, name=name, fieldset=fieldset, owner=info.context.user + corpus=corpus, + name=name, + fieldset=fieldset, + owner=info.context.user, + creator=info.context.user ) extract.save() set_permissions_for_obj_to_user( @@ -1510,3 +1456,77 @@ def mutate(root, info, corpus_id, name, fieldset_id): run_extract.delay(extract.id, info.context.user.id) return StartExtract(ok=True, extract=extract) + + +class Mutation(graphene.ObjectType): + # TOKEN MUTATIONS (IF WE'RE NOT OUTSOURCING JWT CREATION TO AUTH0) ####### + if not settings.USE_AUTH0: + token_auth = ObtainJSONWebTokenWithUser.Field() + else: + token_auth = graphql_jwt.ObtainJSONWebToken.Field() + + verify_token = graphql_jwt.Verify.Field() + refresh_token = graphql_jwt.Refresh.Field() + + # ANNOTATION MUTATIONS ###################################################### + add_annotation = AddAnnotation.Field() + remove_annotation = RemoveAnnotation.Field() + update_annotation = UpdateAnnotation.Field() + add_doc_type_annotation = AddDocTypeAnnotation.Field() + remove_doc_type_annotation = RemoveAnnotation.Field() + + # RELATIONSHIP MUTATIONS ##################################################### + add_relationship = AddRelationship.Field() + remove_relationship = RemoveRelationship.Field() + remove_relationships = RemoveRelationships.Field() + update_relationships = UpdateRelations.Field() + + # LABELSET MUTATIONS ####################################################### + create_labelset = CreateLabelset.Field() + update_labelset = UpdateLabelset.Field() + delete_labelset = DeleteLabelset.Field() + + # LABEL MUTATIONS ########################################################## + create_annotation_label = CreateLabelMutation.Field() + update_annotation_label = UpdateLabelMutation.Field() + delete_annotation_label = DeleteLabelMutation.Field() + delete_multiple_annotation_labels = DeleteMultipleLabelMutation.Field() + create_annotation_label_for_labelset = CreateLabelForLabelsetMutation.Field() + remove_annotation_labels_from_labelset = RemoveLabelsFromLabelsetMutation.Field() + + # DOCUMENT MUTATIONS ####################################################### + upload_document = UploadDocument.Field() # Limited by user.is_usage_capped + update_document = UpdateDocument.Field() + delete_document = DeleteDocument.Field() + export_document = StartDocumentExport.Field() + delete_multiple_documents = DeleteMultipleDocuments.Field() + + # CORPUS MUTATIONS ######################################################### + fork_corpus = StartCorpusFork.Field() + make_corpus_public = MakeCorpusPublic.Field() + create_corpus = CreateCorpusMutation.Field() + update_corpus = UpdateCorpusMutation.Field() + delete_corpus = DeleteCorpusMutation.Field() + link_documents_to_corpus = AddDocumentsToCorpus.Field() + remove_documents_from_corpus = RemoveDocumentsFromCorpus.Field() + + # IMPORT MUTATIONS ######################################################### + import_open_contracts_zip = UploadCorpusImportZip.Field() + import_annotated_doc_to_corpus = UploadAnnotatedDocument.Field() + + # EXPORT MUTATIONS ######################################################### + export_corpus = StartCorpusExport.Field() # Limited by user.is_usage_capped + delete_export = DeleteExport.Field() + + # ANALYSIS MUTATIONS ######################################################### + start_analysis_on_corpus = ( + StartCorpusAnalysisMutation.Field() + ) # Limited by user.is_usage_capped + delete_analysis = DeleteAnalysisMutation.Field() + make_analysis_public = MakeAnalysisPublic.Field() + + # EXTRACT MUTATIONS ########################################################## + create_language_model = CreateLanguageModel.Field() + create_fieldset = CreateFieldset.Field() + create_column = CreateColumn.Field() + start_extract = StartExtract.Field() diff --git a/opencontractserver/extracts/migrations/0002_auto_20240527_0445.py b/opencontractserver/extracts/migrations/0002_auto_20240527_0445.py new file mode 100644 index 00000000..965b479a --- /dev/null +++ b/opencontractserver/extracts/migrations/0002_auto_20240527_0445.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.9 on 2024-05-27 04:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('extracts', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='column', + name='match_text', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='column', + name='query', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/opencontractserver/extracts/models.py b/opencontractserver/extracts/models.py index 129ffee5..3cdc35c5 100644 --- a/opencontractserver/extracts/models.py +++ b/opencontractserver/extracts/models.py @@ -74,8 +74,8 @@ class Column(BaseOCModel): ) # TODO - Should set up validations so EITHER of these can be null but not both. - query = django.db.models.TextField(null=False, blank=False) - match_text = django.db.models.TextField(null=False, blank=False) + query = django.db.models.TextField(null=True, blank=True) + match_text = django.db.models.TextField(null=True, blank=True) output_type = django.db.models.TextField(null=False, blank=False) limit_to_label = django.db.models.CharField(max_length=512, null=True, blank=True) diff --git a/opencontractserver/tests/test_extract_mutations.py b/opencontractserver/tests/test_extract_mutations.py index f4f12ac5..cd25e2c0 100644 --- a/opencontractserver/tests/test_extract_mutations.py +++ b/opencontractserver/tests/test_extract_mutations.py @@ -20,7 +20,8 @@ def __init__(self, user): class ExtractsMutationTestCase(TestCase): def setUp(self): self.user = User.objects.create_user( - username="testuser", password="testpassword" + username="testuser", + password="testpassword" ) self.client = Client(schema, context_value=TestContext(self.user)) @@ -76,9 +77,12 @@ def test_create_fieldset_mutation(self): ) def test_create_column_mutation(self): - language_model = LanguageModel.objects.create(model="TestModel") + language_model = LanguageModel.objects.create(model="TestModel", creator=self.user) fieldset = Fieldset.objects.create( - owner=self.user, name="TestFieldset", description="Test description" + owner=self.user, + name="TestFieldset", + description="Test description", + creator=self.user ) mutation = """ @@ -114,7 +118,10 @@ def test_create_column_mutation(self): def test_start_extract_mutation(self): fieldset = Fieldset.objects.create( - owner=self.user, name="TestFieldset", description="Test description" + owner=self.user, + name="TestFieldset", + description="Test description", + creator=self.user ) mutation = """ diff --git a/opencontractserver/tests/test_extract_queries.py b/opencontractserver/tests/test_extract_queries.py index 54a636d7..d64eb4e0 100644 --- a/opencontractserver/tests/test_extract_queries.py +++ b/opencontractserver/tests/test_extract_queries.py @@ -28,11 +28,18 @@ def setUp(self): ) self.client = Client(schema, context_value=TestContext(self.user)) - self.language_model = LanguageModel.objects.create(model="TestModel") + self.language_model = LanguageModel.objects.create( + model="TestModel", + creator=self.user + ) self.fieldset = Fieldset.objects.create( - owner=self.user, name="TestFieldset", description="Test description" + owner=self.user, + name="TestFieldset", + description="Test description", + creator=self.user ) self.column = Column.objects.create( + creator=self.user, fieldset=self.fieldset, query="TestQuery", output_type="str", @@ -45,12 +52,14 @@ def setUp(self): name="TestExtract", fieldset=self.fieldset, owner=self.user, + creator=self.user ) self.row = Row.objects.create( extract=self.extract, column=self.column, data={"data": "TestData"}, data_definition="str", + creator=self.user ) def test_language_model_query(self): From 2c408590102ec396d328e69ef64872a65cae93fb Mon Sep 17 00:00:00 2001 From: JSv4 Date: Sun, 26 May 2024 22:43:26 -0700 Subject: [PATCH 05/68] Initial extract tests working. --- opencontractserver/extracts/models.py | 3 +- opencontractserver/tasks/extract_tasks.py | 35 +++++++++++--- .../tests/test_extract_tasks.py | 46 ++++++++++++++++--- test.yml | 2 + 4 files changed, 72 insertions(+), 14 deletions(-) diff --git a/opencontractserver/extracts/models.py b/opencontractserver/extracts/models.py index 3cdc35c5..74f1fc9e 100644 --- a/opencontractserver/extracts/models.py +++ b/opencontractserver/extracts/models.py @@ -3,6 +3,7 @@ from guardian.models import GroupObjectPermissionBase, UserObjectPermissionBase from opencontractserver.corpuses.models import Corpus +from opencontractserver.shared.defaults import jsonfield_default_value from opencontractserver.shared.fields import NullableJSONField from opencontractserver.shared.Models import BaseOCModel @@ -162,7 +163,7 @@ class Row(BaseOCModel): column = django.db.models.ForeignKey( "Column", related_name="rows", on_delete=django.db.models.CASCADE ) - data = NullableJSONField() + data = NullableJSONField(default=jsonfield_default_value, null=True, blank=True) data_definition = django.db.models.TextField(null=False, blank=False) started = django.db.models.DateTimeField(null=True, blank=True) completed = django.db.models.DateTimeField(null=True, blank=True) diff --git a/opencontractserver/tasks/extract_tasks.py b/opencontractserver/tasks/extract_tasks.py index c6af8332..d078782e 100644 --- a/opencontractserver/tasks/extract_tasks.py +++ b/opencontractserver/tasks/extract_tasks.py @@ -9,6 +9,7 @@ from opencontractserver.extracts.models import Extract, Row from opencontractserver.types.enums import PermissionTypes from opencontractserver.utils.permissioning import set_permissions_for_obj_to_user +from opencontractserver.utils.embeddings import calculate_embedding_for_text # Mock these functions for now @@ -17,11 +18,15 @@ def agent_fetch_my_definitions(annot): def extract_for_query(annots, query, output_type): + print(f"Ran extract_for_query") return None @shared_task def run_extract(extract_id, user_id): + + print(f"Run extract for extract {extract_id}") + extract = Extract.objects.get(pk=extract_id) with transaction.atomic(): @@ -31,21 +36,30 @@ def run_extract(extract_id, user_id): corpus = extract.corpus fieldset = extract.fieldset - document_ids = corpus.documents.values_list("id", flat=True) + document_ids = corpus.documents.all().values_list("id", flat=True) + print(f"Document ids: {document_ids}") for document_id in document_ids: for column in fieldset.columns.all(): + + print(f"Processing column {column} for doc {document_id}") + with transaction.atomic(): row = Row.objects.create( - extract=extract, column=column, data_definition=column.output_type + extract=extract, + column=column, + data_definition=column.output_type, + creator_id=user_id ) set_permissions_for_obj_to_user(user_id, row, [PermissionTypes.CRUD]) try: + print(f"run_extract() - processing column {column} for {document_id}") row.started = timezone.now() row.save() output_type = eval(column.output_type) + print(f"output_type: {output_type}") annotations = Annotation.objects.filter( document_id=document_id, embedding__isnull=False @@ -57,22 +71,31 @@ def run_extract(extract_id, user_id): ) match_text = column.match_text or column.query + print(f"Match_text: {match_text}") if match_text: - # Find closest annotations + + # need to generate embeddings here + natural_lang_embeddings = calculate_embedding_for_text(match_text) + + # Find closest 5 annotations annotations = annotations.order_by( - L2Distance("embedding", match_text) + L2Distance("embedding", natural_lang_embeddings) )[:5] if column.agentic: - annotations |= agent_fetch_my_definitions(annotations) + annotations = annotations.union(agent_fetch_my_definitions(annotations)) + print(f"Prepare to extract_for_query annotations {annotations} / column {column.query} / {output_type}") val = extract_for_query(annotations, column.query, output_type) + print(f"Extracted value: {val}") - row.data = json.dumps({"data": val}) + row.data = {"data": val} row.completed = timezone.now() + row.save() except Exception as e: + print(f"Ran into error: {e}") row.stacktrace = f"Error processing: {e}" row.failed = timezone.now() row.save() diff --git a/opencontractserver/tests/test_extract_tasks.py b/opencontractserver/tests/test_extract_tasks.py index e45812d0..17d4caff 100644 --- a/opencontractserver/tests/test_extract_tasks.py +++ b/opencontractserver/tests/test_extract_tasks.py @@ -2,9 +2,12 @@ from unittest.mock import patch from django.contrib.auth import get_user_model +from django.core.files.base import ContentFile from django.test import TestCase +from opencontractserver.annotations.models import Annotation from opencontractserver.corpuses.models import Corpus +from opencontractserver.documents.models import Document from opencontractserver.extracts.models import ( Column, Extract, @@ -13,6 +16,7 @@ Row, ) from opencontractserver.tasks.extract_tasks import run_extract +from opencontractserver.tests.fixtures import SAMPLE_PDF_FILE_TWO_PATH User = get_user_model() @@ -28,16 +32,23 @@ def setUp(self): username="testuser", password="testpassword" ) - self.language_model = LanguageModel.objects.create(model="TestModel") + self.language_model = LanguageModel.objects.create( + model="TestModel", + creator=self.user + ) self.fieldset = Fieldset.objects.create( - owner=self.user, name="TestFieldset", description="Test description" + owner=self.user, + name="TestFieldset", + description="Test description", + creator=self.user ) self.column = Column.objects.create( fieldset=self.fieldset, query="TestQuery", output_type="str", language_model=self.language_model, - agentic=False, + agentic=True, + creator=self.user ) self.corpus = Corpus.objects.create(title="TestCorpus", creator=self.user) self.extract = Extract.objects.create( @@ -45,24 +56,45 @@ def setUp(self): name="TestExtract", fieldset=self.fieldset, owner=self.user, + creator=self.user + ) + + pdf_file = ContentFile( + SAMPLE_PDF_FILE_TWO_PATH.open("rb").read(), name="test.pdf" ) + self.doc = Document.objects.create( + creator=self.user, + title="Test Doc", + description="USC Title 1 - Chapter 1", + custom_meta={}, + pdf_file=pdf_file, + backend_lock=True, + ) + + self.corpus.documents.add(self.doc) + self.corpus.save() + @patch("opencontractserver.tasks.extract_tasks.agent_fetch_my_definitions") @patch("opencontractserver.tasks.extract_tasks.extract_for_query") def test_run_extract_task( self, mock_extract_for_query, mock_agent_fetch_my_definitions ): mock_extract_for_query.return_value = "Mocked extracted data" - mock_agent_fetch_my_definitions.return_value = [] + mock_agent_fetch_my_definitions.return_value = Annotation.objects.all() - run_extract(self.extract.id, self.user.id) + # Run this SYNCHRONOUSLY for TESTIN' purposes + run_extract.s(self.extract.id, self.user.id).apply().get() self.extract.refresh_from_db() self.assertIsNotNone(self.extract.started) - row = Row.objects.filter(extract=self.extract, column=self.column).first() + row = Row.objects.filter( + extract=self.extract, + column=self.column + ).first() self.assertIsNotNone(row) - self.assertEqual(row.data, json.dumps({"data": "Mocked extracted data"})) + self.assertEqual(row.data, {"data": "Mocked extracted data"}) self.assertEqual(row.data_definition, "str") self.assertIsNotNone(row.started) self.assertIsNotNone(row.completed) diff --git a/test.yml b/test.yml index 4ae69306..1e7fbf2c 100644 --- a/test.yml +++ b/test.yml @@ -13,6 +13,8 @@ services: vector-embedder: image: jscrudato/vector-embedder-microservice container_name: vector-embedder + environment: + PORT: 8000 django: &django build: From aa5ad70d430759e3c61475fe98110932b1e007a4 Mon Sep 17 00:00:00 2001 From: JSv4 Date: Sun, 26 May 2024 22:46:12 -0700 Subject: [PATCH 06/68] Ran linter. Cleaned up print log statements. --- config/graphql/mutations.py | 15 ++++------ opencontractserver/tasks/extract_tasks.py | 28 +++++++++---------- .../tests/test_extract_mutations.py | 11 ++++---- .../tests/test_extract_queries.py | 9 +++--- .../tests/test_extract_tasks.py | 15 ++++------ 5 files changed, 33 insertions(+), 45 deletions(-) diff --git a/config/graphql/mutations.py b/config/graphql/mutations.py index e03e4846..c7c892d7 100644 --- a/config/graphql/mutations.py +++ b/config/graphql/mutations.py @@ -1338,10 +1338,7 @@ class Arguments: @staticmethod @login_required def mutate(root, info, model): - language_model = LanguageModel( - model=model, - creator=info.context.user - ) + language_model = LanguageModel(model=model, creator=info.context.user) language_model.save() set_permissions_for_obj_to_user( info.context.user, language_model, [PermissionTypes.CRUD] @@ -1364,7 +1361,7 @@ def mutate(root, info, name, description): owner=info.context.user, name=name, description=description, - creator=info.context.user + creator=info.context.user, ) fieldset.save() set_permissions_for_obj_to_user( @@ -1401,9 +1398,7 @@ def mutate( limit_to_label=None, instructions=None, ): - fieldset = Fieldset.objects.get( - pk=from_global_id(fieldset_id)[1] - ) + fieldset = Fieldset.objects.get(pk=from_global_id(fieldset_id)[1]) language_model = LanguageModel.objects.get( pk=from_global_id(language_model_id)[1] ) @@ -1416,7 +1411,7 @@ def mutate( instructions=instructions, language_model=language_model, agentic=agentic, - creator=info.context.user + creator=info.context.user, ) column.save() set_permissions_for_obj_to_user( @@ -1445,7 +1440,7 @@ def mutate(root, info, corpus_id, name, fieldset_id): name=name, fieldset=fieldset, owner=info.context.user, - creator=info.context.user + creator=info.context.user, ) extract.save() set_permissions_for_obj_to_user( diff --git a/opencontractserver/tasks/extract_tasks.py b/opencontractserver/tasks/extract_tasks.py index d078782e..c7b14868 100644 --- a/opencontractserver/tasks/extract_tasks.py +++ b/opencontractserver/tasks/extract_tasks.py @@ -1,4 +1,4 @@ -import json +import logging from celery import shared_task from django.db import transaction @@ -8,8 +8,10 @@ from opencontractserver.annotations.models import Annotation from opencontractserver.extracts.models import Extract, Row from opencontractserver.types.enums import PermissionTypes -from opencontractserver.utils.permissioning import set_permissions_for_obj_to_user from opencontractserver.utils.embeddings import calculate_embedding_for_text +from opencontractserver.utils.permissioning import set_permissions_for_obj_to_user + +logger = logging.getLogger(__name__) # Mock these functions for now @@ -18,14 +20,13 @@ def agent_fetch_my_definitions(annot): def extract_for_query(annots, query, output_type): - print(f"Ran extract_for_query") return None @shared_task def run_extract(extract_id, user_id): - print(f"Run extract for extract {extract_id}") + logger.info(f"Run extract for extract {extract_id}") extract = Extract.objects.get(pk=extract_id) @@ -37,29 +38,27 @@ def run_extract(extract_id, user_id): fieldset = extract.fieldset document_ids = corpus.documents.all().values_list("id", flat=True) - print(f"Document ids: {document_ids}") for document_id in document_ids: for column in fieldset.columns.all(): - print(f"Processing column {column} for doc {document_id}") - with transaction.atomic(): row = Row.objects.create( extract=extract, column=column, data_definition=column.output_type, - creator_id=user_id + creator_id=user_id, ) set_permissions_for_obj_to_user(user_id, row, [PermissionTypes.CRUD]) try: - print(f"run_extract() - processing column {column} for {document_id}") + logger.debug( + f"run_extract() - processing column {column} for {document_id}" + ) row.started = timezone.now() row.save() output_type = eval(column.output_type) - print(f"output_type: {output_type}") annotations = Annotation.objects.filter( document_id=document_id, embedding__isnull=False @@ -71,7 +70,6 @@ def run_extract(extract_id, user_id): ) match_text = column.match_text or column.query - print(f"Match_text: {match_text}") if match_text: @@ -84,18 +82,18 @@ def run_extract(extract_id, user_id): )[:5] if column.agentic: - annotations = annotations.union(agent_fetch_my_definitions(annotations)) + annotations = annotations.union( + agent_fetch_my_definitions(annotations) + ) - print(f"Prepare to extract_for_query annotations {annotations} / column {column.query} / {output_type}") val = extract_for_query(annotations, column.query, output_type) - print(f"Extracted value: {val}") row.data = {"data": val} row.completed = timezone.now() row.save() except Exception as e: - print(f"Ran into error: {e}") + logger.error(f"run_extract() - Ran into error: {e}") row.stacktrace = f"Error processing: {e}" row.failed = timezone.now() row.save() diff --git a/opencontractserver/tests/test_extract_mutations.py b/opencontractserver/tests/test_extract_mutations.py index cd25e2c0..94974e8f 100644 --- a/opencontractserver/tests/test_extract_mutations.py +++ b/opencontractserver/tests/test_extract_mutations.py @@ -20,8 +20,7 @@ def __init__(self, user): class ExtractsMutationTestCase(TestCase): def setUp(self): self.user = User.objects.create_user( - username="testuser", - password="testpassword" + username="testuser", password="testpassword" ) self.client = Client(schema, context_value=TestContext(self.user)) @@ -77,12 +76,14 @@ def test_create_fieldset_mutation(self): ) def test_create_column_mutation(self): - language_model = LanguageModel.objects.create(model="TestModel", creator=self.user) + language_model = LanguageModel.objects.create( + model="TestModel", creator=self.user + ) fieldset = Fieldset.objects.create( owner=self.user, name="TestFieldset", description="Test description", - creator=self.user + creator=self.user, ) mutation = """ @@ -121,7 +122,7 @@ def test_start_extract_mutation(self): owner=self.user, name="TestFieldset", description="Test description", - creator=self.user + creator=self.user, ) mutation = """ diff --git a/opencontractserver/tests/test_extract_queries.py b/opencontractserver/tests/test_extract_queries.py index d64eb4e0..abb41850 100644 --- a/opencontractserver/tests/test_extract_queries.py +++ b/opencontractserver/tests/test_extract_queries.py @@ -29,14 +29,13 @@ def setUp(self): self.client = Client(schema, context_value=TestContext(self.user)) self.language_model = LanguageModel.objects.create( - model="TestModel", - creator=self.user + model="TestModel", creator=self.user ) self.fieldset = Fieldset.objects.create( owner=self.user, name="TestFieldset", description="Test description", - creator=self.user + creator=self.user, ) self.column = Column.objects.create( creator=self.user, @@ -52,14 +51,14 @@ def setUp(self): name="TestExtract", fieldset=self.fieldset, owner=self.user, - creator=self.user + creator=self.user, ) self.row = Row.objects.create( extract=self.extract, column=self.column, data={"data": "TestData"}, data_definition="str", - creator=self.user + creator=self.user, ) def test_language_model_query(self): diff --git a/opencontractserver/tests/test_extract_tasks.py b/opencontractserver/tests/test_extract_tasks.py index 17d4caff..a3c71c61 100644 --- a/opencontractserver/tests/test_extract_tasks.py +++ b/opencontractserver/tests/test_extract_tasks.py @@ -1,4 +1,3 @@ -import json from unittest.mock import patch from django.contrib.auth import get_user_model @@ -33,14 +32,13 @@ def setUp(self): ) self.language_model = LanguageModel.objects.create( - model="TestModel", - creator=self.user + model="TestModel", creator=self.user ) self.fieldset = Fieldset.objects.create( owner=self.user, name="TestFieldset", description="Test description", - creator=self.user + creator=self.user, ) self.column = Column.objects.create( fieldset=self.fieldset, @@ -48,7 +46,7 @@ def setUp(self): output_type="str", language_model=self.language_model, agentic=True, - creator=self.user + creator=self.user, ) self.corpus = Corpus.objects.create(title="TestCorpus", creator=self.user) self.extract = Extract.objects.create( @@ -56,7 +54,7 @@ def setUp(self): name="TestExtract", fieldset=self.fieldset, owner=self.user, - creator=self.user + creator=self.user, ) pdf_file = ContentFile( @@ -89,10 +87,7 @@ def test_run_extract_task( self.extract.refresh_from_db() self.assertIsNotNone(self.extract.started) - row = Row.objects.filter( - extract=self.extract, - column=self.column - ).first() + row = Row.objects.filter(extract=self.extract, column=self.column).first() self.assertIsNotNone(row) self.assertEqual(row.data, {"data": "Mocked extracted data"}) self.assertEqual(row.data_definition, "str") From 011ab3bb2316c97b8da61f637d0a6f33375c591f Mon Sep 17 00:00:00 2001 From: JSv4 Date: Sun, 26 May 2024 23:32:48 -0700 Subject: [PATCH 07/68] Changed output schema of new mutations to match existing mutations. --- config/graphql/mutations.py | 21 +-- frontend/src/extracts/ColumnDetails.tsx | 128 +++++++++++++++ frontend/src/graphql/mutations.ts | 17 ++ frontend/src/graphql/queries.ts | 147 ++++++++++++++++++ frontend/src/graphql/types.ts | 45 ++++++ .../tests/test_extract_mutations.py | 30 ++-- 6 files changed, 364 insertions(+), 24 deletions(-) create mode 100644 frontend/src/extracts/ColumnDetails.tsx diff --git a/config/graphql/mutations.py b/config/graphql/mutations.py index c7c892d7..e847cc32 100644 --- a/config/graphql/mutations.py +++ b/config/graphql/mutations.py @@ -1333,7 +1333,8 @@ class Arguments: model = graphene.String(required=True) ok = graphene.Boolean() - language_model = graphene.Field(LanguageModelType) + message = graphene.String() + obj = graphene.Field(LanguageModelType) @staticmethod @login_required @@ -1343,8 +1344,7 @@ def mutate(root, info, model): set_permissions_for_obj_to_user( info.context.user, language_model, [PermissionTypes.CRUD] ) - return CreateLanguageModel(ok=True, language_model=language_model) - + return CreateLanguageModel(ok=True, message="SUCCESS!", obj=language_model) class CreateFieldset(graphene.Mutation): class Arguments: @@ -1352,7 +1352,8 @@ class Arguments: description = graphene.String(required=True) ok = graphene.Boolean() - fieldset = graphene.Field(FieldsetType) + message = graphene.String() + obj = graphene.Field(FieldsetType) @staticmethod @login_required @@ -1367,7 +1368,7 @@ def mutate(root, info, name, description): set_permissions_for_obj_to_user( info.context.user, fieldset, [PermissionTypes.CRUD] ) - return CreateFieldset(ok=True, fieldset=fieldset) + return CreateFieldset(ok=True, message="SUCCESS!", obj=fieldset) class CreateColumn(graphene.Mutation): @@ -1382,7 +1383,8 @@ class Arguments: agentic = graphene.Boolean(required=True) ok = graphene.Boolean() - column = graphene.Field(ColumnType) + message = graphene.String() + obj = graphene.Field(ColumnType) @staticmethod @login_required @@ -1417,7 +1419,7 @@ def mutate( set_permissions_for_obj_to_user( info.context.user, column, [PermissionTypes.CRUD] ) - return CreateColumn(ok=True, column=column) + return CreateColumn(ok=True, message="SUCCESS!", obj=column) class StartExtract(graphene.Mutation): @@ -1427,7 +1429,8 @@ class Arguments: fieldset_id = graphene.ID(required=True) ok = graphene.Boolean() - extract = graphene.Field(ExtractType) + message = graphene.String() + obj = graphene.Field(ExtractType) @staticmethod @login_required @@ -1450,7 +1453,7 @@ def mutate(root, info, corpus_id, name, fieldset_id): # Start celery task to process extract run_extract.delay(extract.id, info.context.user.id) - return StartExtract(ok=True, extract=extract) + return StartExtract(ok=True, message="SUCCESS!", obj=extract) class Mutation(graphene.ObjectType): diff --git a/frontend/src/extracts/ColumnDetails.tsx b/frontend/src/extracts/ColumnDetails.tsx new file mode 100644 index 00000000..46c0f125 --- /dev/null +++ b/frontend/src/extracts/ColumnDetails.tsx @@ -0,0 +1,128 @@ +import React, { useState } from "react"; +import { Button, Form, Input, Select } from "semantic-ui-react"; +import { useMutation, useQuery } from "@apollo/client"; +import { toast } from "react-toastify"; + +interface ColumnDetailsProps { + column?: ColumnType; + fieldsetId: string; + onSave: () => void; +} + +export const ColumnDetails: React.FC = ({ + column, + fieldsetId, + onSave, +}) => { + const [query, setQuery] = useState(column?.query || ""); + const [matchText, setMatchText] = useState(column?.matchText || ""); + const [outputType, setOutputType] = useState(column?.outputType || ""); + const [limitToLabel, setLimitToLabel] = useState(column?.limitToLabel || ""); + const [instructions, setInstructions] = useState(column?.instructions || ""); + const [languageModelId, setLanguageModelId] = useState( + column?.languageModel.id || "" + ); + const [agentic, setAgentic] = useState(column?.agentic || false); + + const { data: languageModelsData } = useQuery<{ + languageModels: LanguageModelType[]; + }>(GET_LANGUAGE_MODELS); + + const [createColumn] = useMutation(CREATE_COLUMN); + const [updateColumn] = useMutation(UPDATE_COLUMN); + + const handleSave = async () => { + try { + if (column) { + await updateColumn({ + variables: { + id: column.id, + query, + matchText, + outputType, + limitToLabel, + instructions, + languageModelId, + agentic, + }, + }); + } else { + await createColumn({ + variables: { + fieldsetId, + query, + matchText, + outputType, + limitToLabel, + instructions, + languageModelId, + agentic, + }, + }); + } + onSave(); + toast.success("Column saved successfully"); + } catch (error) { + toast.error("Error saving column"); + } + }; + + return ( +
+ + + setQuery(e.target.value)} /> + + + + setMatchText(e.target.value)} + /> + + + + setOutputType(e.target.value)} + /> + + + + setLimitToLabel(e.target.value)} + /> + + + + setInstructions(e.target.value)} + /> + + + + setAgentic(e.target.checked)} + /> + + +
+ ); +}; diff --git a/frontend/src/graphql/mutations.ts b/frontend/src/graphql/mutations.ts index 781075c0..f880d4ed 100644 --- a/frontend/src/graphql/mutations.ts +++ b/frontend/src/graphql/mutations.ts @@ -1070,3 +1070,20 @@ export const REQUEST_DELETE_ANALYSIS = gql` } } `; + +export interface RequestCreateLanguageModelInputType { + model: string; +} + +export interface RequestCreateLanguageModelOutputType {} + +export const REQUEST_CREATE_LANGUAGEMODEL = gql` + mutation CreateLanguageModel($model: String!) { + createLanguageModel(model: $model) { + languageModel { + id + model + } + } + } +`; diff --git a/frontend/src/graphql/queries.ts b/frontend/src/graphql/queries.ts index 02775ae0..0bfec072 100644 --- a/frontend/src/graphql/queries.ts +++ b/frontend/src/graphql/queries.ts @@ -14,6 +14,9 @@ import { AnalysisType, AnnotationLabelType, LabelType, + LanguageModelType, + FieldsetType, + ExtractType, } from "./types"; export interface RequestDocumentsInputs { @@ -967,3 +970,147 @@ export const GET_EXPORTS = gql` } } `; + +export interface GetLanguageModelsOutputs { + languageModels: { + pageInfo: PageInfo; + edges: { + node: LanguageModelType; + }[]; + }; +} + +export const GET_LANGUAGEMODELS = gql` + query GetLanguageModels { + languageModels { + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + edges { + node { + id + model + } + } + } + } +`; + +export interface GetFieldsetsOutputs { + fieldsets: { + pageInfo: PageInfo; + edges: { + node: FieldsetType; + }[]; + }; +} + +export const GET_FIELDSETS = gql` + query GetFieldsets { + fieldsets { + edges { + node { + id + owner { + id + username + } + name + description + columns { + id + query + matchText + outputType + limitToLabel + instructions + languageModel { + id + model + } + agentic + } + } + } + } + } +`; + +export interface GetFieldsetOutputs { + fieldset: FieldsetType; +} + +export const GET_FIELDSET = gql` + query GetFieldset($id: ID!) { + fieldset(id: $id) { + id + owner { + id + username + } + name + description + columns { + id + query + matchText + outputType + limitToLabel + instructions + languageModel { + id + model + } + agentic + } + } + } +`; + +export interface GetExportsOutput { + extract: ExtractType; +} + +export const GET_EXPORT = gql` + query GetExtract($id: ID!) { + extract(id: $id) { + id + corpus { + id + title + } + name + fieldset { + id + name + columns { + id + query + } + } + owner { + id + username + } + created + started + finished + stacktrace + rows { + id + column { + id + } + data + dataDefinition + started + completed + failed + stacktrace + } + } + } +`; diff --git a/frontend/src/graphql/types.ts b/frontend/src/graphql/types.ts index c3a4c3d3..20f86a16 100644 --- a/frontend/src/graphql/types.ts +++ b/frontend/src/graphql/types.ts @@ -1170,3 +1170,48 @@ export type AnalyzerManifestType = { text_labels: AnnotationLabelPythonType[]; label_set: OpenContractsLabelSetType; }; + +export interface LanguageModelType extends Node { + model: string; +} + +export interface FieldsetType extends Node { + owner: UserType; + name: string; + description: string; + columns: ColumnType[]; +} + +export interface ColumnType extends Node { + fieldset: FieldsetType; + query: string; + matchText?: Maybe; + outputType: string; + limitToLabel?: Maybe; + instructions?: Maybe; + languageModel: LanguageModelType; + agentic: boolean; +} + +export interface ExtractType extends Node { + corpus: CorpusType; + name: string; + fieldset: FieldsetType; + owner: UserType; + created: string; + started?: Maybe; + finished?: Maybe; + stacktrace?: Maybe; + rows: RowType[]; +} + +export interface RowType extends Node { + extract: ExtractType; + column: ColumnType; + data: any; + dataDefinition: string; + started?: Maybe; + completed?: Maybe; + failed?: Maybe; + stacktrace?: Maybe; +} diff --git a/opencontractserver/tests/test_extract_mutations.py b/opencontractserver/tests/test_extract_mutations.py index 94974e8f..119b4aaa 100644 --- a/opencontractserver/tests/test_extract_mutations.py +++ b/opencontractserver/tests/test_extract_mutations.py @@ -31,7 +31,7 @@ def test_create_language_model_mutation(self): mutation { createLanguageModel(model: "TestModel") { ok - languageModel { + obj { id model } @@ -43,10 +43,10 @@ def test_create_language_model_mutation(self): self.assertIsNone(result.get("errors")) self.assertTrue(result["data"]["createLanguageModel"]["ok"]) self.assertIsNotNone( - result["data"]["createLanguageModel"]["languageModel"]["id"] + result["data"]["createLanguageModel"]["obj"]["id"] ) self.assertEqual( - result["data"]["createLanguageModel"]["languageModel"]["model"], "TestModel" + result["data"]["createLanguageModel"]["obj"]["model"], "TestModel" ) def test_create_fieldset_mutation(self): @@ -54,7 +54,7 @@ def test_create_fieldset_mutation(self): mutation { createFieldset(name: "TestFieldset", description: "Test description") { ok - fieldset { + obj { id name description @@ -66,12 +66,12 @@ def test_create_fieldset_mutation(self): result = self.client.execute(mutation) self.assertIsNone(result.get("errors")) self.assertTrue(result["data"]["createFieldset"]["ok"]) - self.assertIsNotNone(result["data"]["createFieldset"]["fieldset"]["id"]) + self.assertIsNotNone(result["data"]["createFieldset"]["obj"]["id"]) self.assertEqual( - result["data"]["createFieldset"]["fieldset"]["name"], "TestFieldset" + result["data"]["createFieldset"]["obj"]["name"], "TestFieldset" ) self.assertEqual( - result["data"]["createFieldset"]["fieldset"]["description"], + result["data"]["createFieldset"]["obj"]["description"], "Test description", ) @@ -96,7 +96,7 @@ def test_create_column_mutation(self): agentic: false ) {{ ok - column {{ + obj {{ id query outputType @@ -112,10 +112,10 @@ def test_create_column_mutation(self): result = self.client.execute(mutation) self.assertIsNone(result.get("errors")) self.assertTrue(result["data"]["createColumn"]["ok"]) - self.assertIsNotNone(result["data"]["createColumn"]["column"]["id"]) - self.assertEqual(result["data"]["createColumn"]["column"]["query"], "TestQuery") - self.assertEqual(result["data"]["createColumn"]["column"]["outputType"], "str") - self.assertEqual(result["data"]["createColumn"]["column"]["agentic"], False) + self.assertIsNotNone(result["data"]["createColumn"]["obj"]["id"]) + self.assertEqual(result["data"]["createColumn"]["obj"]["query"], "TestQuery") + self.assertEqual(result["data"]["createColumn"]["obj"]["outputType"], "str") + self.assertEqual(result["data"]["createColumn"]["obj"]["agentic"], False) def test_start_extract_mutation(self): fieldset = Fieldset.objects.create( @@ -133,7 +133,7 @@ def test_start_extract_mutation(self): fieldsetId: "{}" ) {{ ok - extract {{ + obj {{ id name }} @@ -150,8 +150,8 @@ def test_start_extract_mutation(self): result = self.client.execute(mutation) self.assertIsNone(result.get("errors")) self.assertTrue(result["data"]["startExtract"]["ok"]) - self.assertIsNotNone(result["data"]["startExtract"]["extract"]["id"]) + self.assertIsNotNone(result["data"]["startExtract"]["obj"]["id"]) self.assertEqual( - result["data"]["startExtract"]["extract"]["name"], "TestExtract" + result["data"]["startExtract"]["obj"]["name"], "TestExtract" ) mock_task.assert_called_once() From cedd842a5034bf922ad074f841514b870e2259f1 Mon Sep 17 00:00:00 2001 From: JSv4 Date: Mon, 27 May 2024 06:14:09 -0700 Subject: [PATCH 08/68] Add skeletons of additional extract components. --- frontend/src/extracts/ColumnDetails.tsx | 24 ++- frontend/src/extracts/ExtractDataGrid.tsx | 84 +++++++++ frontend/src/extracts/FieldsetDetails.tsx | 69 +++++++ frontend/src/extracts/FieldsetList.tsx | 23 +++ frontend/src/graphql/mutations.ts | 218 +++++++++++++++++++++- frontend/src/graphql/queries.ts | 4 +- 6 files changed, 411 insertions(+), 11 deletions(-) create mode 100644 frontend/src/extracts/ExtractDataGrid.tsx create mode 100644 frontend/src/extracts/FieldsetDetails.tsx create mode 100644 frontend/src/extracts/FieldsetList.tsx diff --git a/frontend/src/extracts/ColumnDetails.tsx b/frontend/src/extracts/ColumnDetails.tsx index 46c0f125..a578dfbe 100644 --- a/frontend/src/extracts/ColumnDetails.tsx +++ b/frontend/src/extracts/ColumnDetails.tsx @@ -2,6 +2,12 @@ import React, { useState } from "react"; import { Button, Form, Input, Select } from "semantic-ui-react"; import { useMutation, useQuery } from "@apollo/client"; import { toast } from "react-toastify"; +import { ColumnType, LanguageModelType } from "../graphql/types"; +import { GET_LANGUAGEMODELS } from "../graphql/queries"; +import { + REQUEST_CREATE_COLUMN, + REQUEST_UPDATE_COLUMN, +} from "../graphql/mutations"; interface ColumnDetailsProps { column?: ColumnType; @@ -26,10 +32,10 @@ export const ColumnDetails: React.FC = ({ const { data: languageModelsData } = useQuery<{ languageModels: LanguageModelType[]; - }>(GET_LANGUAGE_MODELS); + }>(GET_LANGUAGEMODELS); - const [createColumn] = useMutation(CREATE_COLUMN); - const [updateColumn] = useMutation(UPDATE_COLUMN); + const [createColumn] = useMutation(REQUEST_CREATE_COLUMN); + const [updateColumn] = useMutation(REQUEST_UPDATE_COLUMN); const handleSave = async () => { try { @@ -105,10 +111,14 @@ export const ColumnDetails: React.FC = ({ handleSave(e.target.value, fieldset.description)} + /> + + + + handleSave(fieldset.name, e.target.value)} + /> + + +

Columns

+ {fieldset.columns.map((column: ColumnType) => ( + + ))} + + + ); +}; diff --git a/frontend/src/extracts/FieldsetList.tsx b/frontend/src/extracts/FieldsetList.tsx new file mode 100644 index 00000000..fa73a0f7 --- /dev/null +++ b/frontend/src/extracts/FieldsetList.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import { List } from "semantic-ui-react"; +import { useQuery } from "@apollo/client"; + +import { FieldsetDetails } from "./FieldsetDetails"; +import { FieldsetType } from "../graphql/types"; +import { REQUEST_GET_FIELDSETS } from "../graphql/queries"; + +export const FieldsetsList: React.FC = () => { + const { data, refetch } = useQuery<{ fieldsets: FieldsetType[] }>( + REQUEST_GET_FIELDSETS + ); + + return ( + + {data?.fieldsets.map((fieldset) => ( + + + + ))} + + ); +}; diff --git a/frontend/src/graphql/mutations.ts b/frontend/src/graphql/mutations.ts index f880d4ed..8d04de6b 100644 --- a/frontend/src/graphql/mutations.ts +++ b/frontend/src/graphql/mutations.ts @@ -4,9 +4,13 @@ import { ExportTypes, MultipageAnnotationJson } from "../components/types"; import { AnalysisType, AnnotationLabelType, + ColumnType, CorpusType, + ExtractType, + FieldsetType, LabelSetType, LabelType, + LanguageModelType, Maybe, UserExportType, } from "./types"; @@ -1075,14 +1079,224 @@ export interface RequestCreateLanguageModelInputType { model: string; } -export interface RequestCreateLanguageModelOutputType {} +export interface RequestCreateLanguageModelOutputType { + languageModel: { + ok: boolean; + message: string; + obj: LanguageModelType; + }; +} export const REQUEST_CREATE_LANGUAGEMODEL = gql` mutation CreateLanguageModel($model: String!) { createLanguageModel(model: $model) { languageModel { id - model + message + obj { + id + } + } + } + } +`; + +export interface RequestCreateFieldsetInputType { + name: string; + description: string; +} + +export interface RequestCreateFieldsetOutputType { + createFieldset: { + ok: boolean; + message: string; + obj: FieldsetType; + }; +} + +export const REQUEST_CREATE_FIELDSET = gql` + mutation CreateFieldset($name: String!, $description: String!) { + createFieldset(name: $name, description: $description) { + ok + msg + obj { + id + name + description + } + } + } +`; + +export interface RequestUpdateFieldsetOutputType { + updateFieldset: { + ok: boolean; + message: string; + obj: FieldsetType; + }; +} + +export interface RequestUpdateFieldsetInputType { + id: string; + name?: string; + description?: string; +} + +export const REQUEST_UPDATE_FIELDSET = gql` + mutation UpdateFieldset($id: ID!, $name: String, $description: String) { + updateFieldset(id: $id, name: $name, description: $description) { + msg + ok + obj { + id + name + description + } + } + } +`; + +export interface RequestCreateColumnInputType { + fieldsetId: string; + query: string; + matchText?: string; + outputType: string; + limitToLabel?: string; + instructions?: string; + languageModelId: string; + agentic: boolean; +} + +export interface RequestCreateColumnOutputType { + createColumn: { + ok: boolean; + msg: string; + obj: ColumnType; + }; +} + +export const REQUEST_CREATE_COLUMN = gql` + mutation CreateColumn( + $fieldsetId: ID! + $query: String! + $matchText: String + $outputType: String! + $limitToLabel: String + $instructions: String + $languageModelId: ID! + $agentic: Boolean! + ) { + createColumn( + fieldsetId: $fieldsetId + query: $query + matchText: $matchText + outputType: $outputType + limitToLabel: $limitToLabel + instructions: $instructions + languageModelId: $languageModelId + agentic: $agentic + ) { + msg + ok + obj { + id + query + matchText + outputType + limitToLabel + instructions + languageModel { + id + model + } + agentic + } + } + } +`; + +export interface RequestUpdateColumnInputType { + id: string; + fieldsetId: string; + query: string; + matchText?: string; + outputType: string; + limitToLabel?: string; + instructions?: string; + languageModelId: string; + agentic: boolean; +} + +export interface RequestUpdateColumnOutputType { + updateColumn: { + ok: boolean; + msg: string; + obj: ColumnType; + }; +} + +export const REQUEST_UPDATE_COLUMN = gql` + mutation UpdateColumn( + $id: ID! + $query: String + $matchText: String + $outputType: String + $limitToLabel: String + $instructions: String + $languageModelId: ID + $agentic: Boolean + ) { + updateColumn( + id: $id + query: $query + matchText: $matchText + outputType: $outputType + limitToLabel: $limitToLabel + instructions: $instructions + languageModelId: $languageModelId + agentic: $agentic + ) { + msg + ok + obj { + id + query + matchText + outputType + limitToLabel + instructions + languageModel { + id + model + } + agentic + } + } + } +`; + +export interface RequestStartExtractOutputType { + startExtract: { + msg: string; + ok: boolean; + obj: ExtractType; + }; +} + +export interface RequestStartExtractInputType { + corpusId: string; + name: string; + fieldsetId: string; +} + +export const REQUEST_START_EXTRACT = gql` + mutation StartExtract($corpusId: ID!, $name: String!, $fieldsetId: ID!) { + startExtract(corpusId: $corpusId, name: $name, fieldsetId: $fieldsetId) { + msg + ok + obj { + id + name } } } diff --git a/frontend/src/graphql/queries.ts b/frontend/src/graphql/queries.ts index 0bfec072..bdfb4ada 100644 --- a/frontend/src/graphql/queries.ts +++ b/frontend/src/graphql/queries.ts @@ -1008,7 +1008,7 @@ export interface GetFieldsetsOutputs { }; } -export const GET_FIELDSETS = gql` +export const REQUEST_GET_FIELDSETS = gql` query GetFieldsets { fieldsets { edges { @@ -1074,7 +1074,7 @@ export interface GetExportsOutput { extract: ExtractType; } -export const GET_EXPORT = gql` +export const REQUEST_GET_EXTRACT = gql` query GetExtract($id: ID!) { extract(id: $id) { id From 19d238b497bc10bcaaebef62b8a04690a8074ffd Mon Sep 17 00:00:00 2001 From: JSv4 Date: Mon, 27 May 2024 14:16:33 -0700 Subject: [PATCH 09/68] Renaming 'Row' to 'Datacell' as it's NOT a row object but a cell in a row. --- config/graphql/filters.py | 6 +- config/graphql/graphene_types.py | 6 +- config/graphql/queries.py | 26 +- frontend/jest-setup.ts | 8 + frontend/jest.config.js | 25 + frontend/package.json | 6 +- frontend/src/App.test.tsx | 9 - frontend/src/extracts/ColumnDetails.tsx | 2 + frontend/src/extracts/ExtractDataGrid.tsx | 2 + frontend/src/graphql/queries.ts | 1 - frontend/src/graphql/types.ts | 2 +- frontend/src/tests/ColumnDetails.test.tsx | 77 + frontend/src/tests/ExtractDataGrid.test.tsx | 122 ++ frontend/src/tests/utils/factories.ts | 246 +++ frontend/tsconfig.json | 3 +- frontend/yarn.lock | 1514 ++++++++++++++++- .../migrations/0003_auto_20240527_2116.py | 77 + opencontractserver/extracts/models.py | 29 +- opencontractserver/tasks/extract_tasks.py | 4 +- .../tests/test_extract_queries.py | 4 +- .../tests/test_extract_tasks.py | 4 +- 21 files changed, 2052 insertions(+), 121 deletions(-) create mode 100644 frontend/jest-setup.ts create mode 100644 frontend/jest.config.js delete mode 100644 frontend/src/App.test.tsx create mode 100644 frontend/src/tests/ColumnDetails.test.tsx create mode 100644 frontend/src/tests/ExtractDataGrid.test.tsx create mode 100644 frontend/src/tests/utils/factories.ts create mode 100644 opencontractserver/extracts/migrations/0003_auto_20240527_2116.py diff --git a/config/graphql/filters.py b/config/graphql/filters.py index ff391a17..07203dcf 100644 --- a/config/graphql/filters.py +++ b/config/graphql/filters.py @@ -22,7 +22,7 @@ Extract, Fieldset, LanguageModel, - Row, + Datacell, ) from opencontractserver.users.models import Assignment, UserExport @@ -428,9 +428,9 @@ class Meta: } -class RowFilter(django_filters.FilterSet): +class DatacellFilter(django_filters.FilterSet): class Meta: - model = Row + model = Datacell fields = { "data_definition": ["exact"], "started": ["lte", "gte"], diff --git a/config/graphql/graphene_types.py b/config/graphql/graphene_types.py index 228095eb..75b05df7 100644 --- a/config/graphql/graphene_types.py +++ b/config/graphql/graphene_types.py @@ -26,7 +26,7 @@ Extract, Fieldset, LanguageModel, - Row, + Datacell, ) from opencontractserver.users.models import Assignment, UserExport, UserImport @@ -323,11 +323,11 @@ class Meta: connection_class = CountableConnection -class RowType(AnnotatePermissionsForReadMixin, DjangoObjectType): +class DatacellType(AnnotatePermissionsForReadMixin, DjangoObjectType): data = GenericScalar() class Meta: - model = Row + model = Datacell interfaces = [relay.Node] connection_class = CountableConnection diff --git a/config/graphql/queries.py b/config/graphql/queries.py index ce0de2ce..1d8f78a2 100644 --- a/config/graphql/queries.py +++ b/config/graphql/queries.py @@ -27,7 +27,7 @@ LabelsetFilter, LanguageModelFilter, RelationshipFilter, - RowFilter, + DatacellFilter, ) from config.graphql.graphene_types import ( AnalysisType, @@ -46,7 +46,7 @@ PageAwareAnnotationType, PdfPageInfoType, RelationshipType, - RowType, + DatacellType, UserExportType, UserImportType, ) @@ -64,7 +64,7 @@ Extract, Fieldset, LanguageModel, - Row, + Datacell, ) from opencontractserver.shared.resolvers import resolve_oc_model_queryset from opencontractserver.types.enums import LabelType @@ -750,30 +750,30 @@ def resolve_extracts(self, info, **kwargs): Q(owner=info.context.user) | Q(is_public=True) ) - row = relay.Node.Field(RowType) + datacell = relay.Node.Field(DatacellType) @login_required - def resolve_row(self, info, **kwargs): + def resolve_datacell(self, info, **kwargs): django_pk = from_global_id(kwargs.get("id", None))[1] if info.context.user.is_superuser: - return Row.objects.get(id=django_pk) + return Datacell.objects.get(id=django_pk) elif info.context.user.is_anonymous: - return Row.objects.get(Q(id=django_pk) & Q(is_public=True)) + return Datacell.objects.get(Q(id=django_pk) & Q(is_public=True)) else: - return Row.objects.get( + return Datacell.objects.get( Q(id=django_pk) & (Q(extract__owner=info.context.user) | Q(is_public=True)) ) - rows = DjangoFilterConnectionField(RowType, filterset_class=RowFilter) + datacells = DjangoFilterConnectionField(DatacellType, filterset_class=DatacellFilter) @login_required - def resolve_rows(self, info, **kwargs): + def resolve_datacells(self, info, **kwargs): if info.context.user.is_superuser: - return Row.objects.all() + return Datacell.objects.all() elif info.context.user.is_anonymous: - return Row.objects.filter(Q(is_public=True)) + return Datacell.objects.filter(Q(is_public=True)) else: - return Row.objects.filter( + return Datacell.objects.filter( Q(extract__owner=info.context.user) | Q(is_public=True) ) diff --git a/frontend/jest-setup.ts b/frontend/jest-setup.ts new file mode 100644 index 00000000..8617671b --- /dev/null +++ b/frontend/jest-setup.ts @@ -0,0 +1,8 @@ +import type { Config } from "jest"; +import "@testing-library/jest-dom"; + +const config: Config = { + verbose: true, +}; + +export default config; diff --git a/frontend/jest.config.js b/frontend/jest.config.js new file mode 100644 index 00000000..4708943b --- /dev/null +++ b/frontend/jest.config.js @@ -0,0 +1,25 @@ +const { JSDOM } = require("jsdom"); +const jsdom = new JSDOM(""); +const { window } = jsdom; + +function copyProps(src, target) { + Object.defineProperties(target, { + ...Object.getOwnPropertyDescriptors(src), + ...Object.getOwnPropertyDescriptors(target), + }); +} + +global.window = window; +global.document = window.document; +global.navigator = { + userAgent: "node.js", +}; +copyProps(window, global); + +module.exports = { + testEnvironment: "jsdom", + setupFilesAfterEnv: ["./jest-setup.ts"], + transform: { + "^.+\\.(js|jsx|ts|tsx)$": "babel-jest", + }, +}; diff --git a/frontend/package.json b/frontend/package.json index bd5ad118..9ceb1356 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,8 +7,8 @@ "@auth0/auth0-react": "^1.8.0", "@rjsf/core": "^3.2.1", "@rjsf/semantic-ui": "^3.2.1", - "@testing-library/jest-dom": "^5.16.1", - "@testing-library/react": "^12.1.2", + "@testing-library/jest-dom": "^6.4.5", + "@testing-library/react": "12.1.5", "@testing-library/user-event": "^13.5.0", "@types/jest": "^27.0.3", "@types/lodash": "^4.14.178", @@ -80,7 +80,9 @@ "devDependencies": { "@types/lodash.uniqueid": "^4.0.9", "@types/uuid": "^8.3.4", + "babel-jest": "^29.7.0", "husky": "^8.0.1", + "jest": "^29.7.0", "prettier": "^2.7.1" } } diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx deleted file mode 100644 index 05d3d3e5..00000000 --- a/frontend/src/App.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from "react"; -import { render, screen } from "@testing-library/react"; -import { App } from "./App"; - -test("renders learn react link", () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/frontend/src/extracts/ColumnDetails.tsx b/frontend/src/extracts/ColumnDetails.tsx index a578dfbe..811786a3 100644 --- a/frontend/src/extracts/ColumnDetails.tsx +++ b/frontend/src/extracts/ColumnDetails.tsx @@ -20,6 +20,8 @@ export const ColumnDetails: React.FC = ({ fieldsetId, onSave, }) => { + console.log("ColumnDetails - column: ", column); + const [query, setQuery] = useState(column?.query || ""); const [matchText, setMatchText] = useState(column?.matchText || ""); const [outputType, setOutputType] = useState(column?.outputType || ""); diff --git a/frontend/src/extracts/ExtractDataGrid.tsx b/frontend/src/extracts/ExtractDataGrid.tsx index ddeb5a3e..2f2ed380 100644 --- a/frontend/src/extracts/ExtractDataGrid.tsx +++ b/frontend/src/extracts/ExtractDataGrid.tsx @@ -41,6 +41,8 @@ export const ExtractDataGrid: React.FC = ({ const columns = extract.fieldset.columns; const rows = extract.rows; + console.log("Extract:", extract); + return (
diff --git a/frontend/src/graphql/queries.ts b/frontend/src/graphql/queries.ts index bdfb4ada..d7726fe3 100644 --- a/frontend/src/graphql/queries.ts +++ b/frontend/src/graphql/queries.ts @@ -1098,7 +1098,6 @@ export const REQUEST_GET_EXTRACT = gql` created started finished - stacktrace rows { id column { diff --git a/frontend/src/graphql/types.ts b/frontend/src/graphql/types.ts index 20f86a16..7a7cc63e 100644 --- a/frontend/src/graphql/types.ts +++ b/frontend/src/graphql/types.ts @@ -1172,6 +1172,7 @@ export type AnalyzerManifestType = { }; export interface LanguageModelType extends Node { + id: string; model: string; } @@ -1213,5 +1214,4 @@ export interface RowType extends Node { started?: Maybe; completed?: Maybe; failed?: Maybe; - stacktrace?: Maybe; } diff --git a/frontend/src/tests/ColumnDetails.test.tsx b/frontend/src/tests/ColumnDetails.test.tsx new file mode 100644 index 00000000..85d10b77 --- /dev/null +++ b/frontend/src/tests/ColumnDetails.test.tsx @@ -0,0 +1,77 @@ +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { MockedProvider } from "@apollo/client/testing"; +import { ColumnDetails } from "../extracts/ColumnDetails"; +import { ColumnType } from "../graphql/types"; +import { generateMockUser } from "./utils/factories"; + +const mockFieldsetId = "1"; +const mockOnSave = jest.fn(); + +const mockColumn: ColumnType = { + id: "1", + query: "test query", + matchText: "test match", + outputType: "string", + limitToLabel: "test label", + instructions: "test instructions", + languageModel: { + id: "1", + model: "test model", + }, + agentic: false, + fieldset: { + id: "213214", + owner: generateMockUser(), + name: "The set to be", + description: "Everyone's favorite hang", + columns: [], + }, +}; + +describe("ColumnDetails", () => { + it("renders the form with correct values", () => { + render( + + + + ); + + expect(screen.getByLabelText("Query")).toHaveValue(mockColumn.query); + expect(screen.getByLabelText("Match Text")).toHaveValue( + mockColumn.matchText + ); + expect(screen.getByLabelText("Output Type")).toHaveValue( + mockColumn.outputType + ); + expect(screen.getByLabelText("Limit to Label")).toHaveValue( + mockColumn.limitToLabel + ); + expect(screen.getByLabelText("Instructions")).toHaveValue( + mockColumn.instructions + ); + expect(screen.getByLabelText("Language Model")).toHaveValue( + mockColumn.languageModel.id + ); + expect(screen.getByLabelText("Agentic")).not.toBeChecked(); + }); + + it("calls onSave when the save button is clicked", () => { + render( + + + + ); + + fireEvent.click(screen.getByText("Save")); + expect(mockOnSave).toHaveBeenCalled(); + }); +}); diff --git a/frontend/src/tests/ExtractDataGrid.test.tsx b/frontend/src/tests/ExtractDataGrid.test.tsx new file mode 100644 index 00000000..3b745ec7 --- /dev/null +++ b/frontend/src/tests/ExtractDataGrid.test.tsx @@ -0,0 +1,122 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { MockedProvider } from "@apollo/client/testing"; +import { REQUEST_GET_EXTRACT } from "../graphql/queries"; +import { ExtractDataGrid } from "../extracts/ExtractDataGrid"; +import { generateMockLanguageModel } from "./utils/factories"; + +const mockExtractId = "1"; + +const mockExtract = { + id: "1", + corpus: { + id: "1", + title: "Test Corpus", + }, + name: "Test Extract", + fieldset: { + id: "1", + name: "Test Fieldset", + columns: [ + { + id: "1", + query: "Test Query 1", + languageModel: { + id: "12324", + model: "GPT4", + }, + }, + { + id: "2", + query: "Test Query 2", + languageModel: { + id: "12324", + model: "GPT4", + }, + }, + ], + }, + owner: { + id: "1", + username: "testuser", + }, + created: "2023-06-12T10:00:00", + started: null, + finished: null, + rows: [ + { + id: "1", + data: { + data: "Test Data 1", + }, + dataDefinition: "str", + stacktrace: "", + failed: null, + finished: null, + completed: null, + started: null, + column: { + id: "1", + languageModel: { + id: "12312", + }, + }, + }, + ], +}; + +const mockGetExtractQuery = { + request: { + query: REQUEST_GET_EXTRACT, + variables: { id: mockExtractId }, + }, + result: { + data: { + extract: { + id: mockExtractId, + }, + }, + }, +}; + +describe("ExtractDataGrid", () => { + it("renders the data grid with correct data", async () => { + render( + + + + ); + + expect(await screen.findByText("Test Query 1")).toBeInTheDocument(); + expect(screen.getByText("Test Query 2")).toBeInTheDocument(); + expect(screen.getByText("Test Data 1")).toBeInTheDocument(); + expect(screen.getByText("Test Data 2")).toBeInTheDocument(); + }); + + it("renders the start extract button when the extract has not started", async () => { + const mockExtractNotStarted = { + ...mockExtract, + started: null, + }; + + const mockGetExtractQueryNotStarted = { + request: { + query: REQUEST_GET_EXTRACT, + variables: { id: mockExtractId }, + }, + result: { + data: { + extract: mockExtractNotStarted, + }, + }, + }; + + render( + + + + ); + + expect(await screen.findByText("Start Extract")).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/tests/utils/factories.ts b/frontend/src/tests/utils/factories.ts new file mode 100644 index 00000000..1c5f9ec6 --- /dev/null +++ b/frontend/src/tests/utils/factories.ts @@ -0,0 +1,246 @@ +import { v4 as uuidv4 } from "uuid"; +import { + UserType, + AssignmentTypeConnection, + DocumentTypeConnection, + CorpusTypeConnection, + AnnotationLabelTypeConnection, + RelationshipTypeConnection, + AnnotationTypeConnection, + LabelSetTypeConnection, + UserExportTypeConnection, + UserImportTypeConnection, + PageInfo, + AnalysisTypeConnection, + ExtractType, + ColumnType, + RowType, + CorpusType, + FieldsetType, + LanguageModelType, +} from "../../graphql/types"; + +export function generateMockUser(): UserType { + return { + __typename: "UserType", + id: uuidv4(), + password: "mockPassword", + lastLogin: new Date().toISOString(), + isSuperuser: false, + username: "mockUsername", + email: "mock@example.com", + isStaff: false, + isActive: true, + dateJoined: new Date().toISOString(), + name: "Mock User", + createdAssignments: generateMockAssignmentConnection(), + myAssignments: generateMockAssignmentConnection(), + userexportSet: generateMockUserExportTypeConnection(), + userimportSet: generateMockUserImportTypeConnection(), + editingDocuments: generateMockDocumentConnection(), + documentSet: generateMockDocumentConnection(), + corpusSet: generateMockCorpusConnection(), + editingCorpuses: generateMockCorpusConnection(), + labelSet: generateMockAnnotationLabelConnection(), + relationshipSet: generateMockRelationshipConnection(), + annotationSet: generateMockAnnotationConnection(), + labelsetSet: generateMockLabelSetConnection(), + }; +} + +export function generateMockAssignmentConnection(): AssignmentTypeConnection { + return { + __typename: "AssignmentTypeConnection", + pageInfo: generateMockPageInfo(), + edges: [], + totalCount: 0, + }; +} + +export function generateMockUserExportTypeConnection(): UserExportTypeConnection { + return { + __typename: "UserExportTypeConnection", + pageInfo: generateMockPageInfo(), + edges: [], + totalCount: 0, + }; +} + +export function generateMockUserImportTypeConnection(): UserImportTypeConnection { + return { + __typename: "UserImportTypeConnection", + pageInfo: generateMockPageInfo(), + edges: [], + totalCount: 0, + }; +} + +export function generateMockDocumentConnection(): DocumentTypeConnection { + return { + __typename: "DocumentTypeConnection", + pageInfo: generateMockPageInfo(), + edges: [], + totalCount: 0, + }; +} + +export function generateMockCorpusConnection(): CorpusTypeConnection { + return { + __typename: "CorpusTypeConnection", + pageInfo: generateMockPageInfo(), + edges: [], + totalCount: 0, + }; +} + +export function generateMockAnnotationLabelConnection(): AnnotationLabelTypeConnection { + return { + __typename: "AnnotationLabelTypeConnection", + pageInfo: generateMockPageInfo(), + edges: [], + totalCount: 0, + }; +} + +export function generateMockRelationshipConnection(): RelationshipTypeConnection { + return { + __typename: "RelationshipTypeConnection", + pageInfo: generateMockPageInfo(), + edges: [], + totalCount: 0, + }; +} + +export function generateMockAnnotationConnection(): AnnotationTypeConnection { + return { + __typename: "AnnotationTypeConnection", + pageInfo: generateMockPageInfo(), + edges: [], + totalCount: 0, + }; +} + +export function generateMockLabelSetConnection(): LabelSetTypeConnection { + return { + __typename: "LabelSetTypeConnection", + pageInfo: generateMockPageInfo(), + edges: [], + totalCount: 0, + }; +} + +export function generateMockPageInfo(): PageInfo { + return { + __typename: "PageInfo", + hasNextPage: false, + hasPreviousPage: false, + startCursor: null, + endCursor: null, + }; +} + +export function generateMockLanguageModel(): LanguageModelType { + return { + id: uuidv4(), + model: "mockLanguageModel", + }; +} + +export function generateMockFieldset(owner: UserType): FieldsetType { + return { + id: uuidv4(), + owner, + name: "mockFieldset", + description: "Mock fieldset description", + columns: [], + }; +} + +export function generateMockColumn(fieldset: FieldsetType): ColumnType { + return { + id: uuidv4(), + fieldset, + query: "mockQuery", + matchText: "mockMatchText", + outputType: "mockOutputType", + limitToLabel: "mockLimitToLabel", + instructions: "mockInstructions", + languageModel: generateMockLanguageModel(), + agentic: false, + }; +} + +export function generateMockCorpus(owner: UserType): CorpusType { + return { + __typename: "CorpusType", + id: uuidv4(), + title: "Mock Corpus", + appliedAnalyzerIds: [], + is_selected: false, + is_opened: false, + description: "Mock corpus description", + icon: "mockIcon", + documents: generateMockDocumentConnection(), + labelSet: null, + creator: owner, + parent: undefined, + backendLock: false, + userLock: null, + error: false, + created: new Date().toISOString(), + modified: new Date().toISOString(), + assignmentSet: generateMockAssignmentConnection(), + relationshipSet: generateMockRelationshipConnection(), + annotationSet: generateMockAnnotationConnection(), + allAnnotationSummaries: [], + analyses: generateMockAnalysisConnection(), + isPublic: false, + myPermissions: [], + }; +} + +export function generateMockExtract( + corpus: CorpusType, + owner: UserType, + fieldset: FieldsetType +): ExtractType { + return { + id: uuidv4(), + corpus, + name: "Mock Extract", + fieldset, + owner, + created: new Date().toISOString(), + started: null, + finished: null, + stacktrace: null, + rows: [], + }; +} + +export function generateMockRow( + extract: ExtractType, + column: ColumnType +): RowType { + return { + id: uuidv4(), + extract, + column, + data: { + data: "Some Data", + }, + dataDefinition: "str", + started: null, + completed: null, + failed: null, + }; +} + +export function generateMockAnalysisConnection(): AnalysisTypeConnection { + return { + __typename: "AnalysisTypeConnection", + pageInfo: generateMockPageInfo(), + edges: [], + totalCount: 0, + }; +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 6d792280..98fb3c8d 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "target": "es5", + "types": ["@testing-library/jest-dom"], "lib": ["dom", "dom.iterable", "esnext", "es2020.string"], "allowJs": true, "skipLibCheck": true, @@ -17,5 +18,5 @@ "noEmit": true, "jsx": "react-jsx" }, - "include": ["src"] + "include": ["src", "./jest-setup.ts"] } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index e1b782d3..ed1a5d24 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2,6 +2,19 @@ # yarn lockfile v1 +"@adobe/css-tools@^4.3.2": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.3.tgz#90749bde8b89cd41764224f5aac29cd4138f75ff" + integrity sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ== + +"@ampproject/remapping@^2.2.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + "@apideck/better-ajv-errors@^0.3.1": version "0.3.1" resolved "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.1.tgz" @@ -57,11 +70,24 @@ dependencies: "@babel/highlight" "^7.16.0" +"@babel/code-frame@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.6.tgz#ab88da19344445c3d8889af2216606d3329f3ef2" + integrity sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA== + dependencies: + "@babel/highlight" "^7.24.6" + picocolors "^1.0.0" + "@babel/compat-data@^7.13.11", "@babel/compat-data@^7.16.0", "@babel/compat-data@^7.16.4": version "7.16.4" resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.16.4.tgz" integrity sha512-1o/jo7D+kC9ZjHX5v+EHrdjl3PhxMrLSOTGsOdHJ+KL8HCaEK6ehrVL2RS6oHDZp+L7xLirLrPmQtEng769J/Q== +"@babel/compat-data@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.6.tgz#b3600217688cabb26e25f8e467019e66d71b7ae2" + integrity sha512-aC2DGhBq5eEdyXWqrDInSqQjO0k8xtPRf5YylULqx8MCd6jBtzqfta/3ETMRpuKIc5hyswfO80ObyA1MvkCcUQ== + "@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.12.3", "@babel/core@^7.16.0", "@babel/core@^7.7.2", "@babel/core@^7.7.5": version "7.16.5" resolved "https://registry.npmjs.org/@babel/core/-/core-7.16.5.tgz" @@ -83,6 +109,27 @@ semver "^6.3.0" source-map "^0.5.0" +"@babel/core@^7.11.6", "@babel/core@^7.23.9": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.6.tgz#8650e0e4b03589ebe886c4e4a60398db0a7ec787" + integrity sha512-qAHSfAdVyFmIvl0VHELib8xar7ONuSHrE2hLnsaWkYNTI68dmi1x8GYDhJjMI/e7XWal9QBlZkwbOnkcw7Z8gQ== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.24.6" + "@babel/generator" "^7.24.6" + "@babel/helper-compilation-targets" "^7.24.6" + "@babel/helper-module-transforms" "^7.24.6" + "@babel/helpers" "^7.24.6" + "@babel/parser" "^7.24.6" + "@babel/template" "^7.24.6" + "@babel/traverse" "^7.24.6" + "@babel/types" "^7.24.6" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + "@babel/eslint-parser@^7.16.3": version "7.16.5" resolved "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.16.5.tgz" @@ -101,6 +148,16 @@ jsesc "^2.5.1" source-map "^0.5.0" +"@babel/generator@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.6.tgz#dfac82a228582a9d30c959fe50ad28951d4737a7" + integrity sha512-S7m4eNa6YAPJRHmKsLHIDJhNAGNKoWNiWefz1MBbpnt8g9lvMDl1hir4P9bo/57bQEmuwEhnRU/AMWsD0G/Fbg== + dependencies: + "@babel/types" "^7.24.6" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.16.0": version "7.16.0" resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.0.tgz" @@ -126,6 +183,17 @@ browserslist "^4.17.5" semver "^6.3.0" +"@babel/helper-compilation-targets@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.6.tgz#4a51d681f7680043d38e212715e2a7b1ad29cb51" + integrity sha512-VZQ57UsDGlX/5fFA7GkVPplZhHsVc+vuErWgdOiysI9Ksnw0Pbbd6pnPiR/mmJyKHgyIW0c7KT32gmhiF+cirg== + dependencies: + "@babel/compat-data" "^7.24.6" + "@babel/helper-validator-option" "^7.24.6" + browserslist "^4.22.2" + lru-cache "^5.1.1" + semver "^6.3.1" + "@babel/helper-create-class-features-plugin@^7.16.0", "@babel/helper-create-class-features-plugin@^7.16.5": version "7.16.5" resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.16.5.tgz" @@ -168,6 +236,11 @@ dependencies: "@babel/types" "^7.16.0" +"@babel/helper-environment-visitor@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.6.tgz#ac7ad5517821641550f6698dd5468f8cef78620d" + integrity sha512-Y50Cg3k0LKLMjxdPjIl40SdJgMB85iXn27Vk/qbHZCFx/o5XO3PSnpi675h1KEmmDb6OFArfd5SCQEQ5Q4H88g== + "@babel/helper-explode-assignable-expression@^7.16.0": version "7.16.0" resolved "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.0.tgz" @@ -184,6 +257,14 @@ "@babel/template" "^7.16.0" "@babel/types" "^7.16.0" +"@babel/helper-function-name@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.24.6.tgz#cebdd063386fdb95d511d84b117e51fc68fec0c8" + integrity sha512-xpeLqeeRkbxhnYimfr2PC+iA0Q7ljX/d1eZ9/inYbmfG2jpl8Lu3DyXvpOAnrS5kxkfOWJjioIMQsaMBXFI05w== + dependencies: + "@babel/template" "^7.24.6" + "@babel/types" "^7.24.6" + "@babel/helper-get-function-arity@^7.16.0": version "7.16.0" resolved "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.0.tgz" @@ -198,6 +279,13 @@ dependencies: "@babel/types" "^7.16.0" +"@babel/helper-hoist-variables@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.6.tgz#8a7ece8c26756826b6ffcdd0e3cf65de275af7f9" + integrity sha512-SF/EMrC3OD7dSta1bLJIlrsVxwtd0UpjRJqLno6125epQMJ/kyFmpTT4pbvPbdQHzCHg+biQ7Syo8lnDtbR+uA== + dependencies: + "@babel/types" "^7.24.6" + "@babel/helper-member-expression-to-functions@^7.16.5": version "7.16.5" resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.16.5.tgz" @@ -212,6 +300,13 @@ dependencies: "@babel/types" "^7.16.0" +"@babel/helper-module-imports@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.6.tgz#65e54ffceed6a268dc4ce11f0433b82cfff57852" + integrity sha512-a26dmxFJBF62rRO9mmpgrfTLsAuyHk4e1hKTUkD/fcMfynt8gvEKwQPQDVxWhca8dHoDck+55DFt42zV0QMw5g== + dependencies: + "@babel/types" "^7.24.6" + "@babel/helper-module-transforms@^7.16.5": version "7.16.5" resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.16.5.tgz" @@ -226,6 +321,17 @@ "@babel/traverse" "^7.16.5" "@babel/types" "^7.16.0" +"@babel/helper-module-transforms@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.6.tgz#22346ed9df44ce84dee850d7433c5b73fab1fe4e" + integrity sha512-Y/YMPm83mV2HJTbX1Qh2sjgjqcacvOlhbzdCCsSlblOKjSYmQqEbO6rUniWQyRo9ncyfjT8hnUjlG06RXDEmcA== + dependencies: + "@babel/helper-environment-visitor" "^7.24.6" + "@babel/helper-module-imports" "^7.24.6" + "@babel/helper-simple-access" "^7.24.6" + "@babel/helper-split-export-declaration" "^7.24.6" + "@babel/helper-validator-identifier" "^7.24.6" + "@babel/helper-optimise-call-expression@^7.16.0": version "7.16.0" resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.0.tgz" @@ -238,6 +344,11 @@ resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.5.tgz" integrity sha512-59KHWHXxVA9K4HNF4sbHCf+eJeFe0Te/ZFGqBT4OjXhrwvA04sGfaEGsVTdsjoszq0YTP49RC9UKe5g8uN2RwQ== +"@babel/helper-plugin-utils@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.6.tgz#fa02a32410a15a6e8f8185bcbf608f10528d2a24" + integrity sha512-MZG/JcWfxybKwsA9N9PmtF2lOSFSEMVCpIRrbxccZFLJPrJciJdG/UhSh5W96GEteJI2ARqm5UAHxISwRDLSNg== + "@babel/helper-remap-async-to-generator@^7.16.5": version "7.16.5" resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.5.tgz" @@ -265,6 +376,13 @@ dependencies: "@babel/types" "^7.16.0" +"@babel/helper-simple-access@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.6.tgz#1d6e04d468bba4fc963b4906f6dac6286cfedff1" + integrity sha512-nZzcMMD4ZhmB35MOOzQuiGO5RzL6tJbsT37Zx8M5L/i9KSrukGXWTjLe1knIbb/RmxoJE9GON9soq0c0VEMM5g== + dependencies: + "@babel/types" "^7.24.6" + "@babel/helper-skip-transparent-expression-wrappers@^7.16.0": version "7.16.0" resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.16.0.tgz" @@ -279,16 +397,38 @@ dependencies: "@babel/types" "^7.16.0" +"@babel/helper-split-export-declaration@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz#e830068f7ba8861c53b7421c284da30ae656d7a3" + integrity sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw== + dependencies: + "@babel/types" "^7.24.6" + +"@babel/helper-string-parser@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.6.tgz#28583c28b15f2a3339cfafafeaad42f9a0e828df" + integrity sha512-WdJjwMEkmBicq5T9fm/cHND3+UlFa2Yj8ALLgmoSQAJZysYbBjw+azChSGPN4DSPLXOcooGRvDwZWMcF/mLO2Q== + "@babel/helper-validator-identifier@^7.15.7": version "7.15.7" resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz" integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== +"@babel/helper-validator-identifier@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz#08bb6612b11bdec78f3feed3db196da682454a5e" + integrity sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw== + "@babel/helper-validator-option@^7.14.5": version "7.14.5" resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz" integrity sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow== +"@babel/helper-validator-option@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.6.tgz#59d8e81c40b7d9109ab7e74457393442177f460a" + integrity sha512-Jktc8KkF3zIkePb48QO+IapbXlSapOW9S+ogZZkcO6bABgYAxtZcjZ/O005111YLf+j4M84uEgwYoidDkXbCkQ== + "@babel/helper-wrap-function@^7.16.5": version "7.16.5" resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.16.5.tgz" @@ -308,6 +448,14 @@ "@babel/traverse" "^7.16.5" "@babel/types" "^7.16.0" +"@babel/helpers@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.6.tgz#cd124245299e494bd4e00edda0e4ea3545c2c176" + integrity sha512-V2PI+NqnyFu1i0GyTd/O/cTpxzQCYioSkUIRmgo7gFEHKKCg5w46+r/A6WeUR1+P3TeQ49dspGPNd/E3n9AnnA== + dependencies: + "@babel/template" "^7.24.6" + "@babel/types" "^7.24.6" + "@babel/highlight@^7.16.0": version "7.16.0" resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.0.tgz" @@ -317,11 +465,26 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.6.tgz#6d610c1ebd2c6e061cade0153bf69b0590b7b3df" + integrity sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ== + dependencies: + "@babel/helper-validator-identifier" "^7.24.6" + chalk "^2.4.2" + js-tokens "^4.0.0" + picocolors "^1.0.0" + "@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.0", "@babel/parser@^7.16.5", "@babel/parser@^7.7.2": version "7.16.6" resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.16.6.tgz" integrity sha512-Gr86ujcNuPDnNOY8mi383Hvi8IYrJVJYuf3XcuBM/Dgd+bINn/7tHqsj+tKkoreMbmGsFLsltI/JJd8fOFWGDQ== +"@babel/parser@^7.23.9", "@babel/parser@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.6.tgz#5e030f440c3c6c78d195528c3b688b101a365328" + integrity sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.2": version "7.16.2" resolved "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.2.tgz" @@ -552,6 +715,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.5" +"@babel/plugin-syntax-jsx@^7.7.2": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.6.tgz#bcca2964150437f88f65e3679e3d68762287b9c8" + integrity sha512-lWfvAIFNWMlCsU0DRUun2GpFwZdGTukLaHJqRh1JRb80NdAP5Sb1HDHB5X9P9OtgZHQl089UzQkpYlBq2VTPRw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.6" + "@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": version "7.10.4" resolved "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz" @@ -1083,6 +1253,15 @@ "@babel/parser" "^7.16.0" "@babel/types" "^7.16.0" +"@babel/template@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.6.tgz#048c347b2787a6072b24c723664c8d02b67a44f9" + integrity sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw== + dependencies: + "@babel/code-frame" "^7.24.6" + "@babel/parser" "^7.24.6" + "@babel/types" "^7.24.6" + "@babel/traverse@^7.1.0", "@babel/traverse@^7.13.0", "@babel/traverse@^7.16.5", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.2": version "7.16.5" resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.5.tgz" @@ -1099,6 +1278,22 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.6.tgz#0941ec50cdeaeacad0911eb67ae227a4f8424edc" + integrity sha512-OsNjaJwT9Zn8ozxcfoBc+RaHdj3gFmCmYoQLUII1o6ZrUwku0BMg80FoOTPx+Gi6XhcQxAYE4xyjPTo4SxEQqw== + dependencies: + "@babel/code-frame" "^7.24.6" + "@babel/generator" "^7.24.6" + "@babel/helper-environment-visitor" "^7.24.6" + "@babel/helper-function-name" "^7.24.6" + "@babel/helper-hoist-variables" "^7.24.6" + "@babel/helper-split-export-declaration" "^7.24.6" + "@babel/parser" "^7.24.6" + "@babel/types" "^7.24.6" + debug "^4.3.1" + globals "^11.1.0" + "@babel/types@^7.0.0", "@babel/types@^7.12.6", "@babel/types@^7.16.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": version "7.16.0" resolved "https://registry.npmjs.org/@babel/types/-/types-7.16.0.tgz" @@ -1107,6 +1302,15 @@ "@babel/helper-validator-identifier" "^7.15.7" to-fast-properties "^2.0.0" +"@babel/types@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.6.tgz#ba4e1f59870c10dc2fa95a274ac4feec23b21912" + integrity sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ== + dependencies: + "@babel/helper-string-parser" "^7.24.6" + "@babel/helper-validator-identifier" "^7.24.6" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" @@ -1212,7 +1416,7 @@ js-yaml "^3.13.1" resolve-from "^5.0.0" -"@istanbuljs/schema@^0.1.2": +"@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3": version "0.1.3" resolved "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== @@ -1229,6 +1433,18 @@ jest-util "^27.4.2" slash "^3.0.0" +"@jest/console@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.7.0.tgz#cd4822dbdb84529265c5a2bdb529a3c9cc950ffc" + integrity sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + slash "^3.0.0" + "@jest/core@^27.4.5": version "27.4.5" resolved "https://registry.npmjs.org/@jest/core/-/core-27.4.5.tgz" @@ -1263,6 +1479,40 @@ slash "^3.0.0" strip-ansi "^6.0.0" +"@jest/core@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.7.0.tgz#b6cccc239f30ff36609658c5a5e2291757ce448f" + integrity sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg== + dependencies: + "@jest/console" "^29.7.0" + "@jest/reporters" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + ci-info "^3.2.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-changed-files "^29.7.0" + jest-config "^29.7.0" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-resolve-dependencies "^29.7.0" + jest-runner "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + jest-watcher "^29.7.0" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + strip-ansi "^6.0.0" + "@jest/environment@^27.4.4": version "27.4.4" resolved "https://registry.npmjs.org/@jest/environment/-/environment-27.4.4.tgz" @@ -1273,6 +1523,31 @@ "@types/node" "*" jest-mock "^27.4.2" +"@jest/environment@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.7.0.tgz#24d61f54ff1f786f3cd4073b4b94416383baf2a7" + integrity sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw== + dependencies: + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + +"@jest/expect-utils@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6" + integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== + dependencies: + jest-get-type "^29.6.3" + +"@jest/expect@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.7.0.tgz#76a3edb0cb753b70dfbfe23283510d3d45432bf2" + integrity sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ== + dependencies: + expect "^29.7.0" + jest-snapshot "^29.7.0" + "@jest/fake-timers@^27.4.2": version "27.4.2" resolved "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.4.2.tgz" @@ -1285,6 +1560,18 @@ jest-mock "^27.4.2" jest-util "^27.4.2" +"@jest/fake-timers@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.7.0.tgz#fd91bf1fffb16d7d0d24a426ab1a47a49881a565" + integrity sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ== + dependencies: + "@jest/types" "^29.6.3" + "@sinonjs/fake-timers" "^10.0.2" + "@types/node" "*" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-util "^29.7.0" + "@jest/globals@^27.4.4": version "27.4.4" resolved "https://registry.npmjs.org/@jest/globals/-/globals-27.4.4.tgz" @@ -1294,6 +1581,16 @@ "@jest/types" "^27.4.2" expect "^27.4.2" +"@jest/globals@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.7.0.tgz#8d9290f9ec47ff772607fa864ca1d5a2efae1d4d" + integrity sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/types" "^29.6.3" + jest-mock "^29.7.0" + "@jest/reporters@^27.4.5": version "27.4.5" resolved "https://registry.npmjs.org/@jest/reporters/-/reporters-27.4.5.tgz" @@ -1325,6 +1622,43 @@ terminal-link "^2.0.0" v8-to-istanbul "^8.1.0" +"@jest/reporters@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.7.0.tgz#04b262ecb3b8faa83b0b3d321623972393e8f4c7" + integrity sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + "@types/node" "*" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^6.0.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + jest-worker "^29.7.0" + slash "^3.0.0" + string-length "^4.0.1" + strip-ansi "^6.0.0" + v8-to-istanbul "^9.0.1" + +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + dependencies: + "@sinclair/typebox" "^0.27.8" + "@jest/source-map@^27.4.0": version "27.4.0" resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-27.4.0.tgz" @@ -1334,6 +1668,15 @@ graceful-fs "^4.2.4" source-map "^0.6.0" +"@jest/source-map@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.6.3.tgz#d90ba772095cf37a34a5eb9413f1b562a08554c4" + integrity sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.18" + callsites "^3.0.0" + graceful-fs "^4.2.9" + "@jest/test-result@^27.4.2": version "27.4.2" resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-27.4.2.tgz" @@ -1344,6 +1687,16 @@ "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" +"@jest/test-result@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.7.0.tgz#8db9a80aa1a097bb2262572686734baed9b1657c" + integrity sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA== + dependencies: + "@jest/console" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + "@jest/test-sequencer@^27.4.5": version "27.4.5" resolved "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.4.5.tgz" @@ -1354,6 +1707,16 @@ jest-haste-map "^27.4.5" jest-runtime "^27.4.5" +"@jest/test-sequencer@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz#6cef977ce1d39834a3aea887a1726628a6f072ce" + integrity sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw== + dependencies: + "@jest/test-result" "^29.7.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + slash "^3.0.0" + "@jest/transform@^27.4.5": version "27.4.5" resolved "https://registry.npmjs.org/@jest/transform/-/transform-27.4.5.tgz" @@ -1375,6 +1738,27 @@ source-map "^0.6.1" write-file-atomic "^3.0.0" +"@jest/transform@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.7.0.tgz#df2dd9c346c7d7768b8a06639994640c642e284c" + integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== + dependencies: + "@babel/core" "^7.11.6" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^2.0.0" + fast-json-stable-stringify "^2.1.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + write-file-atomic "^4.0.2" + "@jest/types@^27.4.2": version "27.4.2" resolved "https://registry.npmjs.org/@jest/types/-/types-27.4.2.tgz" @@ -1386,6 +1770,50 @@ "@types/yargs" "^16.0.0" chalk "^4.0.0" +"@jest/types@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" + integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== + dependencies: + "@jest/schemas" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" @@ -1500,6 +1928,11 @@ exenv "^1.2.2" prop-types "^15.6.2" +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + "@sinonjs/commons@^1.7.0": version "1.8.3" resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz" @@ -1507,6 +1940,20 @@ dependencies: type-detect "4.0.8" +"@sinonjs/commons@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" + integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^10.0.2": + version "10.3.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66" + integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== + dependencies: + "@sinonjs/commons" "^3.0.0" + "@sinonjs/fake-timers@^8.0.1": version "8.1.0" resolved "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz" @@ -1628,41 +2075,41 @@ loader-utils "^2.0.0" "@testing-library/dom@^8.0.0": - version "8.11.1" - resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-8.11.1.tgz" - integrity sha512-3KQDyx9r0RKYailW2MiYrSSKEfH0GTkI51UGEvJenvcoDoeRYs0PZpi2SXqtnMClQvCqdtTTpOfFETDTVADpAg== + version "8.20.1" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.20.1.tgz#2e52a32e46fc88369eef7eef634ac2a192decd9f" + integrity sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g== dependencies: "@babel/code-frame" "^7.10.4" "@babel/runtime" "^7.12.5" - "@types/aria-query" "^4.2.0" - aria-query "^5.0.0" + "@types/aria-query" "^5.0.1" + aria-query "5.1.3" chalk "^4.1.0" dom-accessibility-api "^0.5.9" - lz-string "^1.4.4" + lz-string "^1.5.0" pretty-format "^27.0.2" -"@testing-library/jest-dom@^5.16.1": - version "5.16.1" - resolved "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.1.tgz" - integrity sha512-ajUJdfDIuTCadB79ukO+0l8O+QwN0LiSxDaYUTI4LndbbUsGi6rWU1SCexXzBA2NSjlVB9/vbkasQIL3tmPBjw== +"@testing-library/jest-dom@^6.4.5": + version "6.4.5" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.4.5.tgz#badb40296477149136dabef32b572ddd3b56adf1" + integrity sha512-AguB9yvTXmCnySBP1lWjfNNUwpbElsaQ567lt2VdGqAdHtpieLgjmcVyv1q7PMIvLbgpDdkWV5Ydv3FEejyp2A== dependencies: + "@adobe/css-tools" "^4.3.2" "@babel/runtime" "^7.9.2" - "@types/testing-library__jest-dom" "^5.9.1" aria-query "^5.0.0" chalk "^3.0.0" - css "^3.0.0" css.escape "^1.5.1" - dom-accessibility-api "^0.5.6" - lodash "^4.17.15" + dom-accessibility-api "^0.6.3" + lodash "^4.17.21" redent "^3.0.0" -"@testing-library/react@^12.1.2": - version "12.1.2" - resolved "https://registry.npmjs.org/@testing-library/react/-/react-12.1.2.tgz" - integrity sha512-ihQiEOklNyHIpo2Y8FREkyD1QAea054U0MVbwH1m8N9TxeFz+KoJ9LkqoKqJlzx2JDm56DVwaJ1r36JYxZM05g== +"@testing-library/react@12.1.5": + version "12.1.5" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.5.tgz#bb248f72f02a5ac9d949dea07279095fa577963b" + integrity sha512-OfTXCJUFgjd/digLUuPxa0+/3ZxsQmE7ub9kcbW/wi96Bh3o/p5vrETcBGfP17NWPGqeYYl5LTRpwyGoMC4ysg== dependencies: "@babel/runtime" "^7.12.5" "@testing-library/dom" "^8.0.0" + "@types/react-dom" "<18.0.0" "@testing-library/user-event@^13.5.0": version "13.5.0" @@ -1681,10 +2128,10 @@ resolved "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== -"@types/aria-query@^4.2.0": - version "4.2.2" - resolved "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz" - integrity sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig== +"@types/aria-query@^5.0.1": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" + integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": version "7.1.17" @@ -1809,6 +2256,13 @@ dependencies: "@types/node" "*" +"@types/graceful-fs@^4.1.3": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" + integrity sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ== + dependencies: + "@types/node" "*" + "@types/hoist-non-react-statics@*", "@types/hoist-non-react-statics@^3.3.0": version "3.3.1" resolved "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz" @@ -1848,7 +2302,7 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@*", "@types/jest@^27.0.3": +"@types/jest@^27.0.3": version "27.0.3" resolved "https://registry.npmjs.org/@types/jest/-/jest-27.0.3.tgz" integrity sha512-cmmwv9t7gBYt7hNKH5Spu7Kuu/DotGa+Ff+JGRKZ4db5eh8PnKS4LuebJ3YLUoyOyIHraTGyULn23YtEAm0VSg== @@ -1943,6 +2397,13 @@ "@types/react" "*" "@types/reactcss" "*" +"@types/react-dom@<18.0.0": + version "17.0.25" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.25.tgz#e0e5b3571e1069625b3a3da2b279379aa33a0cb5" + integrity sha512-urx7A7UxkZQmThYA4So0NelOVjx3V4rNFVJwp0WZlbIK5eM4rNJDiN3R/E9ix0MBh6kAEojk/9YL+Te6D9zHNA== + dependencies: + "@types/react" "^17" + "@types/react-dom@^17.0.11": version "17.0.11" resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.11.tgz" @@ -1984,6 +2445,15 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@^17": + version "17.0.80" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.80.tgz#a5dfc351d6a41257eb592d73d3a85d3b7dbcbb41" + integrity sha512-LrgHIu2lEtIo8M7d1FcI3BdwXWoRQwMoXOZ7+dPTW0lYREjmlHl3P0U1VD0i/9tppOuv8/sam7sOjx34TxSFbA== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "^0.16" + csstype "^3.0.2" + "@types/reactcss@*": version "1.2.6" resolved "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.6.tgz" @@ -2008,6 +2478,11 @@ resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz" integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== +"@types/scheduler@^0.16": + version "0.16.8" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff" + integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A== + "@types/serve-index@^1.9.1": version "1.9.1" resolved "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz" @@ -2044,13 +2519,6 @@ "@types/react" "*" csstype "^3.0.2" -"@types/testing-library__jest-dom@^5.9.1": - version "5.14.2" - resolved "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.2.tgz" - integrity sha512-vehbtyHUShPxIa9SioxDwCvgxukDMH//icJG90sXQBUm5lJOHLT5kNeU9tnivhnA/TkOFMzGIXN2cTc4hY8/kg== - dependencies: - "@types/jest" "*" - "@types/trusted-types@^2.0.2": version "2.0.2" resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz" @@ -2080,6 +2548,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/yargs@^17.0.8": + version "17.0.32" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.32.tgz#030774723a2f7faafebf645f4e5a48371dca6229" + integrity sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog== + dependencies: + "@types/yargs-parser" "*" + "@typescript-eslint/eslint-plugin@^5.5.0": version "5.8.0" resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.8.0.tgz" @@ -2512,6 +2987,13 @@ argparse@^2.0.1: resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +aria-query@5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" + integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== + dependencies: + deep-equal "^2.0.5" + aria-query@^4.2.2: version "4.2.2" resolved "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz" @@ -2525,6 +3007,14 @@ aria-query@^5.0.0: resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz" integrity sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg== +array-buffer-byte-length@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" + integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== + dependencies: + call-bind "^1.0.5" + is-array-buffer "^3.0.4" + array-flatten@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" @@ -2601,11 +3091,6 @@ at-least-node@^1.0.0: resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== -atob@^2.1.2: - version "2.1.2" - resolved "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - attr-accept@^2.2.1: version "2.2.2" resolved "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz" @@ -2623,6 +3108,13 @@ autoprefixer@^10.4.0: picocolors "^1.0.0" postcss-value-parser "^4.1.0" +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + axe-core@^4.3.5: version "4.3.5" resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.3.5.tgz" @@ -2654,6 +3146,19 @@ babel-jest@^27.4.2, babel-jest@^27.4.5: graceful-fs "^4.2.4" slash "^3.0.0" +babel-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" + integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== + dependencies: + "@jest/transform" "^29.7.0" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^29.6.3" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" + babel-loader@^8.2.3: version "8.2.3" resolved "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.3.tgz" @@ -2671,7 +3176,7 @@ babel-plugin-dynamic-import-node@^2.3.3: dependencies: object.assign "^4.1.0" -babel-plugin-istanbul@^6.0.0: +babel-plugin-istanbul@^6.0.0, babel-plugin-istanbul@^6.1.1: version "6.1.1" resolved "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz" integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== @@ -2692,6 +3197,16 @@ babel-plugin-jest-hoist@^27.4.0: "@types/babel__core" "^7.0.0" "@types/babel__traverse" "^7.0.6" +babel-plugin-jest-hoist@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz#aadbe943464182a8922c3c927c3067ff40d24626" + integrity sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.1.14" + "@types/babel__traverse" "^7.0.6" + babel-plugin-macros@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz" @@ -2776,6 +3291,14 @@ babel-preset-jest@^27.4.0: babel-plugin-jest-hoist "^27.4.0" babel-preset-current-node-syntax "^1.0.0" +babel-preset-jest@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz#fa05fa510e7d493896d7b0dd2033601c840f171c" + integrity sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA== + dependencies: + babel-plugin-jest-hoist "^29.6.3" + babel-preset-current-node-syntax "^1.0.0" + babel-preset-react-app@^10.0.1: version "10.0.1" resolved "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-10.0.1.tgz" @@ -2904,6 +3427,16 @@ browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.0, browserslist@^4 node-releases "^2.0.1" picocolors "^1.0.0" +browserslist@^4.22.2: + version "4.23.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab" + integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== + dependencies: + caniuse-lite "^1.0.30001587" + electron-to-chromium "^1.4.668" + node-releases "^2.0.14" + update-browserslist-db "^1.0.13" + bser@2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz" @@ -2944,6 +3477,17 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" +call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" @@ -2992,6 +3536,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001272, caniuse-lite@^1.0.30001286, can resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001292.tgz" integrity sha512-jnT4Tq0Q4ma+6nncYQVe7d73kmDmE9C3OGTx3MvW7lBM/eY1S1DZTMBON7dqV481RhNiS5OxD7k9JQvmDOTirw== +caniuse-lite@^1.0.30001587: + version "1.0.30001623" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001623.tgz#e982099dcb229bb6ab35f5aebe2f8d79ccf6e8a8" + integrity sha512-X/XhAVKlpIxWPpgRTnlgZssJrF0m6YtRA0QDWgsBNT12uZM6LPRydR7ip405Y3t1LamD8cP2TZFEDZFBf5ApcA== + case-sensitive-paths-webpack-plugin@^2.4.0: version "2.4.0" resolved "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz" @@ -3093,6 +3642,15 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + clsx@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz" @@ -3261,6 +3819,11 @@ convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: dependencies: safe-buffer "~5.1.1" +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" @@ -3321,6 +3884,19 @@ cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: path-type "^4.0.0" yaml "^1.10.0" +create-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320" + integrity sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q== + dependencies: + "@jest/types" "^29.6.3" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-config "^29.7.0" + jest-util "^29.7.0" + prompts "^2.0.1" + cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" @@ -3465,15 +4041,6 @@ css.escape@^1.5.1: resolved "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz" integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s= -css@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/css/-/css-3.0.0.tgz" - integrity sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ== - dependencies: - inherits "^2.0.4" - source-map "^0.6.1" - source-map-resolve "^0.6.0" - cssdb@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/cssdb/-/cssdb-5.0.0.tgz" @@ -3597,21 +4164,28 @@ debug@^3.1.1, debug@^3.2.7: dependencies: ms "^2.1.1" +debug@^4.3.1: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + decimal.js@^10.2.1: version "10.3.1" resolved "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz" integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== -decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz" - integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= - dedent@^0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= +dedent@^1.0.0: + version "1.5.3" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a" + integrity sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ== + deep-equal@^1.0.1, deep-equal@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz" @@ -3624,6 +4198,30 @@ deep-equal@^1.0.1, deep-equal@^1.1.1: object-keys "^1.1.1" regexp.prototype.flags "^1.2.0" +deep-equal@^2.0.5: + version "2.2.3" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.3.tgz#af89dafb23a396c7da3e862abc0be27cf51d56e1" + integrity sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA== + dependencies: + array-buffer-byte-length "^1.0.0" + call-bind "^1.0.5" + es-get-iterator "^1.1.3" + get-intrinsic "^1.2.2" + is-arguments "^1.1.1" + is-array-buffer "^3.0.2" + is-date-object "^1.0.5" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + isarray "^2.0.5" + object-is "^1.1.5" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.5.1" + side-channel "^1.0.4" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.1" + which-typed-array "^1.1.13" + deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.4" resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" @@ -3641,6 +4239,15 @@ default-gateway@^6.0.3: dependencies: execa "^5.0.0" +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + define-lazy-prop@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz" @@ -3653,6 +4260,15 @@ define-properties@^1.1.3: dependencies: object-keys "^1.0.12" +define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + defined@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz" @@ -3724,6 +4340,11 @@ diff-sequences@^27.4.0: resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.4.0.tgz" integrity sha512-YqiQzkrsmHMH5uuh8OdQFU9/ZpADnwzml8z0O5HvRNda+5UZsaX/xN+AAxfR2hWq1Y7HZnAzO9J5lJXOuDz2Ww== +diff-sequences@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" + integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" @@ -3770,11 +4391,16 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9: +dom-accessibility-api@^0.5.9: version "0.5.10" resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.10.tgz" integrity sha512-Xu9mD0UjrJisTmv7lmVSDMagQcU9R5hwAbxsaAE/35XPnPLJobbuREfV/rraiSaEj/UOvgrzQs66zyTWTlyd+g== +dom-accessibility-api@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8" + integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== + dom-converter@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz" @@ -3885,6 +4511,16 @@ electron-to-chromium@^1.4.17: resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.26.tgz" integrity sha512-cA1YwlRzO6TGp7yd3+KAqh9Tt6Z4CuuKqsAJP6uF/H5MQryjAGDhMhnY5cEXo8MaRCczpzSBhMPdqRIodkbZYw== +electron-to-chromium@^1.4.668: + version "1.4.783" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.783.tgz#933887165b8b6025a81663d2d97cf4b85cde27b2" + integrity sha512-bT0jEz/Xz1fahQpbZ1D7LgmPYZ3iHVY39NcWWro1+hA2IvjiPeaXtfSqrQ+nXjApMvQRE2ASt1itSLRrebHMRQ== + +emittery@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" + integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== + emittery@^0.8.1: version "0.8.1" resolved "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz" @@ -3975,6 +4611,33 @@ es-cookie@^1.3.2: resolved "https://registry.npmjs.org/es-cookie/-/es-cookie-1.3.2.tgz" integrity sha512-UTlYYhXGLOy05P/vKVT2Ui7WtC7NiRzGtJyAKKn32g5Gvcjn7KAClLPWlipCtxIus934dFg9o9jXiBL0nP+t9Q== +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-get-iterator@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" + integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + has-symbols "^1.0.3" + is-arguments "^1.1.1" + is-map "^2.0.2" + is-set "^2.0.2" + is-string "^1.0.7" + isarray "^2.0.5" + stop-iteration-iterator "^1.0.0" + es-module-lexer@^0.9.0: version "0.9.3" resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz" @@ -3994,6 +4657,11 @@ escalade@^3.1.1: resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escalade@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" @@ -4335,6 +5003,17 @@ expect@^27.4.2: jest-message-util "^27.4.2" jest-regex-util "^27.4.0" +expect@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" + integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== + dependencies: + "@jest/expect-utils" "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + express@^4.17.1: version "4.17.2" resolved "https://registry.npmjs.org/express/-/express-4.17.2.tgz" @@ -4534,6 +5213,13 @@ follow-redirects@^1.0.0, follow-redirects@^1.14.4: resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.6.tgz" integrity sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A== +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + fork-ts-checker-webpack-plugin@^6.5.0: version "6.5.0" resolved "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.0.tgz" @@ -4616,11 +5302,21 @@ function-bind@^1.1.1: resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + fuse.js@^6.5.3: version "6.5.3" resolved "https://registry.npmjs.org/fuse.js/-/fuse.js-6.5.3.tgz" @@ -4645,6 +5341,17 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.1" +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" resolved "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz" @@ -4739,11 +5446,23 @@ globby@^11.0.1, globby@^11.0.4: merge2 "^1.3.0" slash "^3.0.0" +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6: version "4.2.8" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz" integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== +graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + graphql-tag@^2.12.6: version "2.12.6" resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1" @@ -4793,11 +5512,28 @@ has-flag@^4.0.0: resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + has-symbols@^1.0.1, has-symbols@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz" integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + has-tostringtag@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz" @@ -4805,6 +5541,13 @@ has-tostringtag@^1.0.0: dependencies: has-symbols "^1.0.2" +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + has@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" @@ -4812,6 +5555,13 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + he@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" @@ -5069,7 +5819,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -5093,6 +5843,15 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" +internal-slot@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" + integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.0" + side-channel "^1.0.4" + ip@^1.1.0: version "1.1.5" resolved "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz" @@ -5108,7 +5867,7 @@ ipaddr.js@^2.0.1: resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz" integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== -is-arguments@^1.0.4: +is-arguments@^1.0.4, is-arguments@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz" integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== @@ -5116,6 +5875,14 @@ is-arguments@^1.0.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-array-buffer@^3.0.2, is-array-buffer@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" + integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" @@ -5143,6 +5910,11 @@ is-boolean-object@^1.1.0: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-callable@^1.1.3: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + is-callable@^1.1.4, is-callable@^1.2.4: version "1.2.4" resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz" @@ -5155,7 +5927,7 @@ is-core-module@^2.2.0, is-core-module@^2.8.0: dependencies: has "^1.0.3" -is-date-object@^1.0.1: +is-date-object@^1.0.1, is-date-object@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz" integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== @@ -5189,6 +5961,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-map@^2.0.2, is-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + is-module@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz" @@ -5254,11 +6031,23 @@ is-root@^2.1.0: resolved "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz" integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== +is-set@^2.0.2, is-set@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + is-shared-array-buffer@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz" integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== +is-shared-array-buffer@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" + integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== + dependencies: + call-bind "^1.0.7" + is-stream@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" @@ -5283,6 +6072,11 @@ is-typedarray@^1.0.0: resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +is-weakmap@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== + is-weakref@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz" @@ -5290,6 +6084,14 @@ is-weakref@^1.0.1: dependencies: call-bind "^1.0.2" +is-weakset@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.3.tgz#e801519df8c0c43e12ff2834eead84ec9e624007" + integrity sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + is-wsl@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz" @@ -5297,6 +6099,11 @@ is-wsl@^2.2.0: dependencies: is-docker "^2.0.0" +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isarray@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" @@ -5333,6 +6140,17 @@ istanbul-lib-instrument@^5.0.4: istanbul-lib-coverage "^3.2.0" semver "^6.3.0" +istanbul-lib-instrument@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz#91655936cf7380e4e473383081e38478b69993b1" + integrity sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw== + dependencies: + "@babel/core" "^7.23.9" + "@babel/parser" "^7.23.9" + "@istanbuljs/schema" "^0.1.3" + istanbul-lib-coverage "^3.2.0" + semver "^7.5.4" + istanbul-lib-report@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz" @@ -5359,6 +6177,14 @@ istanbul-reports@^3.0.2: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" +istanbul-reports@^3.1.3: + version "3.1.7" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b" + integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + jake@^10.6.1: version "10.8.2" resolved "https://registry.npmjs.org/jake/-/jake-10.8.2.tgz" @@ -5378,6 +6204,15 @@ jest-changed-files@^27.4.2: execa "^5.0.0" throat "^6.0.1" +jest-changed-files@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a" + integrity sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w== + dependencies: + execa "^5.0.0" + jest-util "^29.7.0" + p-limit "^3.1.0" + jest-circus@^27.4.5: version "27.4.5" resolved "https://registry.npmjs.org/jest-circus/-/jest-circus-27.4.5.tgz" @@ -5403,6 +6238,32 @@ jest-circus@^27.4.5: stack-utils "^2.0.3" throat "^6.0.1" +jest-circus@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.7.0.tgz#b6817a45fcc835d8b16d5962d0c026473ee3668a" + integrity sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^1.0.0" + is-generator-fn "^2.0.0" + jest-each "^29.7.0" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + p-limit "^3.1.0" + pretty-format "^29.7.0" + pure-rand "^6.0.0" + slash "^3.0.0" + stack-utils "^2.0.3" + jest-cli@^27.4.5: version "27.4.5" resolved "https://registry.npmjs.org/jest-cli/-/jest-cli-27.4.5.tgz" @@ -5421,6 +6282,23 @@ jest-cli@^27.4.5: prompts "^2.0.1" yargs "^16.2.0" +jest-cli@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.7.0.tgz#5592c940798e0cae677eec169264f2d839a37995" + integrity sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg== + dependencies: + "@jest/core" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + chalk "^4.0.0" + create-jest "^29.7.0" + exit "^0.1.2" + import-local "^3.0.2" + jest-config "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + yargs "^17.3.1" + jest-config@^27.4.5: version "27.4.5" resolved "https://registry.npmjs.org/jest-config/-/jest-config-27.4.5.tgz" @@ -5449,6 +6327,34 @@ jest-config@^27.4.5: pretty-format "^27.4.2" slash "^3.0.0" +jest-config@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.7.0.tgz#bcbda8806dbcc01b1e316a46bb74085a84b0245f" + integrity sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ== + dependencies: + "@babel/core" "^7.11.6" + "@jest/test-sequencer" "^29.7.0" + "@jest/types" "^29.6.3" + babel-jest "^29.7.0" + chalk "^4.0.0" + ci-info "^3.2.0" + deepmerge "^4.2.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-circus "^29.7.0" + jest-environment-node "^29.7.0" + jest-get-type "^29.6.3" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-runner "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^29.7.0" + slash "^3.0.0" + strip-json-comments "^3.1.1" + jest-diff@^27.0.0, jest-diff@^27.4.2: version "27.4.2" resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-27.4.2.tgz" @@ -5459,6 +6365,16 @@ jest-diff@^27.0.0, jest-diff@^27.4.2: jest-get-type "^27.4.0" pretty-format "^27.4.2" +jest-diff@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" + integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.6.3" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + jest-docblock@^27.4.0: version "27.4.0" resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.4.0.tgz" @@ -5466,6 +6382,13 @@ jest-docblock@^27.4.0: dependencies: detect-newline "^3.0.0" +jest-docblock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.7.0.tgz#8fddb6adc3cdc955c93e2a87f61cfd350d5d119a" + integrity sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g== + dependencies: + detect-newline "^3.0.0" + jest-each@^27.4.2: version "27.4.2" resolved "https://registry.npmjs.org/jest-each/-/jest-each-27.4.2.tgz" @@ -5477,6 +6400,17 @@ jest-each@^27.4.2: jest-util "^27.4.2" pretty-format "^27.4.2" +jest-each@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.7.0.tgz#162a9b3f2328bdd991beaabffbb74745e56577d1" + integrity sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ== + dependencies: + "@jest/types" "^29.6.3" + chalk "^4.0.0" + jest-get-type "^29.6.3" + jest-util "^29.7.0" + pretty-format "^29.7.0" + jest-environment-jsdom@^27.4.4: version "27.4.4" resolved "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.4.4.tgz" @@ -5502,11 +6436,28 @@ jest-environment-node@^27.4.4: jest-mock "^27.4.2" jest-util "^27.4.2" +jest-environment-node@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.7.0.tgz#0b93e111dda8ec120bc8300e6d1fb9576e164376" + integrity sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + jest-util "^29.7.0" + jest-get-type@^27.4.0: version "27.4.0" resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.4.0.tgz" integrity sha512-tk9o+ld5TWq41DkK14L4wox4s2D9MtTpKaAVzXfr5CUKm5ZK2ExcaFE0qls2W71zE/6R2TxxrK9w2r6svAFDBQ== +jest-get-type@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" + integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== + jest-haste-map@^27.4.5: version "27.4.5" resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.4.5.tgz" @@ -5527,6 +6478,25 @@ jest-haste-map@^27.4.5: optionalDependencies: fsevents "^2.3.2" +jest-haste-map@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.7.0.tgz#3c2396524482f5a0506376e6c858c3bbcc17b104" + integrity sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA== + dependencies: + "@jest/types" "^29.6.3" + "@types/graceful-fs" "^4.1.3" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.9" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + jest-worker "^29.7.0" + micromatch "^4.0.4" + walker "^1.0.8" + optionalDependencies: + fsevents "^2.3.2" + jest-jasmine2@^27.4.5: version "27.4.5" resolved "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.4.5.tgz" @@ -5559,6 +6529,14 @@ jest-leak-detector@^27.4.2: jest-get-type "^27.4.0" pretty-format "^27.4.2" +jest-leak-detector@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz#5b7ec0dadfdfec0ca383dc9aa016d36b5ea4c728" + integrity sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw== + dependencies: + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + jest-matcher-utils@^27.4.2: version "27.4.2" resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.4.2.tgz" @@ -5569,6 +6547,16 @@ jest-matcher-utils@^27.4.2: jest-get-type "^27.4.0" pretty-format "^27.4.2" +jest-matcher-utils@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12" + integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== + dependencies: + chalk "^4.0.0" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + jest-message-util@^27.4.2: version "27.4.2" resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.4.2.tgz" @@ -5584,6 +6572,21 @@ jest-message-util@^27.4.2: slash "^3.0.0" stack-utils "^2.0.3" +jest-message-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" + integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.6.3" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + stack-utils "^2.0.3" + jest-mock@^27.4.2: version "27.4.2" resolved "https://registry.npmjs.org/jest-mock/-/jest-mock-27.4.2.tgz" @@ -5592,6 +6595,15 @@ jest-mock@^27.4.2: "@jest/types" "^27.4.2" "@types/node" "*" +jest-mock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.7.0.tgz#4e836cf60e99c6fcfabe9f99d017f3fdd50a6347" + integrity sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-util "^29.7.0" + jest-pnp-resolver@^1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz" @@ -5602,6 +6614,11 @@ jest-regex-util@^27.0.0, jest-regex-util@^27.4.0: resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.4.0.tgz" integrity sha512-WeCpMpNnqJYMQoOjm1nTtsgbR4XHAk1u00qDoNBQoykM280+/TmgA5Qh5giC1ecy6a5d4hbSsHzpBtu5yvlbEg== +jest-regex-util@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz#4a556d9c776af68e1c5f48194f4d0327d24e8a52" + integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== + jest-resolve-dependencies@^27.4.5: version "27.4.5" resolved "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.4.5.tgz" @@ -5611,6 +6628,14 @@ jest-resolve-dependencies@^27.4.5: jest-regex-util "^27.4.0" jest-snapshot "^27.4.5" +jest-resolve-dependencies@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz#1b04f2c095f37fc776ff40803dc92921b1e88428" + integrity sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA== + dependencies: + jest-regex-util "^29.6.3" + jest-snapshot "^29.7.0" + jest-resolve@^27.4.2, jest-resolve@^27.4.5: version "27.4.5" resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.4.5.tgz" @@ -5627,6 +6652,21 @@ jest-resolve@^27.4.2, jest-resolve@^27.4.5: resolve.exports "^1.1.0" slash "^3.0.0" +jest-resolve@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.7.0.tgz#64d6a8992dd26f635ab0c01e5eef4399c6bcbc30" + integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== + dependencies: + chalk "^4.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-pnp-resolver "^1.2.2" + jest-util "^29.7.0" + jest-validate "^29.7.0" + resolve "^1.20.0" + resolve.exports "^2.0.0" + slash "^3.0.0" + jest-runner@^27.4.5: version "27.4.5" resolved "https://registry.npmjs.org/jest-runner/-/jest-runner-27.4.5.tgz" @@ -5655,6 +6695,33 @@ jest-runner@^27.4.5: source-map-support "^0.5.6" throat "^6.0.1" +jest-runner@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.7.0.tgz#809af072d408a53dcfd2e849a4c976d3132f718e" + integrity sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ== + dependencies: + "@jest/console" "^29.7.0" + "@jest/environment" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.13.1" + graceful-fs "^4.2.9" + jest-docblock "^29.7.0" + jest-environment-node "^29.7.0" + jest-haste-map "^29.7.0" + jest-leak-detector "^29.7.0" + jest-message-util "^29.7.0" + jest-resolve "^29.7.0" + jest-runtime "^29.7.0" + jest-util "^29.7.0" + jest-watcher "^29.7.0" + jest-worker "^29.7.0" + p-limit "^3.1.0" + source-map-support "0.5.13" + jest-runtime@^27.4.5: version "27.4.5" resolved "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.4.5.tgz" @@ -5687,6 +6754,34 @@ jest-runtime@^27.4.5: strip-bom "^4.0.0" yargs "^16.2.0" +jest-runtime@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.7.0.tgz#efecb3141cf7d3767a3a0cc8f7c9990587d3d817" + integrity sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/globals" "^29.7.0" + "@jest/source-map" "^29.6.3" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + cjs-module-lexer "^1.0.0" + collect-v8-coverage "^1.0.0" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + slash "^3.0.0" + strip-bom "^4.0.0" + jest-serializer@^27.4.0: version "27.4.0" resolved "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.4.0.tgz" @@ -5725,6 +6820,32 @@ jest-snapshot@^27.4.5: pretty-format "^27.4.2" semver "^7.3.2" +jest-snapshot@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.7.0.tgz#c2c574c3f51865da1bb329036778a69bf88a6be5" + integrity sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw== + dependencies: + "@babel/core" "^7.11.6" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-jsx" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/types" "^7.3.3" + "@jest/expect-utils" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^29.7.0" + graceful-fs "^4.2.9" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + natural-compare "^1.4.0" + pretty-format "^29.7.0" + semver "^7.5.3" + jest-util@^27.4.2: version "27.4.2" resolved "https://registry.npmjs.org/jest-util/-/jest-util-27.4.2.tgz" @@ -5737,6 +6858,18 @@ jest-util@^27.4.2: graceful-fs "^4.2.4" picomatch "^2.2.3" +jest-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" + integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + jest-validate@^27.4.2: version "27.4.2" resolved "https://registry.npmjs.org/jest-validate/-/jest-validate-27.4.2.tgz" @@ -5749,6 +6882,18 @@ jest-validate@^27.4.2: leven "^3.1.0" pretty-format "^27.4.2" +jest-validate@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.7.0.tgz#7bf705511c64da591d46b15fce41400d52147d9c" + integrity sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw== + dependencies: + "@jest/types" "^29.6.3" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^29.6.3" + leven "^3.1.0" + pretty-format "^29.7.0" + jest-watch-typeahead@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-1.0.0.tgz" @@ -5775,6 +6920,20 @@ jest-watcher@^27.0.0, jest-watcher@^27.4.2: jest-util "^27.4.2" string-length "^4.0.1" +jest-watcher@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.7.0.tgz#7810d30d619c3a62093223ce6bb359ca1b28a2f2" + integrity sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g== + dependencies: + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + emittery "^0.13.1" + jest-util "^29.7.0" + string-length "^4.0.1" + jest-worker@^26.2.1: version "26.6.2" resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz" @@ -5793,6 +6952,16 @@ jest-worker@^27.0.2, jest-worker@^27.3.1, jest-worker@^27.4.1, jest-worker@^27.4 merge-stream "^2.0.0" supports-color "^8.0.0" +jest-worker@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" + integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== + dependencies: + "@types/node" "*" + jest-util "^29.7.0" + merge-stream "^2.0.0" + supports-color "^8.0.0" + jest@^27.4.3: version "27.4.5" resolved "https://registry.npmjs.org/jest/-/jest-27.4.5.tgz" @@ -5802,6 +6971,16 @@ jest@^27.4.3: import-local "^3.0.2" jest-cli "^27.4.5" +jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.7.0.tgz#994676fc24177f088f1c5e3737f5697204ff2613" + integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== + dependencies: + "@jest/core" "^29.7.0" + "@jest/types" "^29.6.3" + import-local "^3.0.2" + jest-cli "^29.7.0" + jquery@x.*: version "3.6.0" resolved "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz" @@ -5930,6 +7109,11 @@ json5@^2.1.2, json5@^2.2.0: dependencies: minimist "^1.2.5" +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + jsonfile@^6.0.1: version "6.1.0" resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz" @@ -6127,6 +7311,13 @@ lower-case@^2.0.2: dependencies: tslib "^2.0.3" +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" @@ -6134,10 +7325,10 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -lz-string@^1.4.4: - version "1.4.4" - resolved "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz" - integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= +lz-string@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" + integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== magic-string@^0.25.0, magic-string@^0.25.7: version "0.25.7" @@ -6374,6 +7565,11 @@ node-releases@^2.0.1: resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz" integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== +node-releases@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" + integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== + normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" @@ -6438,6 +7634,14 @@ object-is@^1.0.1: call-bind "^1.0.2" define-properties "^1.1.3" +object-is@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" + integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" @@ -6453,6 +7657,16 @@ object.assign@^4.1.0, object.assign@^4.1.2: has-symbols "^1.0.1" object-keys "^1.1.1" +object.assign@^4.1.4: + version "4.1.5" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" + integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== + dependencies: + call-bind "^1.0.5" + define-properties "^1.2.1" + has-symbols "^1.0.3" + object-keys "^1.1.1" + object.entries@^1.1.5: version "1.1.5" resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz" @@ -6583,7 +7797,7 @@ p-limit@^2.0.0, p-limit@^2.2.0: dependencies: p-try "^2.0.0" -p-limit@^3.0.2: +p-limit@^3.0.2, p-limit@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== @@ -6658,7 +7872,7 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-json@^5.0.0: +parse-json@^5.0.0, parse-json@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== @@ -6756,6 +7970,11 @@ picocolors@^1.0.0: resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3: version "2.3.0" resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz" @@ -6766,6 +7985,11 @@ pirates@^4.0.1: resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.4.tgz" integrity sha512-ZIrVPH+A52Dw84R0L3/VS9Op04PuQ2SEoJL6bkshmiTic/HldyW9Tf7oH5mhJZBK7NmDx27vSMrYEXPXclpDKw== +pirates@^4.0.4: + version "4.0.6" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" + integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== + pkg-dir@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz" @@ -6801,6 +8025,11 @@ portfinder@^1.0.28: debug "^3.1.1" mkdirp "^0.5.5" +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + postcss-attribute-case-insensitive@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.0.tgz" @@ -7355,6 +8584,15 @@ pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.4.2: ansi-styles "^5.0.0" react-is "^17.0.1" +pretty-format@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" + integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== + dependencies: + "@jest/schemas" "^29.6.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" @@ -7440,6 +8678,11 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +pure-rand@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" + integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== + q@^1.1.2: version "1.5.1" resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz" @@ -7622,6 +8865,11 @@ react-is@^16.13.1, react-is@^16.6.3, react-is@^16.7.0, react-is@^16.8.1, react-i resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-is@^18.0.0: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" + integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== + react-pdf@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/react-pdf/-/react-pdf-5.7.1.tgz#d540cee152b2747763b0173ed70ab69be5b64a5b" @@ -7865,6 +9113,16 @@ regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.3.1: call-bind "^1.0.2" define-properties "^1.1.3" +regexp.prototype.flags@^1.5.1: + version "1.5.2" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" + integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== + dependencies: + call-bind "^1.0.6" + define-properties "^1.2.1" + es-errors "^1.3.0" + set-function-name "^2.0.1" + regexpp@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz" @@ -7958,6 +9216,11 @@ resolve.exports@^1.1.0: resolved "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz" integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== +resolve.exports@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" + integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== + resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0: version "1.20.0" resolved "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz" @@ -8168,6 +9431,11 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + semver@^7.2.1, semver@^7.3.2, semver@^7.3.5: version "7.3.5" resolved "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz" @@ -8175,6 +9443,11 @@ semver@^7.2.1, semver@^7.3.2, semver@^7.3.5: dependencies: lru-cache "^6.0.0" +semver@^7.5.3, semver@^7.5.4: + version "7.6.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== + send@0.17.2: version "0.17.2" resolved "https://registry.npmjs.org/send/-/send-0.17.2.tgz" @@ -8231,6 +9504,28 @@ serve-static@1.14.2: parseurl "~1.3.3" send "0.17.2" +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +set-function-name@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + setprototypeof@1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz" @@ -8277,6 +9572,11 @@ signal-exit@^3.0.2, signal-exit@^3.0.3: resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz" integrity sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ== +signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz" @@ -8325,13 +9625,13 @@ source-map-loader@^3.0.0: iconv-lite "^0.6.2" source-map-js "^0.6.2" -source-map-resolve@^0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz" - integrity sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w== +source-map-support@0.5.13: + version "0.5.13" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== dependencies: - atob "^2.1.2" - decode-uri-component "^0.2.0" + buffer-from "^1.0.0" + source-map "^0.6.0" source-map-support@^0.5.6, source-map-support@~0.5.20: version "0.5.21" @@ -8423,6 +9723,13 @@ stackframe@^1.1.1: resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= +stop-iteration-iterator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" + integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== + dependencies: + internal-slot "^1.0.4" + string-length@^4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" @@ -8444,7 +9751,7 @@ string-natural-compare@^3.0.1: resolved "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -string-width@^4.1.0, string-width@^4.2.0: +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -9011,6 +10318,14 @@ upath@^1.2.0: resolved "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz" integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== +update-browserslist-db@^1.0.13: + version "1.0.16" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz#f6d489ed90fb2f07d67784eb3f53d7891f736356" + integrity sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ== + dependencies: + escalade "^3.1.2" + picocolors "^1.0.1" + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" @@ -9075,6 +10390,15 @@ v8-to-istanbul@^8.1.0: convert-source-map "^1.6.0" source-map "^0.7.3" +v8-to-istanbul@^9.0.1: + version "9.2.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz#2ed7644a245cddd83d4e087b9b33b3e62dfd10ad" + integrity sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.12" + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^2.0.0" + validate.io-array@^1.0.3: version "1.0.6" resolved "https://registry.npmjs.org/validate.io-array/-/validate.io-array-1.0.6.tgz" @@ -9124,7 +10448,7 @@ w3c-xmlserializer@^2.0.0: dependencies: xml-name-validator "^3.0.0" -walker@^1.0.7: +walker@^1.0.7, walker@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz" integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== @@ -9344,6 +10668,27 @@ which-boxed-primitive@^1.0.2: is-string "^1.0.5" is-symbol "^1.0.3" +which-collection@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== + dependencies: + is-map "^2.0.3" + is-set "^2.0.3" + is-weakmap "^2.0.2" + is-weakset "^2.0.3" + +which-typed-array@^1.1.13: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" + which@^1.3.1: version "1.3.1" resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz" @@ -9566,6 +10911,14 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" +write-file-atomic@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" + integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + ws@^7.4.6: version "7.5.6" resolved "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz" @@ -9596,6 +10949,11 @@ y18n@^5.0.5: resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + yallist@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" @@ -9611,6 +10969,11 @@ yargs-parser@^20.2.2: resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + yargs@^16.2.0: version "16.2.0" resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" @@ -9624,6 +10987,19 @@ yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" +yargs@^17.3.1: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" diff --git a/opencontractserver/extracts/migrations/0003_auto_20240527_2116.py b/opencontractserver/extracts/migrations/0003_auto_20240527_2116.py new file mode 100644 index 00000000..c95c702d --- /dev/null +++ b/opencontractserver/extracts/migrations/0003_auto_20240527_2116.py @@ -0,0 +1,77 @@ +# Generated by Django 3.2.9 on 2024-05-27 21:16 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import opencontractserver.shared.defaults +import opencontractserver.shared.fields + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('auth', '0012_alter_user_first_name_max_length'), + ('documents', '0005_document_embedding'), + ('extracts', '0002_auto_20240527_0445'), + ] + + operations = [ + migrations.CreateModel( + name='Datacell', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('backend_lock', models.BooleanField(default=False)), + ('is_public', models.BooleanField(default=False)), + ('created', models.DateTimeField(auto_now_add=True)), + ('modified', models.DateTimeField(auto_now=True)), + ('data', opencontractserver.shared.fields.NullableJSONField(blank=True, default=opencontractserver.shared.defaults.jsonfield_default_value, null=True)), + ('data_definition', models.TextField()), + ('started', models.DateTimeField(blank=True, null=True)), + ('completed', models.DateTimeField(blank=True, null=True)), + ('failed', models.DateTimeField(blank=True, null=True)), + ('stacktrace', models.TextField(blank=True, null=True)), + ('column', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extracted_datacels', to='extracts.column')), + ('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extracted_datacels', to='documents.document')), + ], + options={ + 'permissions': (('permission_datacell', 'permission datacell'), ('create_datacell', 'create datacell'), ('read_datacell', 'read datacell'), ('update_datacell', 'update datacell'), ('remove_datacell', 'delete datacell')), + }, + ), + migrations.RemoveField( + model_name='extract', + name='stacktrace', + ), + migrations.RenameModel( + old_name='RowGroupObjectPermission', + new_name='DatacellGroupObjectPermission', + ), + migrations.RenameModel( + old_name='RowUserObjectPermission', + new_name='DatacellUserObjectPermission', + ), + migrations.DeleteModel( + name='Row', + ), + migrations.AddField( + model_name='datacell', + name='extract', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extracted_datacels', to='extracts.extract'), + ), + migrations.AddField( + model_name='datacell', + name='user_lock', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='locked_datacell_objects', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='datacellgroupobjectpermission', + name='content_object', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.datacell'), + ), + migrations.AlterField( + model_name='datacelluserobjectpermission', + name='content_object', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.datacell'), + ), + ] diff --git a/opencontractserver/extracts/models.py b/opencontractserver/extracts/models.py index 74f1fc9e..8ef6d50b 100644 --- a/opencontractserver/extracts/models.py +++ b/opencontractserver/extracts/models.py @@ -3,6 +3,7 @@ from guardian.models import GroupObjectPermissionBase, UserObjectPermissionBase from opencontractserver.corpuses.models import Corpus +from opencontractserver.documents.models import Document from opencontractserver.shared.defaults import jsonfield_default_value from opencontractserver.shared.fields import NullableJSONField from opencontractserver.shared.Models import BaseOCModel @@ -132,7 +133,6 @@ class Extract(BaseOCModel): created = django.db.models.DateTimeField(auto_now_add=True) started = django.db.models.DateTimeField(null=True, blank=True) finished = django.db.models.DateTimeField(null=True, blank=True) - stacktrace = django.db.models.TextField(null=True, blank=True) class Meta: permissions = ( @@ -156,12 +156,15 @@ class ExtractGroupObjectPermission(GroupObjectPermissionBase): ) -class Row(BaseOCModel): +class Datacell(BaseOCModel): extract = django.db.models.ForeignKey( - "Extract", related_name="rows", on_delete=django.db.models.CASCADE + "Extract", related_name="extracted_datacels", on_delete=django.db.models.CASCADE ) column = django.db.models.ForeignKey( - "Column", related_name="rows", on_delete=django.db.models.CASCADE + "Column", related_name="extracted_datacels", on_delete=django.db.models.CASCADE + ) + document = django.db.models.ForeignKey( + Document, related_name="extracted_datacels", on_delete=django.db.models.CASCADE ) data = NullableJSONField(default=jsonfield_default_value, null=True, blank=True) data_definition = django.db.models.TextField(null=False, blank=False) @@ -172,21 +175,21 @@ class Row(BaseOCModel): class Meta: permissions = ( - ("permission_row", "permission row"), - ("create_row", "create row"), - ("read_row", "read row"), - ("update_row", "update row"), - ("remove_row", "delete row"), + ("permission_datacell", "permission datacell"), + ("create_datacell", "create datacell"), + ("read_datacell", "read datacell"), + ("update_datacell", "update datacell"), + ("remove_datacell", "delete datacell"), ) -class RowUserObjectPermission(UserObjectPermissionBase): +class DatacellUserObjectPermission(UserObjectPermissionBase): content_object = django.db.models.ForeignKey( - "Row", on_delete=django.db.models.CASCADE + "Datacell", on_delete=django.db.models.CASCADE ) -class RowGroupObjectPermission(GroupObjectPermissionBase): +class DatacellGroupObjectPermission(GroupObjectPermissionBase): content_object = django.db.models.ForeignKey( - "Row", on_delete=django.db.models.CASCADE + "Datacell", on_delete=django.db.models.CASCADE ) diff --git a/opencontractserver/tasks/extract_tasks.py b/opencontractserver/tasks/extract_tasks.py index c7b14868..2409b98b 100644 --- a/opencontractserver/tasks/extract_tasks.py +++ b/opencontractserver/tasks/extract_tasks.py @@ -6,7 +6,7 @@ from pgvector.django import L2Distance from opencontractserver.annotations.models import Annotation -from opencontractserver.extracts.models import Extract, Row +from opencontractserver.extracts.models import Extract, DataCell from opencontractserver.types.enums import PermissionTypes from opencontractserver.utils.embeddings import calculate_embedding_for_text from opencontractserver.utils.permissioning import set_permissions_for_obj_to_user @@ -43,7 +43,7 @@ def run_extract(extract_id, user_id): for column in fieldset.columns.all(): with transaction.atomic(): - row = Row.objects.create( + row = DataCell.objects.create( extract=extract, column=column, data_definition=column.output_type, diff --git a/opencontractserver/tests/test_extract_queries.py b/opencontractserver/tests/test_extract_queries.py index abb41850..8ee66e46 100644 --- a/opencontractserver/tests/test_extract_queries.py +++ b/opencontractserver/tests/test_extract_queries.py @@ -10,7 +10,7 @@ Extract, Fieldset, LanguageModel, - Row, + DataCell, ) User = get_user_model() @@ -53,7 +53,7 @@ def setUp(self): owner=self.user, creator=self.user, ) - self.row = Row.objects.create( + self.row = DataCell.objects.create( extract=self.extract, column=self.column, data={"data": "TestData"}, diff --git a/opencontractserver/tests/test_extract_tasks.py b/opencontractserver/tests/test_extract_tasks.py index a3c71c61..65c87a6d 100644 --- a/opencontractserver/tests/test_extract_tasks.py +++ b/opencontractserver/tests/test_extract_tasks.py @@ -12,7 +12,7 @@ Extract, Fieldset, LanguageModel, - Row, + DataCell, ) from opencontractserver.tasks.extract_tasks import run_extract from opencontractserver.tests.fixtures import SAMPLE_PDF_FILE_TWO_PATH @@ -87,7 +87,7 @@ def test_run_extract_task( self.extract.refresh_from_db() self.assertIsNotNone(self.extract.started) - row = Row.objects.filter(extract=self.extract, column=self.column).first() + row = DataCell.objects.filter(extract=self.extract, column=self.column).first() self.assertIsNotNone(row) self.assertEqual(row.data, {"data": "Mocked extracted data"}) self.assertEqual(row.data_definition, "str") From 512635f40887a8c9b9afd72013285903bcdf837a Mon Sep 17 00:00:00 2001 From: JSv4 Date: Mon, 27 May 2024 14:23:25 -0700 Subject: [PATCH 10/68] Changing Row to Datacell. --- frontend/src/extracts/ExtractDataGrid.tsx | 7 ++++--- frontend/src/graphql/types.ts | 6 ++++-- frontend/src/tests/utils/factories.ts | 11 +++++++---- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/frontend/src/extracts/ExtractDataGrid.tsx b/frontend/src/extracts/ExtractDataGrid.tsx index 2f2ed380..ab903a2f 100644 --- a/frontend/src/extracts/ExtractDataGrid.tsx +++ b/frontend/src/extracts/ExtractDataGrid.tsx @@ -4,7 +4,7 @@ import { useMutation, useQuery } from "@apollo/client"; import { toast } from "react-toastify"; import { ColumnDetails } from "./ColumnDetails"; -import { ColumnType, ExtractType, RowType } from "../graphql/types"; +import { ColumnType, ExtractType, DatacellType } from "../graphql/types"; import { REQUEST_GET_EXTRACT } from "../graphql/queries"; import { REQUEST_START_EXTRACT } from "../graphql/mutations"; @@ -39,7 +39,8 @@ export const ExtractDataGrid: React.FC = ({ const { extract } = data; const columns = extract.fieldset.columns; - const rows = extract.rows; + const datacells = extract.datacells; + const documents = extract.documents ? extract.documents : []; console.log("Extract:", extract); @@ -56,7 +57,7 @@ export const ExtractDataGrid: React.FC = ({ - {rows.map((row: RowType) => ( + {datacells.map((row: DatacellType) => ( {columns.map((column: ColumnType) => ( {row.data[column.id]} diff --git a/frontend/src/graphql/types.ts b/frontend/src/graphql/types.ts index 7a7cc63e..29af2ca9 100644 --- a/frontend/src/graphql/types.ts +++ b/frontend/src/graphql/types.ts @@ -1203,12 +1203,14 @@ export interface ExtractType extends Node { started?: Maybe; finished?: Maybe; stacktrace?: Maybe; - rows: RowType[]; + datacells: DatacellType[]; + documents?: DocumentType[]; } -export interface RowType extends Node { +export interface DatacellType extends Node { extract: ExtractType; column: ColumnType; + document: DocumentType; data: any; dataDefinition: string; started?: Maybe; diff --git a/frontend/src/tests/utils/factories.ts b/frontend/src/tests/utils/factories.ts index 1c5f9ec6..c54992df 100644 --- a/frontend/src/tests/utils/factories.ts +++ b/frontend/src/tests/utils/factories.ts @@ -14,10 +14,11 @@ import { AnalysisTypeConnection, ExtractType, ColumnType, - RowType, + DatacellType, CorpusType, FieldsetType, LanguageModelType, + DocumentType, } from "../../graphql/types"; export function generateMockUser(): UserType { @@ -214,18 +215,20 @@ export function generateMockExtract( started: null, finished: null, stacktrace: null, - rows: [], + datacells: [], }; } export function generateMockRow( extract: ExtractType, - column: ColumnType -): RowType { + column: ColumnType, + document: DocumentType +): DatacellType { return { id: uuidv4(), extract, column, + document, data: { data: "Some Data", }, From a139cd510839c5dd3dd6dcfcda5f459cae36c054 Mon Sep 17 00:00:00 2001 From: JSv4 Date: Mon, 27 May 2024 23:05:14 -0700 Subject: [PATCH 11/68] Refactored tests for nomenclature change from Row instance to Datacell instance. --- config/graphql/mutations.py | 11 ++ config/graphql/queries.py | 5 +- frontend/src/assets/configurations/menus.ts | 6 + .../widgets/selectors/CorpusDropdown.tsx | 94 +++++++++++ frontend/src/extracts/list/ExtractList.tsx | 74 +++++++++ .../src/extracts/list/ExtractListItem.tsx | 71 ++++++++ frontend/src/graphql/cache.ts | 10 ++ frontend/src/graphql/mutations.ts | 18 ++ frontend/src/graphql/queries.ts | 76 ++++++++- frontend/src/views/Extracts.tsx | 154 ++++++++++++++++++ .../extracts/migrations/0001_initial.py | 110 ++++++------- .../migrations/0002_auto_20240527_0445.py | 23 --- .../migrations/0003_auto_20240527_2116.py | 77 --------- opencontractserver/tasks/extract_tasks.py | 5 +- .../tests/test_extract_queries.py | 32 +++- .../tests/test_extract_tasks.py | 4 +- 16 files changed, 595 insertions(+), 175 deletions(-) create mode 100644 frontend/src/components/widgets/selectors/CorpusDropdown.tsx create mode 100644 frontend/src/extracts/list/ExtractList.tsx create mode 100644 frontend/src/extracts/list/ExtractListItem.tsx create mode 100644 frontend/src/views/Extracts.tsx delete mode 100644 opencontractserver/extracts/migrations/0002_auto_20240527_0445.py delete mode 100644 opencontractserver/extracts/migrations/0003_auto_20240527_2116.py diff --git a/config/graphql/mutations.py b/config/graphql/mutations.py index e847cc32..962b36ad 100644 --- a/config/graphql/mutations.py +++ b/config/graphql/mutations.py @@ -1456,6 +1456,16 @@ def mutate(root, info, corpus_id, name, fieldset_id): return StartExtract(ok=True, message="SUCCESS!", obj=extract) +class DeleteExtract(DRFDeletion): + class IOSettings: + model = Extract + lookup_field = "id" + + class Arguments: + id = graphene.String(required=True) + + + class Mutation(graphene.ObjectType): # TOKEN MUTATIONS (IF WE'RE NOT OUTSOURCING JWT CREATION TO AUTH0) ####### if not settings.USE_AUTH0: @@ -1528,3 +1538,4 @@ class Mutation(graphene.ObjectType): create_fieldset = CreateFieldset.Field() create_column = CreateColumn.Field() start_extract = StartExtract.Field() + delete_extract = DeleteExtract.Field() # TODO - test diff --git a/config/graphql/queries.py b/config/graphql/queries.py index 1d8f78a2..f9003bd3 100644 --- a/config/graphql/queries.py +++ b/config/graphql/queries.py @@ -737,7 +737,10 @@ def resolve_extract(self, info, **kwargs): Q(id=django_pk) & (Q(owner=info.context.user) | Q(is_public=True)) ) - extracts = DjangoFilterConnectionField(ExtractType, filterset_class=ExtractFilter) + extracts = DjangoFilterConnectionField( + ExtractType, + filterset_class=ExtractFilter + ) @login_required def resolve_extracts(self, info, **kwargs): diff --git a/frontend/src/assets/configurations/menus.ts b/frontend/src/assets/configurations/menus.ts index df7bcb83..3dbd8de2 100644 --- a/frontend/src/assets/configurations/menus.ts +++ b/frontend/src/assets/configurations/menus.ts @@ -23,4 +23,10 @@ export const header_menu_items = [ protected: false, id: "annotation_menu_button", }, + { + title: "Extracts", + route: "/extracts", + protected: false, + id: "extract_menu_button", + }, ]; diff --git a/frontend/src/components/widgets/selectors/CorpusDropdown.tsx b/frontend/src/components/widgets/selectors/CorpusDropdown.tsx new file mode 100644 index 00000000..52c5a72f --- /dev/null +++ b/frontend/src/components/widgets/selectors/CorpusDropdown.tsx @@ -0,0 +1,94 @@ +import React, { SyntheticEvent, useEffect, useState } from "react"; +import { useQuery, useReactiveVar } from "@apollo/client"; +import { Dropdown, DropdownProps } from "semantic-ui-react"; +import { + GET_CORPUSES, + GetCorpusesInputs, + GetCorpusesOutputs, +} from "../../../graphql/queries"; +import { selectedCorpus } from "../../../graphql/cache"; +import _ from "lodash"; +import { CorpusType } from "../../../graphql/types"; + +interface CorpusOption { + key: string; + text: string; + value: string; +} + +export const CorpusDropdown: React.FC = () => { + const [searchQuery, setSearchQuery] = useState(); + const selected_corpus = useReactiveVar(selectedCorpus); + + const { loading, error, data, refetch } = useQuery< + GetCorpusesOutputs, + GetCorpusesInputs + >(GET_CORPUSES, { + variables: { + textSearch: searchQuery, + }, + }); + + // If the searchQuery changes... refetch corpuses. + useEffect(() => { + refetch({ textSearch: searchQuery }); + }, [searchQuery]); + + const corpuses = data?.corpuses.edges + ? data.corpuses.edges.map((edge) => edge.node) + : []; + + const handleSearchChange = ( + event: React.SyntheticEvent, + { searchQuery }: { searchQuery: string } + ) => { + setSearchQuery(searchQuery); + }; + + const handleSelectionChange = ( + event: SyntheticEvent, + data: DropdownProps + ) => { + const selected = _.find(corpuses, { id: data.value }); + if (selected) { + selectedCorpus(selected as CorpusType); + } else { + selectedCorpus(null); + } + }; + + const getDropdownOptions = (): CorpusOption[] => { + if (data && data.corpuses.edges) { + return data.corpuses.edges + .filter(({ node }) => node !== undefined) + .map(({ node }) => ({ + key: node ? node.id : "", + text: node?.title ? node.title : "", + value: node ? node.id : "", + })); + } + return []; + }; + + if (loading) { + return
Loading...
; + } + + if (error) { + return
Error: {error.message}
; + } + + return ( + + ); +}; diff --git a/frontend/src/extracts/list/ExtractList.tsx b/frontend/src/extracts/list/ExtractList.tsx new file mode 100644 index 00000000..4fcdb542 --- /dev/null +++ b/frontend/src/extracts/list/ExtractList.tsx @@ -0,0 +1,74 @@ +import { Table, Dimmer, Loader } from "semantic-ui-react"; +import { ExtractItemRow } from "./ExtractListItem"; +import { FetchMoreOnVisible } from "../../components/widgets/infinite_scroll/FetchMoreOnVisible"; +import { ExtractType, PageInfo } from "../../graphql/types"; + +interface ExtractListProps { + items: ExtractType[] | undefined; + pageInfo: PageInfo | undefined; + loading: boolean; + style?: Record; + fetchMore: (args?: any) => void | any; + onDelete: (args?: any) => void | any; +} + +export function ExtractList({ + items, + pageInfo, + loading, + style, + fetchMore, + onDelete, +}: ExtractListProps) { + const handleUpdate = () => { + if (!loading && pageInfo?.hasNextPage) { + fetchMore({ + variables: { + limit: 20, + cursor: pageInfo.endCursor, + }, + }); + } + }; + + let extract_rows = items + ? items.map((item) => ( + onDelete(item.id)} + item={item} + /> + )) + : []; + + return ( +
+
+ + + + + + Description + Requested + Started + Completed + Actions + + + {extract_rows} + +
+
+ ); +} diff --git a/frontend/src/extracts/list/ExtractListItem.tsx b/frontend/src/extracts/list/ExtractListItem.tsx new file mode 100644 index 00000000..e0fd6f9c --- /dev/null +++ b/frontend/src/extracts/list/ExtractListItem.tsx @@ -0,0 +1,71 @@ +import { Table, Icon, Button } from "semantic-ui-react"; +import { ExtractType } from "../../graphql/types"; +import { DateTimeWidget } from "../../components/widgets/data-display/DateTimeWidget"; + +interface ExtractItemRowProps { + style?: Record; + item: ExtractType; + key: string; + onDelete: (args?: any) => void | any; +} + +export function ExtractItemRow({ onDelete, item, key }: ExtractItemRowProps) { + let createdTime = ""; + let createdDate = "N/A"; + if (item.created) { + var dCreate = new Date(item.created); + createdTime = dCreate.toLocaleTimeString(); + createdDate = dCreate.toLocaleDateString(); + } + + let startedTime = ""; + let startedDate = "N/A"; + if (item.started) { + var dStart = new Date(item.started); + startedTime = dStart.toLocaleTimeString(); + startedDate = dStart.toLocaleDateString(); + } + + let finishedTime = ""; + let finishedDate = "N/A"; + if (item.finished) { + var dCompleted = new Date(item.finished); + finishedTime = dCompleted.toLocaleTimeString(); + finishedDate = dCompleted.toLocaleDateString(); + } + + return ( + + {item.name} + + + + + {!item.started ? ( + + ) : ( + + )} + + + {!item.finished || !item.started ? ( + + ) : ( + + )} + + +
+
+
+
+ ); +} diff --git a/frontend/src/graphql/cache.ts b/frontend/src/graphql/cache.ts index c92d7e4d..4f094a75 100644 --- a/frontend/src/graphql/cache.ts +++ b/frontend/src/graphql/cache.ts @@ -14,6 +14,7 @@ import { LabelSetType, LabelDisplayBehavior, AnalysisType, + ExtractType, } from "./types"; export const mergeArrayByIdFieldPolicy: FieldPolicy = { @@ -180,6 +181,8 @@ export const showAnnotationLabels = makeVar( LabelDisplayBehavior.ON_HOVER ); export const pagesVisible = makeVar>({}); +export const showEditExtractModal = makeVar(false); +export const showDeleteExtractModal = makeVar(false); /** * Document-related global variables. @@ -190,11 +193,18 @@ export const selectedDocumentIds = makeVar([]); export const viewingDocument = makeVar(null); export const editingDocument = makeVar(null); +/** + * Extract-related global variables + */ +export const selectedExtractId = makeVar(null); +export const openedExtract = makeVar(null); + /** * Corpus-related global variables */ export const corpusSearchTerm = makeVar(""); export const filterToCorpus = makeVar(null); +export const selectedCorpus = makeVar(null); export const openedCorpus = makeVar(null); export const viewingCorpus = makeVar(null); export const deletingCorpus = makeVar(null); diff --git a/frontend/src/graphql/mutations.ts b/frontend/src/graphql/mutations.ts index 8d04de6b..ff48f3e5 100644 --- a/frontend/src/graphql/mutations.ts +++ b/frontend/src/graphql/mutations.ts @@ -824,6 +824,24 @@ export const REQUEST_DELETE_ANNOTATION = gql` } `; +export interface RequestDeleteExtractInputType { + id: string; +} + +export interface RequestDeleteExtractOutputType { + deleteExtract: { + ok: boolean; + }; +} + +export const REQUEST_DELETE_EXTRACT = gql` + mutation ($id: String!) { + deleteExtract(id: $id) { + ok + } + } +`; + export interface NewRelationshipInputType { relationshipLabelId: string; documentId: string; diff --git a/frontend/src/graphql/queries.ts b/frontend/src/graphql/queries.ts index d7726fe3..143b5651 100644 --- a/frontend/src/graphql/queries.ts +++ b/frontend/src/graphql/queries.ts @@ -1070,7 +1070,11 @@ export const GET_FIELDSET = gql` } `; -export interface GetExportsOutput { +export interface RequestGetExtractInput { + id: string; +} + +export interface GetExtractOutput { extract: ExtractType; } @@ -1098,11 +1102,15 @@ export const REQUEST_GET_EXTRACT = gql` created started finished - rows { + datacells { id column { id } + document { + id + name + } data dataDefinition started @@ -1113,3 +1121,67 @@ export const REQUEST_GET_EXTRACT = gql` } } `; + +export interface GetExtractsInput { + name?: string; + created?: string; + started?: string; + finished?: string; +} + +export interface GetExtractsOutput { + extracts: { + pageInfo: PageInfo; + edges: { + node: ExtractType; + }[]; + }; +} + +export const REQUEST_GET_EXTRACTS = gql` + query GetExtracts($id: ID!) { + extracts(id: $id) { + edges { + node { + id + corpus { + id + title + } + name + fieldset { + id + name + columns { + id + query + } + } + owner { + id + username + } + created + started + finished + datacells { + id + column { + id + } + document { + id + name + } + data + dataDefinition + started + completed + failed + stacktrace + } + } + } + } + } +`; diff --git a/frontend/src/views/Extracts.tsx b/frontend/src/views/Extracts.tsx new file mode 100644 index 00000000..4e500200 --- /dev/null +++ b/frontend/src/views/Extracts.tsx @@ -0,0 +1,154 @@ +import { useEffect } from "react"; +import { useMutation, useQuery, useReactiveVar } from "@apollo/client"; +import { useLocation } from "react-router-dom"; +import { toast } from "react-toastify"; +import _ from "lodash"; + +import { + RequestDeleteExtractOutputType, + RequestDeleteExtractInputType, + REQUEST_DELETE_EXTRACT, +} from "../graphql/mutations"; +import { + GET_DOCUMENTS, + GetExtractsOutput, + GetExtractsInput, +} from "../graphql/queries"; +import { + authToken, + openedExtract, + selectedExtractId, + showDeleteExtractModal, + showEditExtractModal, +} from "../graphql/cache"; + +import { ActionDropdownItem, LooseObject } from "../components/types"; +import { CardLayout } from "../components/layout/CardLayout"; +import { ExtractType } from "../graphql/types"; +import { ConfirmModal } from "../components/widgets/modals/ConfirmModal"; +import { ExtractList } from "../extracts/list/ExtractList"; + +export const Documents = () => { + const auth_token = useReactiveVar(authToken); + const opened_extract = useReactiveVar(openedExtract); + const selected_extract_id = useReactiveVar(selectedExtractId); + const show_edit_extract_modal = useReactiveVar(showEditExtractModal); + const show_delete_extract_modal = useReactiveVar(showDeleteExtractModal); + + const location = useLocation(); + + let extract_variables: LooseObject = { + includeMetadata: true, + }; + + const { + refetch: refetchExtracts, + loading: extracts_loading, + error: extracts_error, + data: extracts_data, + fetchMore: fetchMoreExtracts, + } = useQuery(GET_DOCUMENTS, { + variables: extract_variables, + nextFetchPolicy: "network-only", + notifyOnNetworkStatusChange: true, // required to get loading signal on fetchMore + }); + + const extract_nodes = extracts_data?.extracts?.edges + ? extracts_data.extracts.edges + : []; + const extract_items = extract_nodes + .map((edge) => (edge?.node ? edge.node : undefined)) + .filter((item): item is ExtractType => !!item); + + // If we just logged in, refetch extracts in case there are extracts that are not public and are only visible to current user + useEffect(() => { + if (auth_token) { + console.log("DocumentItem - refetchExtracts due to auth_token"); + refetchExtracts(); + } + }, [auth_token]); + + // If we navigated here, refetch documents to ensure we have fresh docs + useEffect(() => { + console.log("DocumentItem - refetchExtracts due to location change"); + refetchExtracts(); + }, [location]); + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Implementing various resolvers / mutations to create action methods + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + const [tryDeleteExtract] = useMutation< + RequestDeleteExtractOutputType, + RequestDeleteExtractInputType + >(REQUEST_DELETE_EXTRACT, { + onCompleted: () => { + selectedExtractId(null); + refetchExtracts(); + }, + }); + + const handleDeleteExtract = ( + id: string, + callback?: (args?: any) => void | any + ) => { + tryDeleteExtract({ variables: { id } }) + .then((data) => { + toast.success("SUCCESS - Deleted Extract"); + if (callback) { + callback(); + } + }) + .catch((err) => { + toast.error("ERROR - Could Not Delete Extract"); + if (callback) { + callback(); + } + }); + }; + + // Build the actions for the search / context bar dropdown menu + let extract_actions: ActionDropdownItem[] = []; + + if (auth_token) { + // document_actions.push({ + // key: "documents_action_dropdown_0", + // title: "Import", + // icon: "cloud upload", + // color: "blue", + // action_function: () => + // showUploadNewDocumentsModal(!show_upload_new_documents_modal), + // }); + } + + return ( + + + handleDeleteExtract(selected_extract_id, () => + showDeleteExtractModal(false) + ) + : () => {} + } + noAction={() => showDeleteExtractModal(false)} + toggleModal={() => showDeleteExtractModal(false)} + visible={show_delete_extract_modal} + /> + + } + SearchBar={Hold For Stuff} + > + + + ); +}; diff --git a/opencontractserver/extracts/migrations/0001_initial.py b/opencontractserver/extracts/migrations/0001_initial.py index 74efcea3..b3533b07 100644 --- a/opencontractserver/extracts/migrations/0001_initial.py +++ b/opencontractserver/extracts/migrations/0001_initial.py @@ -1,8 +1,9 @@ -# Generated by Django 3.2.9 on 2024-05-27 01:01 +# Generated by Django 3.2.9 on 2024-05-27 21:27 from django.conf import settings from django.db import migrations, models import django.db.models.deletion +import opencontractserver.shared.defaults import opencontractserver.shared.fields @@ -11,9 +12,10 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ('documents', '0005_document_embedding'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('corpuses', '0002_initial'), - ('auth', '0012_alter_user_first_name_max_length'), ] operations = [ @@ -25,8 +27,8 @@ class Migration(migrations.Migration): ('is_public', models.BooleanField(default=False)), ('created', models.DateTimeField(auto_now_add=True)), ('modified', models.DateTimeField(auto_now=True)), - ('query', models.TextField()), - ('match_text', models.TextField()), + ('query', models.TextField(blank=True, null=True)), + ('match_text', models.TextField(blank=True, null=True)), ('output_type', models.TextField()), ('limit_to_label', models.CharField(blank=True, max_length=512, null=True)), ('instructions', models.TextField(blank=True, null=True)), @@ -37,25 +39,6 @@ class Migration(migrations.Migration): 'permissions': (('permission_column', 'permission column'), ('create_column', 'create column'), ('read_column', 'read column'), ('update_column', 'update column'), ('remove_column', 'delete column')), }, ), - migrations.CreateModel( - name='Extract', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('backend_lock', models.BooleanField(default=False)), - ('is_public', models.BooleanField(default=False)), - ('modified', models.DateTimeField(auto_now=True)), - ('name', models.CharField(max_length=512)), - ('created', models.DateTimeField(auto_now_add=True)), - ('started', models.DateTimeField(blank=True, null=True)), - ('finished', models.DateTimeField(blank=True, null=True)), - ('stacktrace', models.TextField(blank=True, null=True)), - ('corpus', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extracts', to='corpuses.corpus')), - ('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - options={ - 'permissions': (('permission_extract', 'permission extract'), ('create_extract', 'create extract'), ('read_extract', 'read extract'), ('update_extract', 'update extract'), ('remove_extract', 'delete extract')), - }, - ), migrations.CreateModel( name='Fieldset', fields=[ @@ -91,43 +74,50 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='Row', + name='Extract', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('backend_lock', models.BooleanField(default=False)), + ('is_public', models.BooleanField(default=False)), + ('modified', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=512)), + ('created', models.DateTimeField(auto_now_add=True)), + ('started', models.DateTimeField(blank=True, null=True)), + ('finished', models.DateTimeField(blank=True, null=True)), + ('corpus', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extracts', to='corpuses.corpus')), + ('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('fieldset', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='extracts', to='extracts.fieldset')), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='owned_extracts', to=settings.AUTH_USER_MODEL)), + ('user_lock', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='locked_extract_objects', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'permissions': (('permission_extract', 'permission extract'), ('create_extract', 'create extract'), ('read_extract', 'read extract'), ('update_extract', 'update extract'), ('remove_extract', 'delete extract')), + }, + ), + migrations.CreateModel( + name='Datacell', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('backend_lock', models.BooleanField(default=False)), ('is_public', models.BooleanField(default=False)), ('created', models.DateTimeField(auto_now_add=True)), ('modified', models.DateTimeField(auto_now=True)), - ('data', opencontractserver.shared.fields.NullableJSONField()), + ('data', opencontractserver.shared.fields.NullableJSONField(blank=True, default=opencontractserver.shared.defaults.jsonfield_default_value, null=True)), ('data_definition', models.TextField()), ('started', models.DateTimeField(blank=True, null=True)), ('completed', models.DateTimeField(blank=True, null=True)), ('failed', models.DateTimeField(blank=True, null=True)), ('stacktrace', models.TextField(blank=True, null=True)), - ('column', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rows', to='extracts.column')), + ('column', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extracted_datacels', to='extracts.column')), ('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('extract', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rows', to='extracts.extract')), - ('user_lock', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='locked_row_objects', to=settings.AUTH_USER_MODEL)), + ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extracted_datacels', to='documents.document')), + ('extract', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extracted_datacels', to='extracts.extract')), + ('user_lock', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='locked_datacell_objects', to=settings.AUTH_USER_MODEL)), ], options={ - 'permissions': (('permission_row', 'permission row'), ('create_row', 'create row'), ('read_row', 'read row'), ('update_row', 'update row'), ('remove_row', 'delete row')), + 'permissions': (('permission_datacell', 'permission datacell'), ('create_datacell', 'create datacell'), ('read_datacell', 'read datacell'), ('update_datacell', 'update datacell'), ('remove_datacell', 'delete datacell')), }, ), - migrations.AddField( - model_name='extract', - name='fieldset', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='extracts', to='extracts.fieldset'), - ), - migrations.AddField( - model_name='extract', - name='owner', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='owned_extracts', to=settings.AUTH_USER_MODEL), - ), - migrations.AddField( - model_name='extract', - name='user_lock', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='locked_extract_objects', to=settings.AUTH_USER_MODEL), - ), migrations.AddField( model_name='column', name='fieldset', @@ -144,10 +134,10 @@ class Migration(migrations.Migration): field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='locked_column_objects', to=settings.AUTH_USER_MODEL), ), migrations.CreateModel( - name='RowUserObjectPermission', + name='LanguageModelUserObjectPermission', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.row')), + ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.languagemodel')), ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], @@ -157,10 +147,10 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='RowGroupObjectPermission', + name='LanguageModelGroupObjectPermission', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.row')), + ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.languagemodel')), ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.group')), ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')), ], @@ -170,10 +160,10 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='LanguageModelUserObjectPermission', + name='FieldsetUserObjectPermission', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.languagemodel')), + ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.fieldset')), ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], @@ -183,10 +173,10 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='LanguageModelGroupObjectPermission', + name='FieldsetGroupObjectPermission', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.languagemodel')), + ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.fieldset')), ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.group')), ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')), ], @@ -196,10 +186,10 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='FieldsetUserObjectPermission', + name='ExtractUserObjectPermission', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.fieldset')), + ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.extract')), ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], @@ -209,10 +199,10 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='FieldsetGroupObjectPermission', + name='ExtractGroupObjectPermission', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.fieldset')), + ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.extract')), ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.group')), ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')), ], @@ -222,10 +212,10 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='ExtractUserObjectPermission', + name='DatacellUserObjectPermission', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.extract')), + ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.datacell')), ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], @@ -235,10 +225,10 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name='ExtractGroupObjectPermission', + name='DatacellGroupObjectPermission', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.extract')), + ('content_object', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.datacell')), ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.group')), ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.permission')), ], diff --git a/opencontractserver/extracts/migrations/0002_auto_20240527_0445.py b/opencontractserver/extracts/migrations/0002_auto_20240527_0445.py deleted file mode 100644 index 965b479a..00000000 --- a/opencontractserver/extracts/migrations/0002_auto_20240527_0445.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.9 on 2024-05-27 04:45 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('extracts', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='column', - name='match_text', - field=models.TextField(blank=True, null=True), - ), - migrations.AlterField( - model_name='column', - name='query', - field=models.TextField(blank=True, null=True), - ), - ] diff --git a/opencontractserver/extracts/migrations/0003_auto_20240527_2116.py b/opencontractserver/extracts/migrations/0003_auto_20240527_2116.py deleted file mode 100644 index c95c702d..00000000 --- a/opencontractserver/extracts/migrations/0003_auto_20240527_2116.py +++ /dev/null @@ -1,77 +0,0 @@ -# Generated by Django 3.2.9 on 2024-05-27 21:16 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import opencontractserver.shared.defaults -import opencontractserver.shared.fields - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('auth', '0012_alter_user_first_name_max_length'), - ('documents', '0005_document_embedding'), - ('extracts', '0002_auto_20240527_0445'), - ] - - operations = [ - migrations.CreateModel( - name='Datacell', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('backend_lock', models.BooleanField(default=False)), - ('is_public', models.BooleanField(default=False)), - ('created', models.DateTimeField(auto_now_add=True)), - ('modified', models.DateTimeField(auto_now=True)), - ('data', opencontractserver.shared.fields.NullableJSONField(blank=True, default=opencontractserver.shared.defaults.jsonfield_default_value, null=True)), - ('data_definition', models.TextField()), - ('started', models.DateTimeField(blank=True, null=True)), - ('completed', models.DateTimeField(blank=True, null=True)), - ('failed', models.DateTimeField(blank=True, null=True)), - ('stacktrace', models.TextField(blank=True, null=True)), - ('column', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extracted_datacels', to='extracts.column')), - ('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extracted_datacels', to='documents.document')), - ], - options={ - 'permissions': (('permission_datacell', 'permission datacell'), ('create_datacell', 'create datacell'), ('read_datacell', 'read datacell'), ('update_datacell', 'update datacell'), ('remove_datacell', 'delete datacell')), - }, - ), - migrations.RemoveField( - model_name='extract', - name='stacktrace', - ), - migrations.RenameModel( - old_name='RowGroupObjectPermission', - new_name='DatacellGroupObjectPermission', - ), - migrations.RenameModel( - old_name='RowUserObjectPermission', - new_name='DatacellUserObjectPermission', - ), - migrations.DeleteModel( - name='Row', - ), - migrations.AddField( - model_name='datacell', - name='extract', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='extracted_datacels', to='extracts.extract'), - ), - migrations.AddField( - model_name='datacell', - name='user_lock', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='locked_datacell_objects', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='datacellgroupobjectpermission', - name='content_object', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.datacell'), - ), - migrations.AlterField( - model_name='datacelluserobjectpermission', - name='content_object', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='extracts.datacell'), - ), - ] diff --git a/opencontractserver/tasks/extract_tasks.py b/opencontractserver/tasks/extract_tasks.py index 2409b98b..14016215 100644 --- a/opencontractserver/tasks/extract_tasks.py +++ b/opencontractserver/tasks/extract_tasks.py @@ -6,7 +6,7 @@ from pgvector.django import L2Distance from opencontractserver.annotations.models import Annotation -from opencontractserver.extracts.models import Extract, DataCell +from opencontractserver.extracts.models import Extract, Datacell from opencontractserver.types.enums import PermissionTypes from opencontractserver.utils.embeddings import calculate_embedding_for_text from opencontractserver.utils.permissioning import set_permissions_for_obj_to_user @@ -43,11 +43,12 @@ def run_extract(extract_id, user_id): for column in fieldset.columns.all(): with transaction.atomic(): - row = DataCell.objects.create( + row = Datacell.objects.create( extract=extract, column=column, data_definition=column.output_type, creator_id=user_id, + document_id=document_id ) set_permissions_for_obj_to_user(user_id, row, [PermissionTypes.CRUD]) diff --git a/opencontractserver/tests/test_extract_queries.py b/opencontractserver/tests/test_extract_queries.py index 8ee66e46..08281d5c 100644 --- a/opencontractserver/tests/test_extract_queries.py +++ b/opencontractserver/tests/test_extract_queries.py @@ -1,17 +1,20 @@ from django.contrib.auth import get_user_model +from django.core.files.base import ContentFile from django.test import TestCase from graphene.test import Client from graphql_relay import to_global_id from config.graphql.schema import schema from opencontractserver.corpuses.models import Corpus +from opencontractserver.documents.models import Document from opencontractserver.extracts.models import ( Column, Extract, Fieldset, LanguageModel, - DataCell, + Datacell, ) +from opencontractserver.tests.fixtures import SAMPLE_PDF_FILE_TWO_PATH User = get_user_model() @@ -53,12 +56,25 @@ def setUp(self): owner=self.user, creator=self.user, ) - self.row = DataCell.objects.create( + pdf_file = ContentFile( + SAMPLE_PDF_FILE_TWO_PATH.open("rb").read(), name="test.pdf" + ) + + self.doc = Document.objects.create( + creator=self.user, + title="Test Doc", + description="USC Title 1 - Chapter 1", + custom_meta={}, + pdf_file=pdf_file, + backend_lock=True, + ) + self.row = Datacell.objects.create( extract=self.extract, column=self.column, data={"data": "TestData"}, data_definition="str", creator=self.user, + document=self.doc, ) def test_language_model_query(self): @@ -146,23 +162,23 @@ def test_extract_query(self): ) self.assertEqual(result["data"]["extract"]["name"], "TestExtract") - def test_row_query(self): + def test_datacell_query(self): query = """ query { - row(id: "%s") { + datacell(id: "%s") { id data dataDefinition } } """ % to_global_id( - "RowType", self.row.id + "DatacellType", self.row.id ) result = self.client.execute(query) self.assertIsNone(result.get("errors")) self.assertEqual( - result["data"]["row"]["id"], to_global_id("RowType", self.row.id) + result["data"]["datacell"]["id"], to_global_id("DatacellType", self.row.id) ) - self.assertEqual(result["data"]["row"]["data"], {"data": "TestData"}) - self.assertEqual(result["data"]["row"]["dataDefinition"], "str") + self.assertEqual(result["data"]["datacell"]["data"], {"data": "TestData"}) + self.assertEqual(result["data"]["datacell"]["dataDefinition"], "str") diff --git a/opencontractserver/tests/test_extract_tasks.py b/opencontractserver/tests/test_extract_tasks.py index 65c87a6d..6db90474 100644 --- a/opencontractserver/tests/test_extract_tasks.py +++ b/opencontractserver/tests/test_extract_tasks.py @@ -12,7 +12,7 @@ Extract, Fieldset, LanguageModel, - DataCell, + Datacell, ) from opencontractserver.tasks.extract_tasks import run_extract from opencontractserver.tests.fixtures import SAMPLE_PDF_FILE_TWO_PATH @@ -87,7 +87,7 @@ def test_run_extract_task( self.extract.refresh_from_db() self.assertIsNotNone(self.extract.started) - row = DataCell.objects.filter(extract=self.extract, column=self.column).first() + row = Datacell.objects.filter(extract=self.extract, column=self.column).first() self.assertIsNotNone(row) self.assertEqual(row.data, {"data": "Mocked extracted data"}) self.assertEqual(row.data_definition, "str") From d8611fc93f93ccf839b88601c0e72a3e32e8ff6a Mon Sep 17 00:00:00 2001 From: JSv4 Date: Mon, 27 May 2024 23:16:55 -0700 Subject: [PATCH 12/68] Started adding frontend tests. Not working yet. --- .../widgets/selectors/CorpusDropdown.test.tsx | 121 ++++++++++++++++++ .../selectors/FieldsetDropdown.test.tsx | 90 +++++++++++++ .../widgets/selectors/FieldsetDropdown.tsx | 88 +++++++++++++ frontend/src/graphql/cache.ts | 2 + 4 files changed, 301 insertions(+) create mode 100644 frontend/src/components/widgets/selectors/CorpusDropdown.test.tsx create mode 100644 frontend/src/components/widgets/selectors/FieldsetDropdown.test.tsx create mode 100644 frontend/src/components/widgets/selectors/FieldsetDropdown.tsx diff --git a/frontend/src/components/widgets/selectors/CorpusDropdown.test.tsx b/frontend/src/components/widgets/selectors/CorpusDropdown.test.tsx new file mode 100644 index 00000000..add19967 --- /dev/null +++ b/frontend/src/components/widgets/selectors/CorpusDropdown.test.tsx @@ -0,0 +1,121 @@ +import React from "react"; +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import { MockedProvider } from "@apollo/client/testing"; +import { CorpusDropdown } from "./CorpusDropdown"; +import { GET_CORPUSES } from "../../../graphql/queries"; +import { selectedCorpus } from "../../../graphql/cache"; + +const mocks = [ + { + request: { + query: GET_CORPUSES, + variables: { + textSearch: "", + }, + }, + result: { + data: { + corpuses: { + edges: [ + { + node: { + id: "1", + title: "Corpus 1", + description: "Description 1", + }, + }, + { + node: { + id: "2", + title: "Corpus 2", + description: "Description 2", + }, + }, + ], + }, + }, + }, + }, +]; + +describe("CorpusDropdown", () => { + it("renders corpus options and allows selection", async () => { + render( + + + + ); + + // Wait for the loading state to finish + await waitFor(() => { + expect(screen.queryByText("Loading...")).not.toBeInTheDocument(); + }); + + // Open the dropdown + fireEvent.click(screen.getByText("Select Corpus")); + + // Verify the corpus options are rendered + expect(screen.getByText("Corpus 1")).toBeInTheDocument(); + expect(screen.getByText("Corpus 2")).toBeInTheDocument(); + + // Select a corpus option + fireEvent.click(screen.getByText("Corpus 1")); + + // Verify the selected corpus is updated in the cache + expect(selectedCorpus()).toEqual({ + id: "1", + title: "Corpus 1", + description: "Description 1", + }); + }); + + it("fetches corpuses based on search query", async () => { + const searchMocks = [ + { + request: { + query: GET_CORPUSES, + variables: { + textSearch: "Corpus 1", + }, + }, + result: { + data: { + corpuses: { + edges: [ + { + node: { + id: "1", + title: "Corpus 1", + description: "Description 1", + }, + }, + ], + }, + }, + }, + }, + ]; + + render( + + + + ); + + // Wait for the loading state to finish + await waitFor(() => { + expect(screen.queryByText("Loading...")).not.toBeInTheDocument(); + }); + + // Enter a search query + fireEvent.change(screen.getByPlaceholderText("Select Corpus"), { + target: { value: "Corpus 1" }, + }); + + // Wait for the search results to update + await waitFor(() => { + expect(screen.getByText("Corpus 1")).toBeInTheDocument(); + expect(screen.queryByText("Corpus 2")).not.toBeInTheDocument(); + }); + }); +}); diff --git a/frontend/src/components/widgets/selectors/FieldsetDropdown.test.tsx b/frontend/src/components/widgets/selectors/FieldsetDropdown.test.tsx new file mode 100644 index 00000000..bf2a81f2 --- /dev/null +++ b/frontend/src/components/widgets/selectors/FieldsetDropdown.test.tsx @@ -0,0 +1,90 @@ +import React from "react"; +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import { MockedProvider } from "@apollo/client/testing"; +import { FieldsetDropdown } from "./FieldsetDropdown"; +import { REQUEST_GET_FIELDSETS } from "../../../graphql/queries"; +import { selectedFieldset } from "../../../graphql/cache"; + +const mocks = [ + { + request: { + query: REQUEST_GET_FIELDSETS, + }, + result: { + data: { + fieldsets: { + edges: [ + { + node: { + id: "1", + name: "Fieldset 1", + description: "Description 1", + }, + }, + { + node: { + id: "2", + name: "Fieldset 2", + description: "Description 2", + }, + }, + ], + }, + }, + }, + }, +]; + +describe("FieldsetDropdown", () => { + it("renders fieldset options and allows selection", async () => { + render( + + + + ); + + // Wait for the loading state to finish + await waitFor(() => { + expect(screen.queryByText("Loading...")).not.toBeInTheDocument(); + }); + + // Open the dropdown + fireEvent.click(screen.getByText("Select Fieldset")); + + // Verify the fieldset options are rendered + expect(screen.getByText("Fieldset 1")).toBeInTheDocument(); + expect(screen.getByText("Fieldset 2")).toBeInTheDocument(); + + // Select a fieldset option + fireEvent.click(screen.getByText("Fieldset 1")); + + // Verify the selected fieldset is updated in the cache + expect(selectedFieldset()).toEqual({ + id: "1", + name: "Fieldset 1", + description: "Description 1", + }); + }); + + it("displays an error message if the query fails", async () => { + const errorMocks = [ + { + request: { + query: REQUEST_GET_FIELDSETS, + }, + error: new Error("An error occurred"), + }, + ]; + + render( + + + + ); + + // Wait for the error state to appear + await waitFor(() => { + expect(screen.getByText("Error: An error occurred")).toBeInTheDocument(); + }); + }); +}); diff --git a/frontend/src/components/widgets/selectors/FieldsetDropdown.tsx b/frontend/src/components/widgets/selectors/FieldsetDropdown.tsx new file mode 100644 index 00000000..fd3af53c --- /dev/null +++ b/frontend/src/components/widgets/selectors/FieldsetDropdown.tsx @@ -0,0 +1,88 @@ +import React, { SyntheticEvent, useEffect, useState } from "react"; +import { useQuery, useReactiveVar } from "@apollo/client"; +import { Dropdown, DropdownProps } from "semantic-ui-react"; +import { + REQUEST_GET_FIELDSETS, + GetFieldsetsOutputs, +} from "../../../graphql/queries"; +import { selectedFieldset } from "../../../graphql/cache"; +import _ from "lodash"; +import { FieldsetType } from "../../../graphql/types"; + +interface FieldsetOption { + key: string; + text: string; + value: string; +} + +export const FieldsetDropdown: React.FC = () => { + const [searchQuery, setSearchQuery] = useState(); + const selected_fieldset = useReactiveVar(selectedFieldset); + + const { loading, error, data, refetch } = useQuery( + REQUEST_GET_FIELDSETS + ); + + // If the searchQuery changes... refetch fieldsets. + useEffect(() => { + refetch(); + }, [searchQuery]); + + const fieldsets = data?.fieldsets.edges + ? data.fieldsets.edges.map((edge) => edge.node) + : []; + + const handleSearchChange = ( + event: React.SyntheticEvent, + { searchQuery }: { searchQuery: string } + ) => { + setSearchQuery(searchQuery); + }; + + const handleSelectionChange = ( + event: SyntheticEvent, + data: DropdownProps + ) => { + const selected = _.find(fieldsets, { id: data.value }); + if (selected) { + selectedFieldset(selected as FieldsetType); + } else { + selectedFieldset(null); + } + }; + + const getDropdownOptions = (): FieldsetOption[] => { + if (data && data.fieldsets.edges) { + return data.fieldsets.edges + .filter(({ node }) => node !== undefined) + .map(({ node }) => ({ + key: node ? node.id : "", + text: node?.name ? node.name : "", + value: node ? node.id : "", + })); + } + return []; + }; + + if (loading) { + return
Loading...
; + } + + if (error) { + return
Error: {error.message}
; + } + + return ( + + ); +}; diff --git a/frontend/src/graphql/cache.ts b/frontend/src/graphql/cache.ts index 4f094a75..98f334aa 100644 --- a/frontend/src/graphql/cache.ts +++ b/frontend/src/graphql/cache.ts @@ -15,6 +15,7 @@ import { LabelDisplayBehavior, AnalysisType, ExtractType, + FieldsetType, } from "./types"; export const mergeArrayByIdFieldPolicy: FieldPolicy = { @@ -250,6 +251,7 @@ export const analysisSearchTerm = makeVar(""); * Export-related global variables */ export const exportSearchTerm = makeVar(""); +export const selectedFieldset = makeVar(null); /** * Auth-related global variables From 531acdede65e9c49ea332f2a831f71e1c31085bb Mon Sep 17 00:00:00 2001 From: JSv4 Date: Tue, 28 May 2024 00:38:08 -0700 Subject: [PATCH 13/68] Cleaned up some of the typing. Additional fixes needed to make new types match older convention. Lot of requisite components for the extract flow are built out. --- frontend/src/App.tsx | 2 + .../components/layout/CreateAndSearchBar.tsx | 79 +++++++++++-------- .../widgets/modals/CreateExtractModal.tsx | 44 +++++++++++ frontend/src/extracts/ExtractDataGrid.tsx | 22 ++++-- frontend/src/graphql/cache.ts | 1 + frontend/src/graphql/queries.ts | 44 ++++++----- frontend/src/graphql/types.ts | 15 +++- frontend/src/tests/utils/factories.ts | 1 - frontend/src/views/Corpuses.tsx | 2 +- frontend/src/views/Extracts.tsx | 38 +++++---- 10 files changed, 167 insertions(+), 81 deletions(-) create mode 100644 frontend/src/components/widgets/modals/CreateExtractModal.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index eca0b642..dfec9dcb 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -41,6 +41,7 @@ import useWindowDimensions from "./components/hooks/WindowDimensionHook"; import { MobileNavMenu } from "./components/layout/MobileNavMenu"; import { LabelDisplayBehavior } from "./graphql/types"; import { CookieConsentDialog } from "./components/cookies/CookieConsent"; +import { Extracts } from "./views/Extracts"; export const App = () => { const { REACT_APP_USE_AUTH0 } = process.env; @@ -150,6 +151,7 @@ export const App = () => { } /> } /> } /> + } />