From 7c853a5adf44a459e7cc65451c10f250070a20a4 Mon Sep 17 00:00:00 2001 From: Simon Wacker Date: Wed, 20 May 2026 16:56:01 +0200 Subject: [PATCH 1/8] Upgrade to HotChocolate 16 --- backend/create-certificates.csx | 44 ---- backend/dotnet-tools.json | 4 +- .../src/Configuration/AuthConfiguration.cs | 19 +- .../src/Configuration/GraphQlConfiguration.cs | 196 +++++++++++------- .../Controllers/AuthenticationController.cs | 16 +- backend/src/Data/ApplicationDbContext.cs | 128 +++++++++++- backend/src/Data/Association.cs | 8 + backend/src/Data/AuditableAssociation.cs | 10 + backend/src/Data/AuditableEntity.cs | 20 ++ backend/src/Data/CalorimetricData.cs | 5 +- backend/src/Data/DataX.cs | 52 +++-- backend/src/Data/Entity.cs | 15 +- backend/src/Data/GeometricData.cs | 5 +- backend/src/Data/GetHttpsResource.cs | 2 +- backend/src/Data/HygrothermalData.cs | 5 +- backend/src/Data/IAssociation.cs | 7 + backend/src/Data/IAuditable.cs | 13 ++ backend/src/Data/IData.cs | 6 +- backend/src/Data/INamed.cs | 6 + backend/src/Data/InstitutionAccessRights.cs | 6 +- backend/src/Data/LifeCycleData.cs | 5 +- backend/src/Data/OpticalData.cs | 5 +- backend/src/Data/PhotovoltaicData.cs | 5 +- backend/src/Data/User.cs | 2 +- backend/src/Database.csproj | 66 +++--- .../src/Extensions/EnumerableExtensions.cs | 20 -- backend/src/Extensions/LinqExtensions.cs | 190 +++++++++++++++++ backend/src/Extensions/NodaTimeExtensions.cs | 15 +- backend/src/Extensions/StringExtensions.cs | 15 ++ .../AuditableAssociationFilterType.cs | 19 ++ .../AuditableAssociationSortType.cs | 19 ++ backend/src/GraphQl/AuthorizedConnection.cs | 40 ++++ .../GraphQl/AuthorizedPaginatedConnection.cs | 47 +++++ .../CalorimetricDataByIdDataLoader.cs | 20 -- .../CalorimetricDataLoaders.cs | 31 +++ .../CalorimetricDataQueries.cs | 13 +- .../CalorimetricDataX/CalorimetricDataType.cs | 2 +- .../CreateCalorimetricDataMutation.cs | 7 +- backend/src/GraphQl/Connection.cs | 31 +-- backend/src/GraphQl/DataLoaders.cs | 126 +++++++++++ .../GraphQl/DataX/CreateDataMutationBase.cs | 5 +- backend/src/GraphQl/DataX/DataConnection.cs | 14 +- .../src/GraphQl/DataX/DataConnectionBase.cs | 5 +- backend/src/GraphQl/DataX/DataDataLoaders.cs | 69 ++++++ .../src/GraphQl/DataX/DataFilterTypeBase.cs | 5 +- backend/src/GraphQl/DataX/DataQueries.cs | 1 + backend/src/GraphQl/DataX/DataQueriesBase.cs | 48 ++--- backend/src/GraphQl/DataX/DataResolvers.cs | 20 +- backend/src/GraphQl/DataX/DataSortTypeBase.cs | 5 +- backend/src/GraphQl/DataX/DataType.cs | 19 +- backend/src/GraphQl/DataX/DataTypeBase.cs | 21 +- .../src/GraphQl/DataX/GetHttpsResourceTree.cs | 7 +- ...ceTreeNonRootVerticesByDataIdDataLoader.cs | 34 --- ...HttpsResourceTreeRootByDataIdDataLoader.cs | 34 --- .../GetHttpsResourcesByDataIdDataLoader.cs | 34 --- .../src/GraphQl/DataX/UpdateDataMutation.cs | 4 +- .../AssociationsByAssociateIdDataLoader.cs | 38 ---- ...erType.cs => AuditableEntityFilterType.cs} | 13 +- ...SortType.cs => AuditableEntitySortType.cs} | 10 +- .../GraphQl/Entities/EntityByIdDataLoader.cs | 39 ---- backend/src/GraphQl/Entities/EntityType.cs | 10 +- .../ErrorLoggingDiagnosticEventListener.cs | 187 +++++++++-------- .../src/GraphQl/Extensions/PageExtensions.cs | 66 ++++++ .../Extensions/ResolverContextExtensions.cs | 14 +- .../Extensions/SortingContextExtensions.cs | 30 --- backend/src/GraphQl/Filters/INotField.cs | 21 +- backend/src/GraphQl/Filters/NotField.cs | 87 ++++---- .../GraphQl/Filters/ScalarFilterInputTypes.cs | 64 ++++-- .../CreateGeometricDataMutation.cs | 9 +- .../GeometricDataByIdDataLoader.cs | 20 -- .../GeometricDataX/GeometricDataLoaders.cs | 31 +++ .../GeometricDataX/GeometricDataQueries.cs | 11 +- .../GeometricDataX/GeometricDataType.cs | 2 +- .../GetHttpsResourceByIdDataLoader.cs | 20 -- ...eChildrenByGetHttpsResourceIdDataLoader.cs | 26 --- .../GetHttpsResourceDataLoaders.cs | 49 +++++ .../GetHttpsResourceFilterType.cs | 2 +- .../GetHttpsResourceQueries.cs | 38 ++-- .../GetHttpsResourceResolvers.cs | 16 +- .../GetHttpsResourceSortType.cs | 2 +- .../GetHttpsResources/GetHttpsResourceType.cs | 3 +- ...mputeGetHttpsResourceHashValuesMutation.cs | 5 +- backend/src/GraphQl/GraphQlConstants.cs | 2 + backend/src/GraphQl/GraphQlThrowHelper.cs | 52 +++++ backend/src/GraphQl/GraphQlTypeResources.cs | 8 + .../CreateHygrothermalDataMutation.cs | 7 +- .../HygrothermalDataByIdDataLoader.cs | 20 -- .../HygrothermalDataLoaders.cs | 31 +++ .../HygrothermalDataQueries.cs | 11 +- .../HygrothermalDataX/HygrothermalDataType.cs | 2 +- .../CreateLifeCycleDataMutation.cs | 7 +- .../LifeCycleDataByIdDataLoader.cs | 20 -- .../LifeCycleDataX/LifeCycleDataLoaders.cs | 31 +++ .../LifeCycleDataX/LifeCycleDataQueries.cs | 11 +- .../LifeCycleDataX/LifeCycleDataType.cs | 2 +- .../OpticalDataX/CreateOpticalDataMutation.cs | 7 +- .../OpticalDataX/OpticalDataByIdDataLoader.cs | 20 -- .../OpticalDataX/OpticalDataLoaders.cs | 31 +++ .../OpticalDataX/OpticalDataQueries.cs | 11 +- .../GraphQl/OpticalDataX/OpticalDataType.cs | 2 +- backend/src/GraphQl/PaginatedConnection.cs | 75 +++++++ backend/src/GraphQl/PaginatedEdge.cs | 31 +++ .../CreatePhotovoltaicDataMutation.cs | 9 +- .../PhotovoltaicDataByIdDataLoader.cs | 20 -- .../PhotovoltaicDataLoaders.cs | 31 +++ .../PhotovoltaicDataQueries.cs | 13 +- .../PhotovoltaicDataX/PhotovoltaicDataType.cs | 2 +- .../CreateResponseApprovalsMutation.cs | 3 +- .../ResponseApprovalFilterType.cs | 3 +- .../UpdateResponseApprovalsMutation.cs | 3 +- .../src/GraphQl/{ => Scalars}/LocaleType.cs | 6 +- backend/src/GraphQl/Scalars/MyUriType.cs | 110 ++++++++++ .../src/GraphQl/Scalars/NonNegativeIntType.cs | 72 +++++++ backend/src/GraphQl/Sorting.cs | 18 ++ .../src/GraphQl/Users/UserByIdDataLoader.cs | 20 -- backend/src/GraphQl/Users/UserDataLoaders.cs | 31 +++ backend/src/GraphQl/Users/UserType.cs | 3 +- ...ningAndEncryptionCertificateRotationJob.cs | 20 +- .../SpectralToIntegralMethod.cs | 2 +- backend/src/Program.cs | 1 - backend/src/Services/AccessRightsService.cs | 12 +- backend/src/Services/CacheService.cs | 10 +- .../src/Services/ResponseApprovalService.cs | 7 +- backend/src/Services/SigningService.cs | 2 +- backend/src/Startup.cs | 37 +--- backend/src/Utilities/FileHelpers.cs | 8 +- backend/test/AuditableTests.cs | 75 +++++++ backend/test/Database.Tests.csproj | 14 +- 128 files changed, 2191 insertions(+), 1105 deletions(-) delete mode 100644 backend/create-certificates.csx create mode 100644 backend/src/Data/Association.cs create mode 100644 backend/src/Data/AuditableAssociation.cs create mode 100644 backend/src/Data/AuditableEntity.cs create mode 100644 backend/src/Data/IAssociation.cs create mode 100644 backend/src/Data/IAuditable.cs create mode 100644 backend/src/Data/INamed.cs delete mode 100644 backend/src/Extensions/EnumerableExtensions.cs create mode 100644 backend/src/Extensions/LinqExtensions.cs create mode 100644 backend/src/GraphQl/Associations/AuditableAssociationFilterType.cs create mode 100644 backend/src/GraphQl/Associations/AuditableAssociationSortType.cs create mode 100644 backend/src/GraphQl/AuthorizedConnection.cs create mode 100644 backend/src/GraphQl/AuthorizedPaginatedConnection.cs delete mode 100644 backend/src/GraphQl/CalorimetricDataX/CalorimetricDataByIdDataLoader.cs create mode 100644 backend/src/GraphQl/CalorimetricDataX/CalorimetricDataLoaders.cs create mode 100644 backend/src/GraphQl/DataLoaders.cs create mode 100644 backend/src/GraphQl/DataX/DataDataLoaders.cs delete mode 100644 backend/src/GraphQl/DataX/GetHttpsResourceTreeNonRootVerticesByDataIdDataLoader.cs delete mode 100644 backend/src/GraphQl/DataX/GetHttpsResourceTreeRootByDataIdDataLoader.cs delete mode 100644 backend/src/GraphQl/DataX/GetHttpsResourcesByDataIdDataLoader.cs delete mode 100644 backend/src/GraphQl/Entities/AssociationsByAssociateIdDataLoader.cs rename backend/src/GraphQl/Entities/{EntityFilterType.cs => AuditableEntityFilterType.cs} (90%) rename backend/src/GraphQl/Entities/{EntitySortType.cs => AuditableEntitySortType.cs} (58%) delete mode 100644 backend/src/GraphQl/Entities/EntityByIdDataLoader.cs create mode 100644 backend/src/GraphQl/Extensions/PageExtensions.cs delete mode 100644 backend/src/GraphQl/Extensions/SortingContextExtensions.cs delete mode 100644 backend/src/GraphQl/GeometricDataX/GeometricDataByIdDataLoader.cs create mode 100644 backend/src/GraphQl/GeometricDataX/GeometricDataLoaders.cs delete mode 100644 backend/src/GraphQl/GetHttpsResources/GetHttpsResourceByIdDataLoader.cs delete mode 100644 backend/src/GraphQl/GetHttpsResources/GetHttpsResourceChildrenByGetHttpsResourceIdDataLoader.cs create mode 100644 backend/src/GraphQl/GetHttpsResources/GetHttpsResourceDataLoaders.cs create mode 100644 backend/src/GraphQl/GraphQlThrowHelper.cs create mode 100644 backend/src/GraphQl/GraphQlTypeResources.cs delete mode 100644 backend/src/GraphQl/HygrothermalDataX/HygrothermalDataByIdDataLoader.cs create mode 100644 backend/src/GraphQl/HygrothermalDataX/HygrothermalDataLoaders.cs delete mode 100644 backend/src/GraphQl/LifeCycleDataX/LifeCycleDataByIdDataLoader.cs create mode 100644 backend/src/GraphQl/LifeCycleDataX/LifeCycleDataLoaders.cs delete mode 100644 backend/src/GraphQl/OpticalDataX/OpticalDataByIdDataLoader.cs create mode 100644 backend/src/GraphQl/OpticalDataX/OpticalDataLoaders.cs create mode 100644 backend/src/GraphQl/PaginatedConnection.cs create mode 100644 backend/src/GraphQl/PaginatedEdge.cs delete mode 100644 backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataByIdDataLoader.cs create mode 100644 backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataLoaders.cs rename backend/src/GraphQl/{ => Scalars}/LocaleType.cs (94%) create mode 100644 backend/src/GraphQl/Scalars/MyUriType.cs create mode 100644 backend/src/GraphQl/Scalars/NonNegativeIntType.cs create mode 100644 backend/src/GraphQl/Sorting.cs delete mode 100644 backend/src/GraphQl/Users/UserByIdDataLoader.cs create mode 100644 backend/src/GraphQl/Users/UserDataLoaders.cs create mode 100644 backend/test/AuditableTests.cs diff --git a/backend/create-certificates.csx b/backend/create-certificates.csx deleted file mode 100644 index 1f92e4c0..00000000 --- a/backend/create-certificates.csx +++ /dev/null @@ -1,44 +0,0 @@ -using System.Linq; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; - -// TODO Is there a better way to manage these keys? Is there gonna be a problem in two-years time when the keys become invalid? -// Inspired by https://github.com/openiddict/openiddict-core/blob/78901e3e7e3ee47cf7846a71f758dc9ca110b1a2/src/OpenIddict.Server/OpenIddictServerBuilder.cs#L661-L679 -// and https://github.com/openiddict/openiddict-core/blob/78901e3e7e3ee47cf7846a71f758dc9ca110b1a2/src/OpenIddict.Server/OpenIddictServerBuilder.cs#L661-L679 -foreach (var (fileName, name, flags, password) in new[] { - ("jwt-encryption-certificate.pfx", "Encryption", X509KeyUsageFlags.KeyEncipherment, Args[0]), - ("jwt-signing-certificate.pfx", "Signing", X509KeyUsageFlags.DigitalSignature, Args[1]) -}) -{ - var path = Path.Join("src", fileName); - var certificate = - File.Exists(path) - ? X509CertificateLoader.LoadPkcs12FromFile(path, password) - : null; - if (certificate is null || certificate.NotAfter <= DateTime.Now) - { - var subject = new X500DistinguishedName($"CN=Database OpenId Connect Server {name} Certificate"); - // certificates.LastOrDefault(certificate => certificate.NotBefore < DateTime.Now && certificate.NotAfter > DateTime.Now); - // TODO Is RSA sufficiently secure? Or should we use ECDSA? - using (var algorithm = RSA.Create(keySizeInBits: 2048)) - { - var request = new CertificateRequest( - subject, - algorithm, - HashAlgorithmName.SHA256, - RSASignaturePadding.Pkcs1 - ); - request.CertificateExtensions.Add( - new X509KeyUsageExtension(flags, critical: true) - ); - certificate = request.CreateSelfSigned( - notBefore: DateTimeOffset.UtcNow, - notAfter: DateTimeOffset.UtcNow.AddYears(2) - ); - File.WriteAllBytes( - path, - certificate.Export(X509ContentType.Pfx, password) - ); - } - } -} \ No newline at end of file diff --git a/backend/dotnet-tools.json b/backend/dotnet-tools.json index 6761c304..972eb2a4 100644 --- a/backend/dotnet-tools.json +++ b/backend/dotnet-tools.json @@ -24,7 +24,7 @@ "rollForward": false }, "dotnet-ef": { - "version": "10.0.5", + "version": "10.0.8", "commands": [ "dotnet-ef" ], @@ -66,7 +66,7 @@ "rollForward": false }, "jetbrains.resharper.globaltools": { - "version": "2025.3.3", + "version": "2026.1.1", "commands": [ "jb" ], diff --git a/backend/src/Configuration/AuthConfiguration.cs b/backend/src/Configuration/AuthConfiguration.cs index 51f7100c..6a7e65ad 100644 --- a/backend/src/Configuration/AuthConfiguration.cs +++ b/backend/src/Configuration/AuthConfiguration.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Net.Http; -using System.Reflection; using System.Security.Cryptography.X509Certificates; using Database.Authentication; using Database.Authorization; @@ -10,6 +9,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using NodaTime; using OpenIddict.Abstractions; using OpenIddict.Client; using Quartz; @@ -21,7 +21,7 @@ public static class AuthConfiguration { private static readonly TimeSpan s_cookieExpirationTimeSpan = TimeSpan.FromDays(1); - private static void BootstrapCertificates() + private static void BootstrapCertificates(IClock clock) { using var store = new X509Store(OpenIdConnectConstants.CertificateStoreName, OpenIdConnectConstants.CertificateStoreLocation); try @@ -34,11 +34,12 @@ private static void BootstrapCertificates() distinguishedName, validOnly: true ); - if (certificates.Count == 0) + if (certificates.Count is 0) { store.Add( JwtSigningAndEncryptionCertificateRotationJob.CreateSigningCertificate( - distinguishedName + distinguishedName, + clock ) ); } @@ -50,11 +51,12 @@ private static void BootstrapCertificates() distinguishedName, validOnly: true ); - if (certificates.Count == 0) + if (certificates.Count is 0) { store.Add( JwtSigningAndEncryptionCertificateRotationJob.CreateEncryptionCertificate( - distinguishedName + distinguishedName, + clock ) ); } @@ -93,10 +95,11 @@ private static IEnumerable FindCertificates(string distinguish public static void ConfigureServices( IServiceCollection services, IWebHostEnvironment environment, - AppSettings appSettings + AppSettings appSettings, + IClock clock ) { - BootstrapCertificates(); + BootstrapCertificates(clock); services.AddScoped(); services.AddScoped(); ConfigureAuthenticationAndAuthorizationServices(services); diff --git a/backend/src/Configuration/GraphQlConfiguration.cs b/backend/src/Configuration/GraphQlConfiguration.cs index 69fbdf38..f485fdbd 100644 --- a/backend/src/Configuration/GraphQlConfiguration.cs +++ b/backend/src/Configuration/GraphQlConfiguration.cs @@ -4,6 +4,8 @@ using Database.GraphQl; using Database.GraphQl.DataX; using Database.GraphQl.Filters; +using Database.GraphQl.Scalars; +using HotChocolate.AspNetCore; using HotChocolate.Configuration; using HotChocolate.Data; using HotChocolate.Data.Filters; @@ -13,9 +15,10 @@ using HotChocolate.Language; using HotChocolate.Types; using HotChocolate.Types.Descriptors; -using HotChocolate.Types.Descriptors.Definitions; +using HotChocolate.Types.Descriptors.Configurations; using HotChocolate.Types.NodaTime; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -32,8 +35,7 @@ IWebHostEnvironment environment { // Automatic-Persisted-Queries Services services - .AddMemoryCache() - .AddSha256DocumentHashProvider(HashFormat.Hex); // https://chillicream.com/docs/hotchocolate/v15/security/#fips-compliance + .AddMemoryCache(); // GraphQL Server var serverBuilder = services .AddGraphQLServer(); @@ -43,59 +45,92 @@ IWebHostEnvironment environment serverBuilder.TryAddTypeInterceptor(); } serverBuilder - // TODO add warmup task once we upgrade to version 16: https://chillicream.com/docs/hotchocolate/v16/server/warmup - // .AddWarmupTask(async (executor, cancellationToken) => - // { - // await executor.ExecuteAsync("{ __typename }", cancellationToken); - // }) + .AddSha256DocumentHashProvider(HashFormat.Hex) // https://chillicream.com/docs/hotchocolate/v15/security/#fips-compliance + .AddApplicationService() // for `AddHttpRequestInterceptor` + .AddApplicationService>() // for `AddDiagnosticEventListener` .DisableIntrospection(false) // if the introspection result becomes too big we need to disable it in production - .BindRuntimeType() - // Services https://chillicream.com/docs/hotchocolate/v13/integrations/entity-framework#registerdbcontext .RegisterDbContextFactory() + // .AddInstrumentation() .AddMutationConventions(new MutationConventionOptions { ApplyToAllMutations = false }) // Extensions + .AddNodaTime() + // .AddTypeConverter( + // _ => _.ToDateTimeOffset() + // ) + // .AddTypeConverter( + // _ => OffsetDateTime.FromDateTimeOffset(_) + // ) .AddProjections() .AddFiltering() .AddSorting() .AddConvention() .AddQueryContext() .AddAuthorization() - .AddGlobalObjectIdentification() .AddQueryFieldToMutationPayloads() - .ModifyOptions(options => + .AddGlobalObjectIdentification(_ => + { + // _.MaxAllowedNodeBatchSize = 100; + _.EnsureAllNodesCanBeResolved = true; + } + ) + .ModifyOptions(_ => { // https://github.com/ChilliCream/hotchocolate/blob/main/src/HotChocolate/Core/src/Types/Configuration/Contracts/ISchemaOptions.cs - options.StrictValidation = true; - options.UseXmlDocumentation = false; - options.SortFieldsByName = true; - options.RemoveUnreachableTypes = false; - options.RemoveUnusedTypeSystemDirectives = true; - options.DefaultBindingBehavior = BindingBehavior.Implicit; + _.StrictValidation = true; + _.UseXmlDocumentation = false; + _.SortFieldsByName = true; + _.RemoveUnreachableTypes = false; + _.RemoveUnusedTypeSystemDirectives = true; + _.DefaultBindingBehavior = BindingBehavior.Implicit; // options.DefaultFieldBindingFlags = FieldBindingFlags.InstanceAndStatic; - options.EnableDirectiveIntrospection = true; - options.DefaultDirectiveVisibility = DirectiveVisibility.Public; - options.DefaultResolverStrategy = ExecutionStrategy.Parallel; - options.ValidatePipelineOrder = true; - options.StrictRuntimeTypeValidation = true; - options.EnableOneOf = true; - options.EnsureAllNodesCanBeResolved = true; - options.EnableFlagEnums = false; - options.EnableDefer = false; - options.EnableStream = false; - options.EnableSemanticNonNull = false; - options.StripLeadingIFromInterface = false; - options.EnableTag = true; - options.PublishRootFieldPagesToPromiseCache = true; + _.EnableDirectiveIntrospection = true; + _.EnableOptInFeatures = true; + _.DefaultDirectiveVisibility = DirectiveVisibility.Public; + _.DefaultResolverStrategy = ExecutionStrategy.Parallel; + _.ValidatePipelineOrder = true; + _.StrictRuntimeTypeValidation = true; + _.EnableFlagEnums = false; + _.EnableDefer = false; + _.EnableStream = false; + _.StripLeadingIFromInterface = false; + _.EnableTag = true; + _.PublishRootFieldPagesToPromiseCache = true; + // options.OperationDocumentCacheSize = 200; + // options.PreparedOperationCacheSize = 100; } ) - .ModifyRequestOptions(options => + .ModifyServerOptions(_ => + { + _.AllowedGetOperations = AllowedGetOperations.Query; + _.Batching = AllowedBatching.All; + _.EnableGetRequests = false; + _.EnableMultipartRequests = true; + _.EnableSchemaRequests = true; + // Nitro + _.Tool.DisableTelemetry = true; + _.Tool.Enable = true; // environment.IsDevelopment() + _.Tool.GraphQLEndpoint = GraphQlConstants.EndpointPath; + _.Tool.IncludeCookies = false; + _.Tool.Title = "GraphQL"; + _.Tool.UseBrowserUrlAsGraphQLEndpoint = false; + _.Tool.UseGet = false; + } + ) + .ModifyRequestOptions(_ => { // https://github.com/ChilliCream/hotchocolate/blob/main/src/HotChocolate/Core/src/Execution/Options/RequestExecutorOptions.cs - options.ExecutionTimeout = TimeSpan.FromSeconds(120); - options.IncludeExceptionDetails = !environment.IsProduction(); // Default is `Debugger.IsAttached`. - /* options.QueryCacheSize = ...; */ - /* options.UseComplexityMultipliers = ...; */ - options.EnableSchemaFileSupport = true; + _.ExecutionTimeout = TimeSpan.FromSeconds(120); + _.IncludeExceptionDetails = !environment.IsProduction(); // Default is `Debugger.IsAttached`. + _.AllowErrorHandlingModeOverride = true; + // options.QueryCacheSize = ...; + // options.UseComplexityMultipliers = ...; + // options.EnableSchemaFileSupport = true; + } + ) + .ModifyCostOptions(_ => + { + _.MaxFieldCost = 10000; + _.MaxTypeCost = 10000; } ) // Configure @@ -108,11 +143,6 @@ IWebHostEnvironment environment // Persisted queries /* .AddFileSystemOperationDocumentStorage("./persisted_operations") */ /* .UsePersistedOperationPipeline(); */ - // HotChocolate uses the default authentication scheme, - // which we set to `null` in `AuthConfiguration` to force - // users to be explicit about what scheme to use when - // making it easier to grasp the various authentication - // flows. .AddHttpRequestInterceptor(async (httpContext, requestExecutor, requestBuilder, cancellationToken) => { await httpContext.RequestServices @@ -125,23 +155,12 @@ await httpContext.RequestServices ) ) // Scalar Types + // TODO Add `MyUuidType` based on https://github.com/ChilliCream/graphql-platform/blob/main/src/HotChocolate/Core/src/Types/Types/Scalars/UuidType.cs .AddType(new UuidType("Uuid", defaultFormat: 'D')) // https://chillicream.com/docs/hotchocolate/defining-a-schema/scalars#uuid-type - .AddType(new UrlType("Url")) - .AddType(new JsonType("Any", BindingBehavior.Implicit)) // https://chillicream.com/blog/2023/02/08/new-in-hot-chocolate-13#json-scalar + .AddType(new MyUriType()) + .AddType(new AnyType("Any")) .AddType() - .AddType() - .AddType() - // .AddType() - // Register converters between NodaTime's `OffsetDateTime` and .NET's - // `DateTimeOffset` to reuse the existing `DateTimeType` - // https://chillicream.com/docs/hotchocolate/v15/defining-a-schema/scalars#custom-converters - .BindRuntimeType() - .AddTypeConverter( - _ => _.ToDateTimeOffset() - ) - .AddTypeConverter( - _ => OffsetDateTime.FromDateTimeOffset(_) - ) + .BindRuntimeType() // Object Types .AddType() // Query, Mutation, Subscription, Object, and Input Types @@ -152,10 +171,11 @@ await httpContext.RequestServices .AddTypes() // Paging .AddDbContextCursorPagingProvider() + // .AddCursorKeySerializer(new OffsetDateTimeCursorKeySerializer()) .ModifyPagingOptions(_ => { - _.MaxPageSize = 100; - _.DefaultPageSize = 100; + _.MaxPageSize = (int)GraphQlConstants.MaximumPageSize; + _.DefaultPageSize = (int)GraphQlConstants.MaximumPageSize; _.IncludeTotalCount = true; _.IncludeNodesField = false; // TODO I actually want to infer connection names from fields (which is the default in HotChocolate). However, the current `database.graphql` schema that I hand-wrote still infers connection names from types. @@ -166,6 +186,26 @@ await httpContext.RequestServices .UseAutomaticPersistedOperationPipeline() .AddInMemoryOperationDocumentStorage(); // Needed by the automatic persisted operation pipeline } + + // + // private sealed class MyUuidType : UuidType + // { + // private const string SpecifiedByString = "https://tools.ietf.org/html/rfc4122"; + // + // public MyUuidType( + // string name, + // string? description = null, + // char defaultFormat = '\0', + // bool enforceFormat = false, + // BindingBehavior bind = BindingBehavior.Explicit + // ) + // : base(name, description, defaultFormat, enforceFormat, + // bind) + // { + // SpecifiedBy = new Uri(SpecifiedByString, UriKind.Absolute); + // } + // } + } // https://github.com/ChilliCream/graphql-platform/blob/main/src/HotChocolate/Core/src/Types/Configuration/TypeInterceptor.cs @@ -177,14 +217,14 @@ public override void OnBeforeInitialize(ITypeDiscoveryContext discoveryContext) Console.WriteLine($"[INIT] Discovered type '{discoveryContext.Type.GetType().Name}'"); } - public override void OnBeforeCompleteName(ITypeCompletionContext completionContext, DefinitionBase definition) + public override void OnBeforeCompleteName(ITypeCompletionContext completionContext, TypeSystemConfiguration configuration) { - Console.WriteLine($"[NAME] Finalizing name '{definition.Name}' for type '{completionContext.Type.GetType().Name}'"); + Console.WriteLine($"[NAME] Finalizing name '{configuration.Name}' for type '{completionContext.Type.GetType().Name}'"); } - public override void OnAfterCompleteType(ITypeCompletionContext completionContext, DefinitionBase definition) + public override void OnAfterCompleteType(ITypeCompletionContext completionContext, TypeSystemConfiguration configuration) { - Console.WriteLine($"[DONE] Completed type '{completionContext.Type.GetType().Name}' with name '{definition.Name}'"); + Console.WriteLine($"[DONE] Completed type '{completionContext.Type.GetType().Name}' with name '{configuration.Name}'"); } } @@ -222,7 +262,9 @@ protected override void Configure(IFilterConventionDescriptor descriptor) descriptor.Provider( new QueryableFilterProvider(_ => _ .AddDefaultFieldHandlers() - .AddFieldHandler() + .AddFieldHandler(context => + new QueryableComparableInClosedIntervalHandler(context.TypeConverter, context.InputParser) + ) ) ); } @@ -364,16 +406,24 @@ this IFilterConventionDescriptor descriptor .BindRuntimeType() .BindRuntimeType() .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType() .BindRuntimeType() .BindRuntimeType() - // .BindRuntimeType() - // .BindRuntimeType() - // .BindRuntimeType() - // .BindRuntimeType() - .BindRuntimeType() - .BindRuntimeType() - .BindRuntimeType() - .BindRuntimeType(); + .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType(); } } diff --git a/backend/src/Controllers/AuthenticationController.cs b/backend/src/Controllers/AuthenticationController.cs index c661c6ef..bd788125 100644 --- a/backend/src/Controllers/AuthenticationController.cs +++ b/backend/src/Controllers/AuthenticationController.cs @@ -46,7 +46,7 @@ protected override void Dispose(bool disposing) } [HttpGet("~/connect/login")] - public ActionResult LogIn(string? returnUrl) + public ActionResult LogIn(string? returnTo) { // Ask the OpenIddict client middleware to redirect the user agent to the identity provider. return Challenge( @@ -60,7 +60,7 @@ public ActionResult LogIn(string? returnUrl) ) { // Only allow local return URLs to prevent open redirect attacks. - RedirectUri = SanitizeReturnUrl(returnUrl) + RedirectUri = SanitizeReturnUrl(returnTo) }, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme ); @@ -69,7 +69,7 @@ public ActionResult LogIn(string? returnUrl) [HttpPost("~/connect/logout")] [Authorize(AuthenticationSchemes = AuthenticationConstants.CookieAndBearerTokenAuthenticationScheme)] [RequireAntiforgeryToken] - public async Task LogOut(string? returnUrl) + public async Task LogOut(string? returnTo) { // Retrieve the identity stored in the local authentication cookie. If it's not available, // this indicate that the user is already logged out locally (or has not logged in yet). @@ -78,7 +78,7 @@ public async Task LogOut(string? returnUrl) { // Only allow local return URLs to prevent open redirect attacks. // https://learn.microsoft.com/en-us/aspnet/core/security/preventing-open-redirects - return LocalRedirect(SanitizeReturnUrl(returnUrl)); + return LocalRedirect(SanitizeReturnUrl(returnTo)); } // Remove the local authentication cookie before triggering a redirection to the remote server. @@ -98,7 +98,7 @@ public async Task LogOut(string? returnUrl) ) { // Only allow local return URLs to prevent open redirect attacks. - RedirectUri = SanitizeReturnUrl(returnUrl) + RedirectUri = SanitizeReturnUrl(returnTo) }, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme ); @@ -251,11 +251,11 @@ public async Task LogOutCallback(string provider) ); } - private string SanitizeReturnUrl(string? returnUrl) + private string SanitizeReturnUrl(string? returnTo) { return - returnUrl is not null && Url.IsLocalUrl(returnUrl) - ? returnUrl + returnTo is not null && Url.IsLocalUrl(returnTo) + ? returnTo : "/"; } } \ No newline at end of file diff --git a/backend/src/Data/ApplicationDbContext.cs b/backend/src/Data/ApplicationDbContext.cs index 61575e2e..19d9ccd3 100644 --- a/backend/src/Data/ApplicationDbContext.cs +++ b/backend/src/Data/ApplicationDbContext.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using Database.Enumerations; using Database.Extensions; -using Database.GraphQl.Extensions; using GreenDonut.Data; using Laraue.EfCoreTriggers.Common.Extensions; using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore; @@ -16,6 +15,7 @@ using SchemaNameOptionsExtension = Database.Data.Extensions.SchemaNameOptionsExtension; using NodaTime; using System.Text.Json; +using Database.GraphQl; namespace Database.Data; @@ -27,6 +27,7 @@ public sealed class ApplicationDbContext { private const string DefaultSchemaName = "database"; private readonly string _schemaName; + private readonly IClock _clock; internal const string CalorimetricObserverTypeName = "calorimetric_observer"; internal const string CoatedSideTypeName = "coated_side"; @@ -38,14 +39,16 @@ public sealed class ApplicationDbContext internal const string StandardizerTypeName = "standardizer"; public ApplicationDbContext( - DbContextOptions options + DbContextOptions options, + IClock clock ) : base(options) { - // The schema-name option is set in `Metabase.Startup` by an invocation of - // `UseSchemaName` on a `DbContextOptionsBuilder` instance. + // The schema-name option is set in `Metabase.Startup` by an invocation + // of `UseSchemaName` on a `DbContextOptionsBuilder` instance. var schemaNameOptions = options.FindExtension(); _schemaName = schemaNameOptions is null ? DefaultSchemaName : schemaNameOptions.SchemaName; + _clock = clock; } // https://docs.microsoft.com/en-us/ef/core/miscellaneous/nullable-reference-types#dbcontext-and-dbset @@ -118,6 +121,53 @@ public OffsetDateTimeUtcValueConverter() } } + public override int SaveChanges() + { + UpdateTimestamps(); + return base.SaveChanges(); + } + + public override Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + UpdateTimestamps(); + return base.SaveChangesAsync(cancellationToken); + } + + private void UpdateTimestamps() + { + var entries = ChangeTracker + .Entries() + .Where(_ => + _.State == EntityState.Added + || _.State == EntityState.Modified + // || _.State == EntityState.Deleted + ); + var now = _clock.GetUtcNow().ToDateTimeOffset(); + foreach (var entry in entries) + { + switch (entry.State) + { + case EntityState.Added: + if (entry.Entity.CreatedAt == default) + { + entry.Entity.CreatedAt = now; + } + entry.Entity.UpdatedAt = now; + break; + case EntityState.Modified: + entry.Entity.UpdatedAt = now; + break; + // NOTE that soft deletes do not cascade + // case EntityState.Deleted: + // // soft delete + // entry.State = EntityState.Modified; + // entry.Entity.DeletedAt = now; + // entry.Entity.UpdatedAt = now; + // break; + } + } + } + public IQueryable Data(DataKind dataKind) { return dataKind switch @@ -144,38 +194,38 @@ QueryContext queryContext { return ( CalorimetricData.AsQueryable() - .With(queryContext, sort => sort.StabilizeOrder()) .Where(where) + .With(queryContext, Sorting.DefaultEntityOrder) .ToAsyncEnumerable() ) .Union( GeometricData.AsQueryable() - .With(queryContext, sort => sort.StabilizeOrder()) .Where(where) + .With(queryContext, Sorting.DefaultEntityOrder) .ToAsyncEnumerable() ) .Union( HygrothermalData.AsQueryable() - .With(queryContext, sort => sort.StabilizeOrder()) .Where(where) + .With(queryContext, Sorting.DefaultEntityOrder) .ToAsyncEnumerable() ) .Union( LifeCycleData.AsQueryable() - .With(queryContext, sort => sort.StabilizeOrder()) .Where(where) + .With(queryContext, Sorting.DefaultEntityOrder) .ToAsyncEnumerable() ) .Union( OpticalData.AsQueryable() - .With(queryContext, sort => sort.StabilizeOrder()) .Where(where) + .With(queryContext, Sorting.DefaultEntityOrder) .ToAsyncEnumerable() ) .Union( PhotovoltaicData.AsQueryable() - .With(queryContext, sort => sort.StabilizeOrder()) .Where(where) + .With(queryContext, Sorting.DefaultEntityOrder) .ToAsyncEnumerable() ); } @@ -515,5 +565,63 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity() ) .ToTable("institution_access_rights"); + foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + { + if (typeof(IEntity).IsAssignableFrom(entityType.ClrType)) + { + var entity = modelBuilder.Entity(entityType.ClrType); + // https://www.npgsql.org/efcore/modeling/generated-properties.html#guiduuid-generation + entity + .Property(nameof(IEntity.Id)) + .HasDefaultValueSql("gen_random_uuid()"); + // https://www.npgsql.org/efcore/modeling/concurrency.html#the-postgresql-xmin-system-column + entity + .Property(nameof(IEntity.Version)) + .IsRowVersion(); + } + if (typeof(IAssociation).IsAssignableFrom(entityType.ClrType)) + { + var association = modelBuilder.Entity(entityType.ClrType); + // https://www.npgsql.org/efcore/modeling/concurrency.html#the-postgresql-xmin-system-column + association + .Property(nameof(IAssociation.Version)) + .IsRowVersion(); + } + if (typeof(IAuditable).IsAssignableFrom(entityType.ClrType)) + { + var auditable = modelBuilder.Entity(entityType.ClrType); + auditable + .Property(nameof(IAuditable.CreatedAt)) + .HasDefaultValueSql("now()"); + auditable + .Property(nameof(IAuditable.UpdatedAt)) + .HasDefaultValueSql("now()"); + // exclude soft-deleted entities with the effect that + // `context..ToList()` only returns rows where + // `DeletedAt` is null and + // `context..IgnoreQueryFilters().ToList()` returns + // all rows + // entity + // .HasQueryFilter((IAuditable _) => _.DeletedAt == null); + } + if (typeof(IEntity).IsAssignableFrom(entityType.ClrType) + && typeof(INamed).IsAssignableFrom(entityType.ClrType)) + { + var entity = modelBuilder.Entity(entityType.ClrType); + // https://www.npgsql.org/efcore/modeling/generated-properties.html#guiduuid-generation + entity + .HasIndex(nameof(INamed.Name), nameof(IEntity.Id)) + .IsUnique(); + } + if (typeof(IEntity).IsAssignableFrom(entityType.ClrType) + && typeof(IAuditable).IsAssignableFrom(entityType.ClrType)) + { + var entity = modelBuilder.Entity(entityType.ClrType); + // https://www.npgsql.org/efcore/modeling/generated-properties.html#guiduuid-generation + entity + .HasIndex(nameof(IAuditable.CreatedAt), nameof(IEntity.Id)) + .IsUnique(); + } + } } } \ No newline at end of file diff --git a/backend/src/Data/Association.cs b/backend/src/Data/Association.cs new file mode 100644 index 00000000..08751fa6 --- /dev/null +++ b/backend/src/Data/Association.cs @@ -0,0 +1,8 @@ +namespace Database.Data; + +public abstract class Association +{ + // Configured via `IsRowVersion` in `ApplicationDbContext` instead of the annotation + // [Timestamp] + public uint Version { get; private set; } // https://www.npgsql.org/efcore/modeling/concurrency.html +} \ No newline at end of file diff --git a/backend/src/Data/AuditableAssociation.cs b/backend/src/Data/AuditableAssociation.cs new file mode 100644 index 00000000..044118cd --- /dev/null +++ b/backend/src/Data/AuditableAssociation.cs @@ -0,0 +1,10 @@ +using System; + +namespace Database.Data; + +public abstract class AuditableAssociation +: Association, IAuditable +{ + public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset UpdatedAt { get; set; } +} \ No newline at end of file diff --git a/backend/src/Data/AuditableEntity.cs b/backend/src/Data/AuditableEntity.cs new file mode 100644 index 00000000..c7d71196 --- /dev/null +++ b/backend/src/Data/AuditableEntity.cs @@ -0,0 +1,20 @@ +using System; + +namespace Database.Data; + +public abstract class AuditableEntity +: Entity, IAuditable +{ + public AuditableEntity() + : base() + { + } + + public AuditableEntity(Guid id) + : base(id) + { + } + + public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset UpdatedAt { get; set; } +} \ No newline at end of file diff --git a/backend/src/Data/CalorimetricData.cs b/backend/src/Data/CalorimetricData.cs index eb118c32..26e0d52e 100644 --- a/backend/src/Data/CalorimetricData.cs +++ b/backend/src/Data/CalorimetricData.cs @@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Threading.Tasks; using Database.Extractors; -using NodaTime; namespace Database.Data; @@ -18,7 +17,7 @@ public CalorimetricData( string? description, string[] warnings, Guid creatorId, - OffsetDateTime createdAt, + DateTimeOffset createdAt, AppliedMethod appliedMethod, double[] gValues, double[] uValues @@ -47,7 +46,7 @@ public CalorimetricData( string? description, string[] warnings, Guid creatorId, - OffsetDateTime createdAt, + DateTimeOffset createdAt, double[] gValues, double[] uValues ) : base( diff --git a/backend/src/Data/DataX.cs b/backend/src/Data/DataX.cs index eddc6d40..9076ac90 100644 --- a/backend/src/Data/DataX.cs +++ b/backend/src/Data/DataX.cs @@ -3,21 +3,32 @@ using System.Linq; using System.Threading.Tasks; using Database.Enumerations; -using NodaTime; namespace Database.Data; -public abstract class DataX( - Guid? userId, - string locale, - Guid componentId, - string? name, - string? description, - string[] warnings, - Guid creatorId, - OffsetDateTime createdAt -) : Entity, IData +public abstract class DataX : AuditableEntity, IData { + public DataX( + Guid? userId, + string locale, + Guid componentId, + string? name, + string? description, + string[] warnings, + Guid creatorId, + DateTimeOffset createdAt + ) + { + UserId = userId; + Locale = locale; + ComponentId = componentId; + Name = name; + Description = description; + Warnings = warnings; + CreatorId = creatorId; + CreatedAt = createdAt; + } + protected DataX( Guid? userId, string locale, @@ -26,7 +37,7 @@ protected DataX( string? description, string[] warnings, Guid creatorId, - OffsetDateTime createdAt, + DateTimeOffset createdAt, AppliedMethod appliedMethod ) : this( @@ -49,7 +60,7 @@ public void Update( string? name, string? description, string[] warnings, - OffsetDateTime createdAt, + DateTimeOffset createdAt, Guid creatorId ) { @@ -72,14 +83,13 @@ public void Retract() PublishingState = PublishingState.RETRACTED; } - public Guid? UserId { get; private set; } = userId; - public string Locale { get; private set; } = locale; - public Guid ComponentId { get; private set; } = componentId; - public string? Name { get; private set; } = name; - public string? Description { get; private set; } = description; - public string[] Warnings { get; private set; } = warnings; - public Guid CreatorId { get; private set; } = creatorId; - public OffsetDateTime CreatedAt { get; private set; } = createdAt; + public Guid? UserId { get; private set; } + public string Locale { get; private set; } + public Guid ComponentId { get; private set; } + public string? Name { get; private set; } + public string? Description { get; private set; } + public string[] Warnings { get; private set; } + public Guid CreatorId { get; private set; } public AppliedMethod AppliedMethod { get; private set; } = default!; public ICollection Approvals { get; } = []; diff --git a/backend/src/Data/Entity.cs b/backend/src/Data/Entity.cs index b3c54bfe..d831fdc6 100644 --- a/backend/src/Data/Entity.cs +++ b/backend/src/Data/Entity.cs @@ -1,16 +1,25 @@ using System; -// using System.ComponentModel.DataAnnotations.Schema; - namespace Database.Data; public abstract class Entity : IEntity { - public Guid Id { get; private set; } + public Entity() + { + } + + public Entity(Guid id) + { + Id = id; + } + + public Guid Id { get; init; } // [NotMapped] // public Guid Uuid { get => Id; } + // Configured via `IsRowVersion` in `ApplicationDbContext` instead of the annotation + // [Timestamp] public uint Version { get; private set; } // https://www.npgsql.org/efcore/modeling/concurrency.html } \ No newline at end of file diff --git a/backend/src/Data/GeometricData.cs b/backend/src/Data/GeometricData.cs index 5be4f13c..bb5098d8 100644 --- a/backend/src/Data/GeometricData.cs +++ b/backend/src/Data/GeometricData.cs @@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Threading.Tasks; using Database.Extractors; -using NodaTime; namespace Database.Data; @@ -18,7 +17,7 @@ public GeometricData( string? description, string[] warnings, Guid creatorId, - OffsetDateTime createdAt, + DateTimeOffset createdAt, AppliedMethod appliedMethod, double[] widths, double[] heights, @@ -48,7 +47,7 @@ public GeometricData( string? description, string[] warnings, Guid creatorId, - OffsetDateTime createdAt, + DateTimeOffset createdAt, double[] widths, double[] heights, double[] thicknesses diff --git a/backend/src/Data/GetHttpsResource.cs b/backend/src/Data/GetHttpsResource.cs index a15ace42..e7537619 100644 --- a/backend/src/Data/GetHttpsResource.cs +++ b/backend/src/Data/GetHttpsResource.cs @@ -14,7 +14,7 @@ namespace Database.Data; public sealed class GetHttpsResource -: Entity +: AuditableEntity { public const string FilesDirectoryPath = "./files/"; public const string TableName = "get_https_resource"; diff --git a/backend/src/Data/HygrothermalData.cs b/backend/src/Data/HygrothermalData.cs index 3d33a023..79f79f12 100644 --- a/backend/src/Data/HygrothermalData.cs +++ b/backend/src/Data/HygrothermalData.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Threading.Tasks; -using NodaTime; namespace Database.Data; @@ -17,7 +16,7 @@ public HygrothermalData( string? description, string[] warnings, Guid creatorId, - OffsetDateTime createdAt, + DateTimeOffset createdAt, AppliedMethod appliedMethod ) : base( userId, @@ -42,7 +41,7 @@ public HygrothermalData( string? description, string[] warnings, Guid creatorId, - OffsetDateTime createdAt + DateTimeOffset createdAt ) : base( userId, locale, diff --git a/backend/src/Data/IAssociation.cs b/backend/src/Data/IAssociation.cs new file mode 100644 index 00000000..a49e56f6 --- /dev/null +++ b/backend/src/Data/IAssociation.cs @@ -0,0 +1,7 @@ +namespace Database.Data; + +public interface IAssociation +{ + // Configured via `[Timestamp]` in `Association` + public uint Version { get; } // https://www.npgsql.org/efcore/modeling/concurrency.html +} \ No newline at end of file diff --git a/backend/src/Data/IAuditable.cs b/backend/src/Data/IAuditable.cs new file mode 100644 index 00000000..2e059fc4 --- /dev/null +++ b/backend/src/Data/IAuditable.cs @@ -0,0 +1,13 @@ +using System; + +namespace Database.Data; + +public interface IAuditable +{ + // TODO Switch to NodaTime `OffsetDateTime` once there is a `ICursorKeySerializer` implementation for it. Then sorting by `CreatedAt` and `UpdatedAt` with pagination will keep working. + public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset UpdatedAt { get; set; } + + // soft delete + // public Instant? DeletedAt { get; set; } +} \ No newline at end of file diff --git a/backend/src/Data/IData.cs b/backend/src/Data/IData.cs index 79a242e4..f00832d8 100644 --- a/backend/src/Data/IData.cs +++ b/backend/src/Data/IData.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Database.Enumerations; using Database.GraphQl; -using NodaTime; namespace Database.Data; @@ -15,7 +14,7 @@ namespace Database.Data; [JsonDerivedType(typeof(LifeCycleData), typeDiscriminator: nameof(LifeCycleData))] [JsonDerivedType(typeof(OpticalData), typeDiscriminator: nameof(OpticalData))] [JsonDerivedType(typeof(PhotovoltaicData), typeDiscriminator: nameof(PhotovoltaicData))] -public interface IData : IEntity +public interface IData : IEntity, IAuditable { public static readonly Guid BedJsonDataFormatId = new("9ca9e8f5-94bf-4fdd-81e3-31a58d7ca708"); @@ -25,7 +24,6 @@ public interface IData : IEntity string? Description { get; } string[] Warnings { get; } Guid CreatorId { get; } - OffsetDateTime CreatedAt { get; } AppliedMethod AppliedMethod { get; } ICollection Approvals { get; } ICollection Resources { get; } @@ -62,7 +60,7 @@ void Update( string? name, string? description, string[] warnings, - OffsetDateTime createdAt, + DateTimeOffset createdAt, Guid creatorId ); diff --git a/backend/src/Data/INamed.cs b/backend/src/Data/INamed.cs new file mode 100644 index 00000000..a4f46fc2 --- /dev/null +++ b/backend/src/Data/INamed.cs @@ -0,0 +1,6 @@ +namespace Database.Data; + +public interface INamed +{ + public string Name { get; } +} \ No newline at end of file diff --git a/backend/src/Data/InstitutionAccessRights.cs b/backend/src/Data/InstitutionAccessRights.cs index 95dc8c2a..10db044f 100644 --- a/backend/src/Data/InstitutionAccessRights.cs +++ b/backend/src/Data/InstitutionAccessRights.cs @@ -16,7 +16,7 @@ public sealed class InstitutionAccessRights( uint? allowedDatasetsPerTime, Duration period ) -: Entity +: AuditableEntity { public Guid InstitutionId { get; set; } = institutionId; public uint? AllowedUserCount { get; set; } = allowedUserCount; @@ -38,7 +38,7 @@ Duration period [Projectable] public bool HasRestrictionsByUser => AllowedUserCount != null; - internal bool IsDataRestrictedByTime(IData dataItem, CacheService cacheService, out string? reason) + internal bool IsDataRestrictedByTime(IData dataItem, IClock clock, CacheService cacheService, out string? reason) { var isRestricted = false; reason = null; @@ -46,7 +46,7 @@ internal bool IsDataRestrictedByTime(IData dataItem, CacheService cacheService, if (AllowedDatasetsPerTime is not null) { var accessesPerPeriod = cacheService.GetOrCreateAccessCountForPeriod(InstitutionId); - if (accessesPerPeriod.StartTime + Period < OffsetDateTime.UtcNow) + if (accessesPerPeriod.StartTime + Period < clock.GetUtcNow()) { if (accessesPerPeriod.Count >= AllowedDatasetsPerTime) { diff --git a/backend/src/Data/LifeCycleData.cs b/backend/src/Data/LifeCycleData.cs index 4290aafc..c5b67c55 100644 --- a/backend/src/Data/LifeCycleData.cs +++ b/backend/src/Data/LifeCycleData.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Threading.Tasks; -using NodaTime; namespace Database.Data; @@ -17,7 +16,7 @@ public LifeCycleData( string? description, string[] warnings, Guid creatorId, - OffsetDateTime createdAt, + DateTimeOffset createdAt, AppliedMethod appliedMethod ) : base( userId, @@ -42,7 +41,7 @@ public LifeCycleData( string? description, string[] warnings, Guid creatorId, - OffsetDateTime createdAt + DateTimeOffset createdAt ) : base( userId, locale, diff --git a/backend/src/Data/OpticalData.cs b/backend/src/Data/OpticalData.cs index 7f0173c0..9c929b5c 100644 --- a/backend/src/Data/OpticalData.cs +++ b/backend/src/Data/OpticalData.cs @@ -5,7 +5,6 @@ using Database.Enumerations; using Database.Enumerations.DataPoints; using Database.Extractors; -using NodaTime; namespace Database.Data; @@ -20,7 +19,7 @@ public OpticalData( string? description, string[] warnings, Guid creatorId, - OffsetDateTime createdAt, + DateTimeOffset createdAt, OpticalComponentType? type, OpticalComponentSubtype? subtype, CoatedSide? coatedSide, @@ -65,7 +64,7 @@ public OpticalData( string? description, string[] warnings, Guid creatorId, - OffsetDateTime createdAt, + DateTimeOffset createdAt, OpticalComponentType? type, OpticalComponentSubtype? subtype, CoatedSide? coatedSide, diff --git a/backend/src/Data/PhotovoltaicData.cs b/backend/src/Data/PhotovoltaicData.cs index 41d15961..9ec61855 100644 --- a/backend/src/Data/PhotovoltaicData.cs +++ b/backend/src/Data/PhotovoltaicData.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Threading.Tasks; -using NodaTime; namespace Database.Data; @@ -17,7 +16,7 @@ public PhotovoltaicData( string? description, string[] warnings, Guid creatorId, - OffsetDateTime createdAt, + DateTimeOffset createdAt, AppliedMethod appliedMethod ) : base( userId, @@ -42,7 +41,7 @@ public PhotovoltaicData( string? description, string[] warnings, Guid creatorId, - OffsetDateTime createdAt + DateTimeOffset createdAt ) : base( userId, locale, diff --git a/backend/src/Data/User.cs b/backend/src/Data/User.cs index 1ca1b218..858e7a98 100644 --- a/backend/src/Data/User.cs +++ b/backend/src/Data/User.cs @@ -4,7 +4,7 @@ public sealed class User( string subject, string name ) -: Entity +: AuditableEntity { public string Subject { get; private set; } = subject; public string Name { get; private set; } = name; diff --git a/backend/src/Database.csproj b/backend/src/Database.csproj index 27942a7b..baa7653e 100644 --- a/backend/src/Database.csproj +++ b/backend/src/Database.csproj @@ -13,33 +13,33 @@ - - - + - - - - - - - - - - + + + + + + + + + + + - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - + + + @@ -47,25 +47,25 @@ - - - - - - - - - - - - + + + + + + + + + + + + - + diff --git a/backend/src/Extensions/EnumerableExtensions.cs b/backend/src/Extensions/EnumerableExtensions.cs deleted file mode 100644 index c7143af6..00000000 --- a/backend/src/Extensions/EnumerableExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.Contracts; -using System.Linq; - -namespace Database.Extensions; - -public static class EnumerableExtensions -{ - [Pure] - public static IEnumerable NotNull(this IEnumerable enumerable) where T : class - { - return enumerable.Where(item => item is not null).Select(item => item!); - } - - [Pure] - public static IEnumerable NotNull(this IEnumerable enumerable) where T : struct - { - return enumerable.Where(item => item.HasValue).Select(item => item!.Value); - } -} \ No newline at end of file diff --git a/backend/src/Extensions/LinqExtensions.cs b/backend/src/Extensions/LinqExtensions.cs new file mode 100644 index 00000000..346869c1 --- /dev/null +++ b/backend/src/Extensions/LinqExtensions.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Linq.Expressions; +using System.Runtime.InteropServices; + +namespace Database.Extensions; + +public enum OrderDirection +{ + ASCENDING, + DESCENDING +} + +public static class LinqExtensions +{ + [Pure] + public static IEnumerable If( + this IEnumerable source, + bool condition, + Func, IEnumerable> transform + ) + { + return condition ? transform(source) : source; + } + + [Pure] + public static IQueryable If( + this IQueryable source, + bool condition, + Func, + IQueryable> transform + ) + { + return condition ? transform(source) : source; + } + + [Pure] + public static List IfList( + this List source, + bool condition, + Func, List> transform + ) + { + return condition ? transform(source) : source; + } + + [Pure] + public static List ToReversed(this List source) + { + var copy = new List(source); + copy.Reverse(); + return copy; + } + + [Pure] + public static T? GetAtOrDefault(this T[] array, int index, T? defaultValue = default) where T : class + { + return (index >= 0 && index < array.Length) ? array[index] : defaultValue; + } + + [Pure] + public static T? GetFirstOrDefault(this T[] array) where T : class + { + return array.Length > 0 ? array[0] : default; + } + + [Pure] + public static T? GetAtOrDefault(this IReadOnlyList list, int index) where T : class + { + return (index >= 0 && index < list.Count) ? list[index] : default; + } + + [Pure] + public static T? GetFirstOrDefault(this IReadOnlyList list) where T : class + { + return list.Count > 0 ? list[0] : default; + } + + [Pure] + public static T? GetLastOrDefault(this IReadOnlyList list) where T : class + { + return list.Count > 0 ? list[^1] : default; + } + + [Pure] + public static IEnumerable NotNull(this IEnumerable enumerable) where T : class + { + return enumerable.Where(item => item is not null).Select(item => item!); + } + + [Pure] + public static IEnumerable NotNull(this IEnumerable enumerable) where T : struct + { + return enumerable.Where(item => item.HasValue).Select(item => item!.Value); + } + + [Pure] + public static IOrderedQueryable OrderByDirection( + this IQueryable source, + Expression> keySelector, + OrderDirection direction + ) + { + return direction is OrderDirection.ASCENDING + ? source.OrderBy(keySelector) + : source.OrderByDescending(keySelector); + } + + + [Pure] + public static IEnumerable Interleave(this IEnumerable> sequences) + { + var enumerators = new LinkedList>(); + try + { + foreach (var sequence in sequences) + { + var enumerator = sequence.GetEnumerator(); + if (enumerator.MoveNext()) + { + enumerators.AddLast(enumerator); + yield return enumerator.Current; + } + else + { + enumerator.Dispose(); + } + } + var node = enumerators.First; + while (node is { Value: var enumerator, Next: var nextNode }) + { + if (enumerator.MoveNext()) + { + yield return enumerator.Current; + } + else + { + enumerators.Remove(node); + enumerator.Dispose(); + } + node = nextNode ?? enumerators.First; + } + } + finally + { + foreach (var enumerator in enumerators) + enumerator.Dispose(); + } + } + + [Pure] + public static IEnumerable Scan( + this IEnumerable source, + TAccumulate seed, + Func function) + { + var accumulate = seed; + foreach (var item in source) + { + (accumulate, var result) = function(accumulate, item); + yield return result; + } + } + + [Pure] + public static List Rotate( + this List list, + Predicate after + ) + where T : class + { + if (list.Count is 0) + { + return list; + } + var afterIndex = list.FindIndex(after); + if (afterIndex is -1) + { + return list; + } + var index = (afterIndex + 1) % list.Count; + var result = new List(list.Count); + var span = CollectionsMarshal.AsSpan(list); + result.AddRange(span.Slice(index)); + result.AddRange(span.Slice(0, index)); + return result; + } +} \ No newline at end of file diff --git a/backend/src/Extensions/NodaTimeExtensions.cs b/backend/src/Extensions/NodaTimeExtensions.cs index 9f5edc65..80d21035 100644 --- a/backend/src/Extensions/NodaTimeExtensions.cs +++ b/backend/src/Extensions/NodaTimeExtensions.cs @@ -4,13 +4,18 @@ namespace Database.Extensions; public static class NodaTimeExtensions { - extension(OffsetDateTime) + public static OffsetDateTime GetUtcNow(this IClock clock) { - public static OffsetDateTime UtcNow => - SystemClock.Instance - .GetCurrentInstant() - .WithOffset(Offset.Zero); + return clock.GetCurrentInstant().WithOffset(Offset.Zero); + } + public static int CompareTo(this OffsetDateTime current, OffsetDateTime other) + { + return OffsetDateTime.Comparer.Instant.Compare(current, other); + } + + extension(OffsetDateTime) + { public static bool operator >(OffsetDateTime x, OffsetDateTime y) { return OffsetDateTime.Comparer.Instant.Compare(x, y) > 0; diff --git a/backend/src/Extensions/StringExtensions.cs b/backend/src/Extensions/StringExtensions.cs index 435228db..5d006340 100644 --- a/backend/src/Extensions/StringExtensions.cs +++ b/backend/src/Extensions/StringExtensions.cs @@ -5,6 +5,21 @@ namespace Database.Extensions; public static class StringExtensions { + public static string FirstCharToLower(this string value) + { + return string.IsNullOrEmpty(value) + || !char.IsLetter(value, 0) + || char.IsLower(value, 0) + ? value + : char.ToLowerInvariant(value[0]) + value[1..]; + } + + public static string? NullIfEmpty(this string value) + => string.IsNullOrEmpty(value) ? null : value; + + public static string? NullIfWhitespace(this string value) + => string.IsNullOrWhiteSpace(value) ? null : value; + public static string Base64Encode(this string plainText) { return Convert.ToBase64String( diff --git a/backend/src/GraphQl/Associations/AuditableAssociationFilterType.cs b/backend/src/GraphQl/Associations/AuditableAssociationFilterType.cs new file mode 100644 index 00000000..70059ad5 --- /dev/null +++ b/backend/src/GraphQl/Associations/AuditableAssociationFilterType.cs @@ -0,0 +1,19 @@ +using HotChocolate.Data.Filters; +using Database.Data; + +namespace Database.GraphQl.Associations; + +public abstract class AuditableAssociationFilterType + : FilterInputType + where TAssociation : IAssociation, IAuditable +{ + protected override void Configure( + IFilterInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.BindFieldsExplicitly(); + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Associations/AuditableAssociationSortType.cs b/backend/src/GraphQl/Associations/AuditableAssociationSortType.cs new file mode 100644 index 00000000..05109771 --- /dev/null +++ b/backend/src/GraphQl/Associations/AuditableAssociationSortType.cs @@ -0,0 +1,19 @@ +using HotChocolate.Data.Sorting; +using Database.Data; + +namespace Database.GraphQl.Associations; + +public abstract class AuditableAssociationSortType + : SortInputType + where TAssociation : IAssociation, IAuditable +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.BindFieldsExplicitly(); + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/AuthorizedConnection.cs b/backend/src/GraphQl/AuthorizedConnection.cs new file mode 100644 index 00000000..9a13a8b6 --- /dev/null +++ b/backend/src/GraphQl/AuthorizedConnection.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut; +using GreenDonut.Data; +using HotChocolate.CostAnalysis.Types; +using Database.Data; + +namespace Database.GraphQl; + +public abstract class AuthorizedConnection( + TSubject subject, + Func createEdge, + Func> isAuthorized, + QueryContext queryContext +) : Connection(subject, createEdge, queryContext) + where TSubject : IEntity + where TAssociationsByOneIdDataLoader : IDataLoader +{ + [Cost(0)] + public async IAsyncEnumerable GetEdgesAsync( + ClaimsPrincipal claimsPrincipal, + TAuthorization authorization, + TAssociationsByOneIdDataLoader dataLoader, + [EnumeratorCancellation] CancellationToken cancellationToken + ) + { + if (!await isAuthorized(claimsPrincipal, Subject, authorization, cancellationToken)) + { + yield break; + } + await foreach (var edge in GetEdgesAsync(dataLoader, cancellationToken)) + { + yield return edge; + } + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/AuthorizedPaginatedConnection.cs b/backend/src/GraphQl/AuthorizedPaginatedConnection.cs new file mode 100644 index 00000000..c418864d --- /dev/null +++ b/backend/src/GraphQl/AuthorizedPaginatedConnection.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut; +using GreenDonut.Data; +using HotChocolate.CostAnalysis.Types; +using Database.Data; + +namespace Database.GraphQl; + +public abstract class AuthorizedPaginatedConnection( + TSubject subject, + Func createEdge, + Func> isAuthorized, + PagingArguments pagingArguments, + QueryContext queryContext +) : PaginatedConnection( + subject, + createEdge, + pagingArguments, + queryContext +) + where TSubject : IEntity + where TAssociation : class + where TAssociationsByOneIdDataLoader : IDataLoader> +{ + [Cost(0)] + public async IAsyncEnumerable GetEdgesAsync( + ClaimsPrincipal claimsPrincipal, + TAuthorization authorization, + TAssociationsByOneIdDataLoader dataLoader, + [EnumeratorCancellation] CancellationToken cancellationToken + ) + { + if (!await isAuthorized(claimsPrincipal, authorization, cancellationToken)) + { + yield break; + } + await foreach (var edge in base.GetEdgesAsync(dataLoader, cancellationToken)) + { + yield return edge; + } + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataByIdDataLoader.cs b/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataByIdDataLoader.cs deleted file mode 100644 index 52cbce26..00000000 --- a/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataByIdDataLoader.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Database.Data; -using Database.GraphQl.Entities; -using GreenDonut; -using Microsoft.EntityFrameworkCore; - -namespace Database.GraphQl.CalorimetricDataX; - -public sealed class CalorimetricDataByIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : EntityByIdDataLoader( - batchScheduler, - options, - dbContextFactory, - dbContext => dbContext.CalorimetricData - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataLoaders.cs b/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataLoaders.cs new file mode 100644 index 00000000..eea3cf77 --- /dev/null +++ b/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataLoaders.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut; +using GreenDonut.Data; +using Database.Data; +using Microsoft.EntityFrameworkCore; + +namespace Database.GraphQl.CalorimetricDataX; + +public sealed class CalorimetricDataLoaders +: DataLoaders +{ + [DataLoader] + public static ValueTask> GetCalorimetricDataByIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetEntityByIdAsync( + ids, + databaseContext => databaseContext.CalorimetricData, + queryContext, + databaseContextFactory, + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataQueries.cs b/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataQueries.cs index 3bd1573b..99684588 100644 --- a/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataQueries.cs +++ b/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataQueries.cs @@ -1,14 +1,13 @@ using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Database.Authorization; using Database.Data; using Database.GraphQl.DataX; +using Database.GraphQl.Scalars; using Database.Services; using HotChocolate; using HotChocolate.Data; -using HotChocolate.Data.Sorting; using HotChocolate.Resolvers; using HotChocolate.Types; @@ -19,15 +18,12 @@ public sealed class CalorimetricDataQueries : DataQueriesBase { [UsePaging] - // [UseProjection] // We disabled projections because when requesting `id` all results had the - // same `id` and when also requesting `uuid`, the latter was always the empty UUID `000...`. [UseFiltering] [UseSorting] - public Task> GetAllCalorimetricDataAsync( + public Task> GetAllCalorimetricDataAsync( [GraphQLType] string? locale, ApplicationDbContext context, AccessRightsService accessRightsService, - ISortingContext sorting, IResolverContext resolverContext, CancellationToken cancellationToken ) @@ -36,7 +32,6 @@ CancellationToken cancellationToken context.CalorimetricData, locale, accessRightsService, - sorting, resolverContext, cancellationToken ); @@ -47,11 +42,10 @@ CancellationToken cancellationToken // same `id` and when also requesting `uuid`, the latter was always the empty UUID `000...`. [UseFiltering] [UseSorting] - public Task> GetAllPendingCalorimetricDataAsync( + public Task> GetAllPendingCalorimetricDataAsync( [GraphQLType] string? locale, ApplicationDbContext context, AccessRightsService accessRightsService, - ISortingContext sorting, IResolverContext resolverContext, CommonAuthorization authorization, CancellationToken cancellationToken @@ -61,7 +55,6 @@ CancellationToken cancellationToken context.CalorimetricData, locale, accessRightsService, - sorting, resolverContext, authorization, cancellationToken diff --git a/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataType.cs b/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataType.cs index 6951cdf9..f395b413 100644 --- a/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataType.cs +++ b/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataType.cs @@ -5,7 +5,7 @@ namespace Database.GraphQl.CalorimetricDataX; public sealed class CalorimetricDataType - : DataTypeBase + : DataTypeBase { protected override void Configure( IObjectTypeDescriptor descriptor diff --git a/backend/src/GraphQl/CalorimetricDataX/CreateCalorimetricDataMutation.cs b/backend/src/GraphQl/CalorimetricDataX/CreateCalorimetricDataMutation.cs index 5fa7bbbb..1c84da8e 100644 --- a/backend/src/GraphQl/CalorimetricDataX/CreateCalorimetricDataMutation.cs +++ b/backend/src/GraphQl/CalorimetricDataX/CreateCalorimetricDataMutation.cs @@ -8,6 +8,7 @@ using Database.Data; using Database.Extensions; using Database.GraphQl.DataX; +using Database.GraphQl.Scalars; using Database.Services; using HotChocolate; using HotChocolate.Types; @@ -21,7 +22,7 @@ public sealed record CreateCalorimetricDataInput( string? Name, string? Description, string[] Warnings, - OffsetDateTime CreatedAt, + DateTimeOffset CreatedAt, Guid CreatorId, AppliedMethodInput AppliedMethod, RootGetHttpsResourceInput RootResource, @@ -106,6 +107,7 @@ public async Task CreateCalorimetricDataAsync( IDataByDatabaseAndIdAndKindDataLoader dataByDatabaseAndIdAndKindDataLoader, IDataFormatByIdDataLoader dataFormatByIdDataLoader, ResponseApprovalService responseApprovalService, + IClock clock, CancellationToken cancellationToken ) { @@ -135,6 +137,7 @@ CancellationToken cancellationToken CreateCalorimetricDataErrorCode.UNKNOWN_DATA, dataFormatByIdDataLoader, CreateCalorimetricDataErrorCode.UNKNOWN_DATA_FORMAT, + clock, cancellationToken ) ).Failed(out var dataFormat, out var validateErrorPayload) @@ -167,4 +170,4 @@ CancellationToken cancellationToken return NewPayload(calorimetricData, null); } -} +} \ No newline at end of file diff --git a/backend/src/GraphQl/Connection.cs b/backend/src/GraphQl/Connection.cs index 595ae887..7d737d3b 100644 --- a/backend/src/GraphQl/Connection.cs +++ b/backend/src/GraphQl/Connection.cs @@ -4,41 +4,48 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; -using Database.Data; using GreenDonut; using GreenDonut.Data; +using HotChocolate.CostAnalysis.Types; +using Database.Data; namespace Database.GraphQl; -public abstract class Connection( +public abstract class Connection( TSubject subject, Func createEdge, QueryContext queryContext - ) +) where TSubject : IEntity - where TAssociationsByAssociateIdDataLoader : IDataLoader + where TAssociationsByOneIdDataLoader : IDataLoader { - private readonly Func _createEdge = createEdge; - private readonly QueryContext _queryContext = queryContext; - protected TSubject Subject { get; } = subject; + [Cost(0)] public async Task GetTotalCountAsync( - TAssociationsByAssociateIdDataLoader dataLoader, + TAssociationsByOneIdDataLoader dataLoader, CancellationToken cancellationToken ) { - return (uint)(await dataLoader.With(_queryContext).LoadRequiredAsync(Subject.Id, cancellationToken)).Length; + return (uint)( + ( + await dataLoader + .With(queryContext) + .LoadAsync(Subject.Id, cancellationToken) + ) + ?.Length ?? 0 + ); } + [Cost(0)] public async IAsyncEnumerable GetEdgesAsync( - TAssociationsByAssociateIdDataLoader dataLoader, + TAssociationsByOneIdDataLoader dataLoader, [EnumeratorCancellation] CancellationToken cancellationToken ) { - foreach (var association in await dataLoader.With(_queryContext).LoadRequiredAsync(Subject.Id, cancellationToken)) + foreach (var association in await dataLoader.With(queryContext).LoadAsync(Subject.Id, cancellationToken) ?? []) { - yield return _createEdge(association); + yield return createEdge(association); } } } \ No newline at end of file diff --git a/backend/src/GraphQl/DataLoaders.cs b/backend/src/GraphQl/DataLoaders.cs new file mode 100644 index 00000000..e3f59548 --- /dev/null +++ b/backend/src/GraphQl/DataLoaders.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut.Data; +using LinqKit; +using Database.Data; +using Microsoft.EntityFrameworkCore; + +namespace Database.GraphQl; + +public abstract class DataLoaders +{ + public static async ValueTask> GetEntityByIdAsync + ( + IReadOnlyList ids, + Func> getEntities, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + where TEntity : class, IEntity, IAuditable + { + await using var databaseContext = + databaseContextFactory.CreateDbContext(); + return await getEntities(databaseContext) + .AsNoTrackingWithIdentityResolution() + .Where(_ => ids.Contains(_.Id)) + .With(queryContext, Sorting.DefaultEntityOrder) + .ToDictionaryAsync(_ => _.Id, cancellationToken); + } + + public static async ValueTask> GetManyByOneIdAsync( + IReadOnlyList ids, + Func> getMany, + Expression> getOneId, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + where TMany : class, IEntity, IAuditable + { + await using var databaseContext = + databaseContextFactory.CreateDbContext(); + return + await getMany(databaseContext) + .AsExpandable() + .AsNoTracking() + .Where(_ => ids.Contains(getOneId.Invoke(_))) + .With(queryContext, Sorting.DefaultEntityOrder) + .GroupBy(getOneId) + .Select(_ => new { _.Key, Items = _.ToArray() }) + .ToDictionaryAsync(_ => _.Key, _ => _.Items, cancellationToken); + } + + public static async ValueTask>> GetManyByOneIdAsync( + IReadOnlyList ids, + Func> getMany, + Expression> getOneId, + PagingArguments pagingArguments, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + where TMany : class, IEntity, IAuditable + { + await using var databaseContext = + databaseContextFactory.CreateDbContext(); + return + await getMany(databaseContext) + .AsExpandable() + .AsNoTracking() + .Where(_ => ids.Contains(getOneId.Invoke(_))) + .With(queryContext, Sorting.DefaultEntityOrder) + .ToBatchPageAsync(getOneId, pagingArguments, cancellationToken); + } + + public static async ValueTask> GetAssociationsByOneIdAsync( + IReadOnlyList ids, + Func> getAssociations, + Expression> getOneId, + Expression> getOtherId, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + where TAssociation : class, IAssociation, IAuditable + { + await using var databaseContext = + databaseContextFactory.CreateDbContext(); + return + await getAssociations(databaseContext) + .AsExpandable() + .AsNoTracking() + .Where(_ => ids.Contains(getOneId.Invoke(_))) + .With(queryContext, _ => _.AddDescending(getOneId).AddDescending(getOtherId)) + .GroupBy(getOneId) + .Select(_ => new { _.Key, Items = _.ToArray() }) + .ToDictionaryAsync(_ => _.Key, _ => _.Items, cancellationToken); + } + + public static async ValueTask>> GetAssociationsByOneIdAsync( + IReadOnlyList ids, + Func> getAssociations, + Expression> getOneId, + Expression> getOtherId, + PagingArguments pagingArguments, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + where TAssociation : class, IAssociation, IAuditable + { + await using var databaseContext = + databaseContextFactory.CreateDbContext(); + return + await getAssociations(databaseContext) + .AsExpandable() + .AsNoTracking() + .Where(_ => ids.Contains(getOneId.Invoke(_))) + .With(queryContext, _ => _.AddDescending(getOneId).AddDescending(getOtherId)) + .ToBatchPageAsync(getOneId, pagingArguments, cancellationToken); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/CreateDataMutationBase.cs b/backend/src/GraphQl/DataX/CreateDataMutationBase.cs index 378f9927..7471b6c8 100644 --- a/backend/src/GraphQl/DataX/CreateDataMutationBase.cs +++ b/backend/src/GraphQl/DataX/CreateDataMutationBase.cs @@ -16,7 +16,7 @@ namespace Database.GraphQl.DataX; public interface IValidateCreateInput { Guid ComponentId { get; } - OffsetDateTime CreatedAt { get; } + DateTimeOffset CreatedAt { get; } Guid CreatorId { get; } AppliedMethodInput AppliedMethod { get; } RootGetHttpsResourceInput RootResource { get; } @@ -42,6 +42,7 @@ protected async Task> ValidateAsync( TErrorCode unknownCrossDatabaseData, IDataFormatByIdDataLoader dataFormatByIdDataLoader, TErrorCode unknownDataFormatErrorCode, + IClock clock, CancellationToken cancellationToken ) { @@ -56,7 +57,7 @@ CancellationToken cancellationToken ) ); } - if (input.CreatedAt > OffsetDateTime.UtcNow) + if (input.CreatedAt > clock.GetUtcNow().ToDateTimeOffset()) { errors.Add( NewError( diff --git a/backend/src/GraphQl/DataX/DataConnection.cs b/backend/src/GraphQl/DataX/DataConnection.cs index a6e4b206..23315c1f 100644 --- a/backend/src/GraphQl/DataX/DataConnection.cs +++ b/backend/src/GraphQl/DataX/DataConnection.cs @@ -1,6 +1,4 @@ -using System; using System.Collections.Generic; -using System.Linq; using HotChocolate.Types.Pagination; namespace Database.GraphQl.DataX; @@ -9,11 +7,11 @@ public sealed class DataConnection( IReadOnlyList edges, uint totalCount, ConnectionPageInfo pageInfo - ) - : DataConnectionBase( - edges, - totalCount, - pageInfo - ) +) +: DataConnectionBase( + edges, + totalCount, + pageInfo +) { } \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/DataConnectionBase.cs b/backend/src/GraphQl/DataX/DataConnectionBase.cs index ae976dd8..2d7a0aa0 100644 --- a/backend/src/GraphQl/DataX/DataConnectionBase.cs +++ b/backend/src/GraphQl/DataX/DataConnectionBase.cs @@ -1,7 +1,6 @@ -using System; using System.Collections.Generic; +using Database.GraphQl.Scalars; using HotChocolate; -using HotChocolate.Types; using HotChocolate.Types.Pagination; namespace Database.GraphQl.DataX; @@ -10,7 +9,7 @@ public abstract class DataConnectionBase( IReadOnlyList edges, uint totalCount, ConnectionPageInfo pageInfo - ) +) { public IReadOnlyList Edges { get; } = edges; diff --git a/backend/src/GraphQl/DataX/DataDataLoaders.cs b/backend/src/GraphQl/DataX/DataDataLoaders.cs new file mode 100644 index 00000000..26ea47c6 --- /dev/null +++ b/backend/src/GraphQl/DataX/DataDataLoaders.cs @@ -0,0 +1,69 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut; +using GreenDonut.Data; +using Database.Data; +using Microsoft.EntityFrameworkCore; + +namespace Database.GraphQl.DataX; + +public sealed class DataDataLoaders +: DataLoaders +{ + [DataLoader] + public static ValueTask> GetHttpsResourcesByDataIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetManyByOneIdAsync( + ids, + (databaseContext) => databaseContext.GetHttpsResources, + _ => _.DataId, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask> GetHttpsResourceTreeNonRootVerticesByDataIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetManyByOneIdAsync( + ids, + (databaseContext) => databaseContext.GetHttpsResources.Where(_ => _.ParentId != null), + _ => _.DataId, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask> GetHttpsResourceTreeRootByDataIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetManyByOneIdAsync( + ids, + (databaseContext) => databaseContext.GetHttpsResources.Where(_ => _.ParentId == null), + _ => _.DataId, + queryContext, + databaseContextFactory, + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/DataFilterTypeBase.cs b/backend/src/GraphQl/DataX/DataFilterTypeBase.cs index 7f56ab86..001f734f 100644 --- a/backend/src/GraphQl/DataX/DataFilterTypeBase.cs +++ b/backend/src/GraphQl/DataX/DataFilterTypeBase.cs @@ -5,8 +5,8 @@ namespace Database.GraphQl.DataX; public abstract class DataFilterTypeBase - : EntityFilterType - where TData : IData + : AuditableEntityFilterType + where TData : IData, IAuditable { protected override void Configure( IFilterInputTypeDescriptor descriptor @@ -19,7 +19,6 @@ IFilterInputTypeDescriptor descriptor descriptor.Field(x => x.Description); descriptor.Field(x => x.ComponentId); descriptor.Field(x => x.CreatorId); - descriptor.Field(x => x.CreatedAt); descriptor.Field(x => x.AppliedMethod); descriptor.Field(x => x.Approvals); descriptor.Field(x => x.Resources); diff --git a/backend/src/GraphQl/DataX/DataQueries.cs b/backend/src/GraphQl/DataX/DataQueries.cs index 6885c5fe..d463ec65 100644 --- a/backend/src/GraphQl/DataX/DataQueries.cs +++ b/backend/src/GraphQl/DataX/DataQueries.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Database.Data; using Database.Enumerations; +using Database.GraphQl.Scalars; using Database.Services; using HotChocolate; using HotChocolate.Types; diff --git a/backend/src/GraphQl/DataX/DataQueriesBase.cs b/backend/src/GraphQl/DataX/DataQueriesBase.cs index 216b0ec6..17d29e04 100644 --- a/backend/src/GraphQl/DataX/DataQueriesBase.cs +++ b/backend/src/GraphQl/DataX/DataQueriesBase.cs @@ -1,16 +1,15 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Database.Authorization; using Database.Data; -using Database.GraphQl.Entities; using Database.GraphQl.Extensions; +using Database.GraphQl.Scalars; using Database.Services; +using GreenDonut.Data; using HotChocolate; using HotChocolate.Data; -using HotChocolate.Data.Sorting; using HotChocolate.Resolvers; using Microsoft.EntityFrameworkCore; @@ -19,33 +18,31 @@ namespace Database.GraphQl.DataX; public abstract class DataQueriesBase where TData : class, IData { - protected async Task> GetAllDataAsync( + protected async Task> GetAllDataAsync( DbSet data, [GraphQLType] string? locale, AccessRightsService accessRightsService, - ISortingContext sorting, IResolverContext resolverContext, CancellationToken cancellationToken ) { - sorting.StabilizeOrder(); - var filteredData = + var sortedAndFilteredData = data.AsNoTracking() .Where(_ => _.PublishingState != Enumerations.PublishingState.PENDING) - .Sort(resolverContext) - .Filter(resolverContext); - if (!await filteredData.AnyAsync(x => x.DataAccessRights.HasRestrictions, cancellationToken)) + .With(resolverContext.GetQueryContext(), Sorting.DefaultEntityOrder); + if (await sortedAndFilteredData.AnyAsync(_ => _.DataAccessRights.HasRestrictions, cancellationToken)) { - return filteredData; + sortedAndFilteredData = (await accessRightsService.ApplyAccessRightsOnData(sortedAndFilteredData, cancellationToken)).AsQueryable(); } - return await accessRightsService.ApplyAccessRightsOnData(filteredData, cancellationToken); + return await sortedAndFilteredData + .ToPageAsync(resolverContext.GetPagingArguments(), cancellationToken) + .ToConnectionAsync(); } - protected async Task> GetAllPendingDataAsync( + protected async Task> GetAllPendingDataAsync( DbSet data, [GraphQLType] string? locale, AccessRightsService accessRightsService, - ISortingContext sorting, IResolverContext resolverContext, CommonAuthorization authorization, CancellationToken cancellationToken @@ -53,19 +50,22 @@ CancellationToken cancellationToken { if (!await authorization.IsDatabaseOperator(cancellationToken)) { - return Enumerable.Empty().AsQueryable(); + return await Enumerable.Empty() + .AsQueryable() + .ToPageAsync(resolverContext.GetPagingArguments(), cancellationToken) + .ToConnectionAsync(); } - sorting.StabilizeOrder(); - var filteredData = + var sortedAndFilteredData = data.AsNoTracking() .Where(_ => _.PublishingState == Enumerations.PublishingState.PENDING) - .Sort(resolverContext) - .Filter(resolverContext); - if (!await filteredData.AnyAsync(x => x.DataAccessRights.HasRestrictions, cancellationToken)) + .With(resolverContext.GetQueryContext(), Sorting.DefaultEntityOrder); + if (await sortedAndFilteredData.AnyAsync(_ => _.DataAccessRights.HasRestrictions, cancellationToken)) { - return filteredData; + sortedAndFilteredData = (await accessRightsService.ApplyAccessRightsOnData(sortedAndFilteredData, cancellationToken)).AsQueryable(); } - return await accessRightsService.ApplyAccessRightsOnData(filteredData, cancellationToken); + return await sortedAndFilteredData + .ToPageAsync(resolverContext.GetPagingArguments(), cancellationToken) + .ToConnectionAsync(); } protected Task HasDataAsync( @@ -77,14 +77,14 @@ CancellationToken cancellationToken { return data.AsNoTracking() .Where(_ => _.PublishingState != Enumerations.PublishingState.PENDING) - .Filter(resolverContext) + .With(resolverContext.GetQueryContext(), Sorting.DefaultEntityOrder) .AnyAsync(cancellationToken); } protected async Task GetDataAsync( Guid id, [GraphQLType] string? locale, - EntityByIdDataLoader byId, + GreenDonut.DataLoaderBase byId, AccessRightsService accessRightsService, CancellationToken cancellationToken ) diff --git a/backend/src/GraphQl/DataX/DataResolvers.cs b/backend/src/GraphQl/DataX/DataResolvers.cs index 68ab7997..a605d4fe 100644 --- a/backend/src/GraphQl/DataX/DataResolvers.cs +++ b/backend/src/GraphQl/DataX/DataResolvers.cs @@ -1,8 +1,6 @@ -using System; using System.Threading; using System.Threading.Tasks; using Database.Data; -using Database.Extensions; using Database.GraphQl.Extensions; using Database.GraphQl.GetHttpsResources; using GreenDonut; @@ -10,7 +8,6 @@ using HotChocolate; using HotChocolate.Data; using HotChocolate.Resolvers; -using NodaTime; namespace Database.GraphQl.DataX; @@ -18,29 +15,22 @@ public sealed class DataResolvers { [UseFiltering] [UseSorting] - public async Task GetGetHttpsResources( + public Task GetHttpsResources( [Parent] IData data, IResolverContext resolverContext, - GetHttpsResourcesByDataIdDataLoader byId, + IHttpsResourcesByDataIdDataLoader byId, CancellationToken cancellationToken ) { - var queryContext = resolverContext.GetQueryContext(); - return await - byId - .With(queryContext) + return byId + .With(resolverContext.GetQueryContext()) .LoadRequiredAsync(data.Id, cancellationToken); } - public GetHttpsResourceTree GetGetHttpsResourceTree( + public GetHttpsResourceTree GetHttpsResourceTree( [Parent] IData data ) { return new GetHttpsResourceTree(data); } - - public OffsetDateTime GetTimestamp() - { - return OffsetDateTime.UtcNow; - } } \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/DataSortTypeBase.cs b/backend/src/GraphQl/DataX/DataSortTypeBase.cs index 880630de..c5e92791 100644 --- a/backend/src/GraphQl/DataX/DataSortTypeBase.cs +++ b/backend/src/GraphQl/DataX/DataSortTypeBase.cs @@ -5,8 +5,8 @@ namespace Database.GraphQl.DataX; public abstract class DataSortTypeBase - : EntitySortType - where TData : IData + : AuditableEntitySortType + where TData : IData, IAuditable { protected override void Configure( ISortInputTypeDescriptor descriptor @@ -18,7 +18,6 @@ ISortInputTypeDescriptor descriptor descriptor.Field(x => x.Description); descriptor.Field(x => x.ComponentId); descriptor.Field(x => x.CreatorId); - descriptor.Field(x => x.CreatedAt); descriptor.Field(x => x.AppliedMethod); } } \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/DataType.cs b/backend/src/GraphQl/DataX/DataType.cs index 0af49f42..101b46ba 100644 --- a/backend/src/GraphQl/DataX/DataType.cs +++ b/backend/src/GraphQl/DataX/DataType.cs @@ -1,4 +1,5 @@ using Database.Data; +using Database.GraphQl.Scalars; using HotChocolate.Types; namespace Database.GraphQl.DataX; @@ -16,27 +17,27 @@ IInterfaceTypeDescriptor descriptor { // `1..` is a range as introduced in https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-8#indices-and-ranges descriptor.Name(nameof(IData)[1..]); + descriptor + .Field(_ => _.PublishingState) + .Ignore(); descriptor .Field(GraphQlConstants.UuidFieldName) .Type>(); + descriptor + .Field(_ => _.UpdatedAt) + .Name(TimestampFieldName); + descriptor + .Field(x => x.Locale) + .Type>(); descriptor .Field(DatabaseIdFieldName) .Type>() .Resolve(_ => appSettings.DatabaseId); - descriptor - .Field(TimestampFieldName) - .Type>(); descriptor .Field(ResourceTreeFieldName) .Type>>(); - descriptor - .Field(x => x.Locale) - .Type>(); descriptor .Field(x => x.Approval) .Type>>(); - descriptor - .Field(_ => _.PublishingState) - .Ignore(); } } \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/DataTypeBase.cs b/backend/src/GraphQl/DataX/DataTypeBase.cs index 4c8373ef..bcfcda17 100644 --- a/backend/src/GraphQl/DataX/DataTypeBase.cs +++ b/backend/src/GraphQl/DataX/DataTypeBase.cs @@ -1,5 +1,7 @@ using System; using Database.Data; +using Database.GraphQl.Entities; +using Database.GraphQl.Scalars; using GreenDonut; using HotChocolate.Types; @@ -8,31 +10,30 @@ namespace Database.GraphQl.DataX; public abstract class DataTypeBase : EntityType where TData : IData - where TDataByIdDataLoader : IDataLoader + where TDataByIdDataLoader : IDataLoader { protected override void Configure( IObjectTypeDescriptor descriptor ) { base.Configure(descriptor); + descriptor + .Field(_ => _.PublishingState) + .Ignore(); + descriptor + .Field(_ => _.UpdatedAt) + .Name(DataType.TimestampFieldName); descriptor .Field(x => x.Locale) .Type>(); descriptor .Field(x => x.Resources) - .ResolveWith(t => t.GetGetHttpsResources(default!, default!, default!, default!)); + .ResolveWith(t => t.GetHttpsResources(default!, default!, default!, default!)); descriptor .Field(DataType.ResourceTreeFieldName) - .ResolveWith(t => t.GetGetHttpsResourceTree(default!)); - descriptor - .Field(DataType.TimestampFieldName) - .Type>() - .ResolveWith(t => t.GetTimestamp()); + .ResolveWith(t => t.GetHttpsResourceTree(default!)); descriptor .Field(x => x.Approval) .Type>>(); - descriptor - .Field(_ => _.PublishingState) - .Ignore(); } } \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/GetHttpsResourceTree.cs b/backend/src/GraphQl/DataX/GetHttpsResourceTree.cs index 5751c639..76c9c24a 100644 --- a/backend/src/GraphQl/DataX/GetHttpsResourceTree.cs +++ b/backend/src/GraphQl/DataX/GetHttpsResourceTree.cs @@ -16,7 +16,7 @@ Data.IData data ) { public async Task GetRoot( - GetHttpsResourceTreeRootByDataIdDataLoader byId, + IHttpsResourceTreeRootByDataIdDataLoader byId, CancellationToken cancellationToken ) { @@ -30,14 +30,13 @@ CancellationToken cancellationToken [UseSorting] public async Task> GetNonRootVertices( IResolverContext resolverContext, - GetHttpsResourceTreeNonRootVerticesByDataIdDataLoader byId, + IHttpsResourceTreeNonRootVerticesByDataIdDataLoader byId, CancellationToken cancellationToken ) { - var queryContext = resolverContext.GetQueryContext(); return ( await byId - .With(queryContext) + .With(resolverContext.GetQueryContext()) .LoadRequiredAsync(data.Id, cancellationToken) ) .Select(v => diff --git a/backend/src/GraphQl/DataX/GetHttpsResourceTreeNonRootVerticesByDataIdDataLoader.cs b/backend/src/GraphQl/DataX/GetHttpsResourceTreeNonRootVerticesByDataIdDataLoader.cs deleted file mode 100644 index 51446c32..00000000 --- a/backend/src/GraphQl/DataX/GetHttpsResourceTreeNonRootVerticesByDataIdDataLoader.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Linq; -using Database.Data; -using Database.GraphQl.Entities; -using GreenDonut; -using Microsoft.EntityFrameworkCore; - -namespace Database.GraphQl.DataX; - -public sealed class GetHttpsResourceTreeNonRootVerticesByDataIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids) => - dbContext.GetHttpsResources.AsNoTracking().Where(x => - x.ParentId != null && ids.Contains( - x.CalorimetricDataId - ?? x.GeometricDataId - ?? x.HygrothermalDataId - ?? x.LifeCycleDataId - ?? x.OpticalDataId - ?? x.PhotovoltaicDataId - ?? Guid.Empty - ) - ), - x => x.DataId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/GetHttpsResourceTreeRootByDataIdDataLoader.cs b/backend/src/GraphQl/DataX/GetHttpsResourceTreeRootByDataIdDataLoader.cs deleted file mode 100644 index 07fe5492..00000000 --- a/backend/src/GraphQl/DataX/GetHttpsResourceTreeRootByDataIdDataLoader.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Linq; -using Database.Data; -using Database.GraphQl.Entities; -using GreenDonut; -using Microsoft.EntityFrameworkCore; - -namespace Database.GraphQl.DataX; - -public sealed class GetHttpsResourceTreeRootByDataIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids) => - dbContext.GetHttpsResources.AsNoTracking().Where(x => - x.ParentId == null && ids.Contains( - x.CalorimetricDataId - ?? x.GeometricDataId - ?? x.HygrothermalDataId - ?? x.LifeCycleDataId - ?? x.OpticalDataId - ?? x.PhotovoltaicDataId - ?? Guid.Empty - ) - ), - x => x.DataId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/GetHttpsResourcesByDataIdDataLoader.cs b/backend/src/GraphQl/DataX/GetHttpsResourcesByDataIdDataLoader.cs deleted file mode 100644 index 0882dc0c..00000000 --- a/backend/src/GraphQl/DataX/GetHttpsResourcesByDataIdDataLoader.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Linq; -using Database.Data; -using Database.GraphQl.Entities; -using GreenDonut; -using Microsoft.EntityFrameworkCore; - -namespace Database.GraphQl.DataX; - -public sealed class GetHttpsResourcesByDataIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids) => - dbContext.GetHttpsResources.AsNoTracking().Where(x => - ids.Contains( - x.CalorimetricDataId - ?? x.GeometricDataId - ?? x.HygrothermalDataId - ?? x.LifeCycleDataId - ?? x.OpticalDataId - ?? x.PhotovoltaicDataId - ?? Guid.Empty - ) - ), - x => x.DataId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/UpdateDataMutation.cs b/backend/src/GraphQl/DataX/UpdateDataMutation.cs index f9f5ae1b..7fc0321c 100644 --- a/backend/src/GraphQl/DataX/UpdateDataMutation.cs +++ b/backend/src/GraphQl/DataX/UpdateDataMutation.cs @@ -5,12 +5,12 @@ using System.Threading.Tasks; using Database.Authorization; using Database.Data; +using Database.GraphQl.Scalars; using Database.Enumerations; using Database.Extensions; using Database.Services; using HotChocolate; using HotChocolate.Types; -using NodaTime; namespace Database.GraphQl.DataX; @@ -22,7 +22,7 @@ public sealed record UpdateDataInput( string? Name, string? Description, string[] Warnings, - OffsetDateTime CreatedAt, + DateTimeOffset CreatedAt, Guid CreatorId ) : IIdentifyDataInput; diff --git a/backend/src/GraphQl/Entities/AssociationsByAssociateIdDataLoader.cs b/backend/src/GraphQl/Entities/AssociationsByAssociateIdDataLoader.cs deleted file mode 100644 index 5efb927d..00000000 --- a/backend/src/GraphQl/Entities/AssociationsByAssociateIdDataLoader.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Database.Data; -using GreenDonut; -using Microsoft.EntityFrameworkCore; - -namespace Database.GraphQl.Entities; - -public abstract class AssociationsByAssociateIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory, - Func, IQueryable> getAssociations, - Func getAssociateId - ) - : GroupedDataLoader(batchScheduler, options) -{ - private readonly IDbContextFactory _dbContextFactory = dbContextFactory; - private readonly Func _getAssociateId = getAssociateId; - private readonly Func, IQueryable> _getAssociations = getAssociations; - - protected override async Task> LoadGroupedBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken - ) - { - await using var dbContext = - _dbContextFactory.CreateDbContext(); - return ( - await _getAssociations(dbContext, keys) - .ToListAsync(cancellationToken) - ) - .ToLookup(_getAssociateId); - } -} \ No newline at end of file diff --git a/backend/src/GraphQl/Entities/EntityFilterType.cs b/backend/src/GraphQl/Entities/AuditableEntityFilterType.cs similarity index 90% rename from backend/src/GraphQl/Entities/EntityFilterType.cs rename to backend/src/GraphQl/Entities/AuditableEntityFilterType.cs index c3cc4a9e..8fe8dfca 100644 --- a/backend/src/GraphQl/Entities/EntityFilterType.cs +++ b/backend/src/GraphQl/Entities/AuditableEntityFilterType.cs @@ -1,16 +1,11 @@ -using System; -using System.Linq; -using Database.Data; -using HotChocolate.Configuration; using HotChocolate.Data.Filters; -using HotChocolate.Types; -using HotChocolate.Types.Descriptors.Definitions; +using Database.Data; namespace Database.GraphQl.Entities; -public abstract class EntityFilterType +public abstract class AuditableEntityFilterType : FilterInputType - where TEntity : IEntity + where TEntity : IEntity, IAuditable { protected override void Configure( IFilterInputTypeDescriptor descriptor @@ -18,6 +13,8 @@ IFilterInputTypeDescriptor descriptor { descriptor.BindFieldsExplicitly(); descriptor.Field(x => x.Id); + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); // TODO Do we want to filter by: descriptor.Field(x => x.Version); } diff --git a/backend/src/GraphQl/Entities/EntitySortType.cs b/backend/src/GraphQl/Entities/AuditableEntitySortType.cs similarity index 58% rename from backend/src/GraphQl/Entities/EntitySortType.cs rename to backend/src/GraphQl/Entities/AuditableEntitySortType.cs index f3bbdb68..88e613bd 100644 --- a/backend/src/GraphQl/Entities/EntitySortType.cs +++ b/backend/src/GraphQl/Entities/AuditableEntitySortType.cs @@ -1,18 +1,20 @@ -using Database.Data; using HotChocolate.Data.Sorting; +using Database.Data; namespace Database.GraphQl.Entities; -public abstract class EntitySortType +public abstract class AuditableEntitySortType : SortInputType - where TEntity : IEntity + where TEntity : IEntity, IAuditable { protected override void Configure( ISortInputTypeDescriptor descriptor ) { + base.Configure(descriptor); descriptor.BindFieldsExplicitly(); descriptor.Field(x => x.Id); - // descriptor.Field(x => x.Version); + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); } } \ No newline at end of file diff --git a/backend/src/GraphQl/Entities/EntityByIdDataLoader.cs b/backend/src/GraphQl/Entities/EntityByIdDataLoader.cs deleted file mode 100644 index a79193fa..00000000 --- a/backend/src/GraphQl/Entities/EntityByIdDataLoader.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Database.Data; -using GreenDonut; -using Microsoft.EntityFrameworkCore; - -namespace Database.GraphQl.Entities; - -public abstract class EntityByIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory, - Func> getQueryable - ) - : BatchDataLoader(batchScheduler, options) - where TEntity : class, IEntity -{ - private readonly IDbContextFactory _dbContextFactory = dbContextFactory; - private readonly Func> _getQueryable = getQueryable; - - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken - ) - { - await using var dbContext = - _dbContextFactory.CreateDbContext(); - return await _getQueryable(dbContext).AsNoTrackingWithIdentityResolution() - .Where(entity => keys.Contains(entity.Id)) - .ToDictionaryAsync( - entity => entity.Id, - entity => (TEntity?)entity, - cancellationToken - ); - } -} \ No newline at end of file diff --git a/backend/src/GraphQl/Entities/EntityType.cs b/backend/src/GraphQl/Entities/EntityType.cs index 6e1402c1..6dfd7111 100644 --- a/backend/src/GraphQl/Entities/EntityType.cs +++ b/backend/src/GraphQl/Entities/EntityType.cs @@ -1,19 +1,21 @@ using System; -using Database.Data; using GreenDonut; using HotChocolate.Types; +using Database.Data; +using Database.GraphQl.Scalars; -namespace Database.GraphQl; +namespace Database.GraphQl.Entities; public abstract class EntityType : ObjectType where TEntity : IEntity - where TEntityByIdDataLoader : IDataLoader + where TEntityByIdDataLoader : IDataLoader { protected override void Configure( IObjectTypeDescriptor descriptor ) { + base.Configure(descriptor); descriptor .ImplementsNode() .IdField(t => t.Id) @@ -21,11 +23,11 @@ IObjectTypeDescriptor descriptor context .DataLoader() .LoadAsync(id, context.RequestAborted) - ! // Notice the null-forgiving operator `!`. It's bad that we need to use it here. ); descriptor .Field(GraphQlConstants.UuidFieldName) .Type>() + .Cost(0) .Resolve(context => context.Parent().Id ); diff --git a/backend/src/GraphQl/ErrorLoggingDiagnosticEventListener.cs b/backend/src/GraphQl/ErrorLoggingDiagnosticEventListener.cs index b91859a6..d574dfd3 100644 --- a/backend/src/GraphQl/ErrorLoggingDiagnosticEventListener.cs +++ b/backend/src/GraphQl/ErrorLoggingDiagnosticEventListener.cs @@ -1,15 +1,13 @@ using System; using System.Collections.Generic; -using System.Globalization; -using System.Text; using System.Text.RegularExpressions; using Database.Logging; using HotChocolate; using HotChocolate.Execution; using HotChocolate.Execution.Instrumentation; -using HotChocolate.Execution.Processing; using HotChocolate.Resolvers; using Microsoft.Extensions.Logging; +using HotChocolate.Language; namespace Database.GraphQl; @@ -27,45 +25,47 @@ public static partial void RequestError( [LoggerMessage( Level = LogLevel.Error, - Message = "Resolver error. Field: '{FieldName}'. Operation: '{Operation}'." + Message = "Request error. Document: {Document}" )] - public static partial void ResolverError( + public static partial void RequestError( this ILogger logger, - Exception? exception, - string fieldName, - IOperation operation, + IOperationDocument? document, [TagProvider(typeof(HotChocolateIErrorTagProvider), nameof(HotChocolateIErrorTagProvider.RecordTags))] IError error ); [LoggerMessage( Level = LogLevel.Error, - Message = "Subscription event error. Operation: {Operation}" + Message = "Resolver error. Field: '{FieldName}'. Document: {Document}" )] - public static partial void SubscriptionEventError( + public static partial void ResolverError( this ILogger logger, - Exception exception, - IOperation operation + string fieldName, + DocumentNode document, + [TagProvider(typeof(HotChocolateIErrorTagProvider), nameof(HotChocolateIErrorTagProvider.RecordTags))] IError error, + Exception? exception ); [LoggerMessage( Level = LogLevel.Error, - Message = "Subscription event error. Operation: {Operation}" + Message = "Resolver error. Field: '{FieldName}'. Document: {Document}" )] - public static partial void SubscriptionTransportError( + public static partial void ResolverError( this ILogger logger, - Exception exception, - IOperation operation + Exception? exception, + string fieldName, + IOperationDocument? document, + [TagProvider(typeof(HotChocolateIErrorTagProvider), nameof(HotChocolateIErrorTagProvider.RecordTags))] IError error ); [LoggerMessage( Level = LogLevel.Error, - Message = "Syntax error. Document: {Document}" + Message = "Subscription event error. Id: '{SubscriptionId}'. Operation: {Document}" )] - public static partial void SyntaxError( + public static partial void SubscriptionEventError( this ILogger logger, - Exception? exception, - IOperationDocument? document, - [TagProvider(typeof(HotChocolateIErrorTagProvider), nameof(HotChocolateIErrorTagProvider.RecordTags))] IError error + Exception exception, + ulong subscriptionId, + IOperationDocument? document ); [LoggerMessage( @@ -128,55 +128,57 @@ ILogger logger : ExecutionDiagnosticEventListener { // this diagnostic event is raised when a request is executed ... - public override IDisposable ExecuteRequest(IRequestContext context) + public override IDisposable ExecuteRequest(RequestContext context) { // ... we will return an activity scope that is used to signal when the request is finished. return new RequestScope(logger, context); } public override void RequestError( - IRequestContext context, - Exception exception + RequestContext context, + Exception error ) { - logger.RequestError(exception, context.Request.Document); - base.RequestError(context, exception); + logger.RequestError(error, context.Request.Document); + base.RequestError(context, error); } - public override void ResolverError( - IMiddlewareContext context, + public override void RequestError( + RequestContext context, IError error ) { - logger.ResolverError(error.Exception, context.Selection.Field.Name, context.Operation, error); - base.ResolverError(context, error); + logger.RequestError(context.Request.Document, error); + base.RequestError(context, error); } - public override void SubscriptionEventError( - SubscriptionEventContext context, - Exception exception + public override void ResolverError( + IMiddlewareContext context, + IError error ) { - logger.SubscriptionEventError(exception, context.Subscription.Operation); - base.SubscriptionEventError(context, exception); + logger.ResolverError(context.Selection.Field.Name, context.Operation.Document, error, error.Exception); + base.ResolverError(context, error); } - public override void SubscriptionTransportError( - ISubscription subscription, - Exception exception + public override void ResolverError( + RequestContext context, + ISelection selection, + IError error ) { - logger.SubscriptionTransportError(exception, subscription.Operation); - base.SubscriptionTransportError(subscription, exception); + logger.ResolverError(error.Exception, selection.Field.Name, context.Request?.Document, error); + base.ResolverError(context, selection, error); } - public override void SyntaxError( - IRequestContext context, - IError error + public override void SubscriptionEventError( + RequestContext context, + ulong subscriptionId, + Exception exception ) { - logger.SyntaxError(error.Exception, context.Request.Document, error); - base.SyntaxError(context, error); + logger.SubscriptionEventError(exception, subscriptionId, context.Request.Document); + base.SubscriptionEventError(context, subscriptionId, exception); } public override void TaskError( @@ -189,7 +191,7 @@ IError error } public override void ValidationErrors( - IRequestContext context, + RequestContext context, IReadOnlyList errors ) { @@ -200,7 +202,7 @@ IReadOnlyList errors base.ValidationErrors(context, errors); } - private sealed partial class RequestScope(ILogger logger, IRequestContext context) : IDisposable + private sealed partial class RequestScope(ILogger logger, RequestContext context) : IDisposable { [GeneratedRegex(@"apiKey|authKey|privateKey|password|passphrase|secret|secure|security|token", RegexOptions.IgnoreCase, "")] private static partial Regex SecretRegex(); @@ -209,43 +211,45 @@ private sealed partial class RequestScope(ILogger" - : variableValue.Value.ToString() - ); - stringBuilder.Append('\''); - stringBuilder.AppendFormat(CultureInfo.InvariantCulture, $"{Environment.NewLine}"); - } - catch (Exception exception) - { - // all input type records will land here. - stringBuilder.AppendFormat(CultureInfo.InvariantCulture, $"Failed stringifying the value: {exception.Message}"); - stringBuilder.AppendFormat(CultureInfo.InvariantCulture, $"{Environment.NewLine}"); - } - } - } - _variables = stringBuilder.ToString(); + // TODO Where are the variables now if not anymore in context.Variables? + // if (_variables is not null) + // { + // return _variables; + // } + // if (context.Variables is null) + // { + // return null; + // } + // StringBuilder stringBuilder = new(); + // foreach (var variableValueCollection in context.Variables) + // { + // foreach (var variableValue in variableValueCollection) + // { + // try + // { + // stringBuilder.AppendFormat( + // CultureInfo.InvariantCulture, + // $"{variableValue.Name} : {variableValue.Type} = " + // ); + // stringBuilder.Append('\''); + // stringBuilder.Append( + // SecretRegex().IsMatch(variableValue.Name) + // ? "" + // : variableValue.Value.ToString() + // ); + // stringBuilder.Append('\''); + // stringBuilder.AppendFormat(CultureInfo.InvariantCulture, $"{Environment.NewLine}"); + // } + // catch (Exception exception) + // { + // // all input type records will land here. + // stringBuilder.AppendFormat(CultureInfo.InvariantCulture, $"Failed stringifying the value: {exception.Message}"); + // stringBuilder.AppendFormat(CultureInfo.InvariantCulture, $"{Environment.NewLine}"); + // } + // } + // } + // _variables = stringBuilder.ToString(); + _variables = null; return _variables; } @@ -253,28 +257,29 @@ public void Dispose() { if (logger.IsEnabled(LogLevel.Debug)) { - if (context.Document is not null) + if (context.OperationDocumentInfo.Document is not null) { #pragma warning disable CA1873 // Evaluation of this argument may be expensive and unnecessary if logging is disabled (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1873) logger.Executed( - context.Document.ToString(true), + context.OperationDocumentInfo.Document.ToString(true), StringifyVariables() ); #pragma warning restore CA1873 } } // when the request is finished it will dispose the activity scope - if (context.Result is IOperationResult { Errors.Count: > 0 } operationResult) + if (context.Result is OperationResult { Errors.Count: > 0 } operationResult) { foreach (var error in operationResult.Errors) { logger.OperationError(context.Request.Document, StringifyVariables(), error); } } - if (context.Exception is { }) - { - logger.UnexpectedExecutionException(context.Request.Document, StringifyVariables(), context.Exception); - } + // TODO Where is the exception now? + // if (context.Exception is { }) + // { + // logger.UnexpectedExecutionException(context.Request.Document, StringifyVariables(), context.Exception); + // } } } } \ No newline at end of file diff --git a/backend/src/GraphQl/Extensions/PageExtensions.cs b/backend/src/GraphQl/Extensions/PageExtensions.cs new file mode 100644 index 00000000..cdbe08dc --- /dev/null +++ b/backend/src/GraphQl/Extensions/PageExtensions.cs @@ -0,0 +1,66 @@ +using System.Threading.Tasks; +using GreenDonut.Data; +using HotChocolate.Types.Pagination; + +namespace Database.GraphQl.Extensions; + +// Inspired by https://github.com/ChilliCream/graphql-platform/blob/main/src/HotChocolate/Data/src/Data/Extensions/HotChocolatePaginationResultExtensions.cs +public static class PageExtensions +{ + public static async ValueTask GetTotalCountAsync( + this Task?> pagePromise + ) + { + return (await pagePromise)?.TotalCount ?? 0; + } + + public static async ValueTask GetPageInfoAsync( + this Task?> pagePromise + ) + { + var page = await pagePromise; + return new ConnectionPageInfo( + page?.HasNextPage ?? false, + page?.HasPreviousPage ?? false, + page?.CreateStartCursor(), + page?.CreateEndCursor() + ); + } + + // public static async Task> ToConnectionAsync( + // this Task> pagePromise, + // Func, PageEntry, Task>> createEdgeAsync, + // Func>, ConnectionPageInfo, int, Connection> createConnection + // ) + // where TTarget : class + // where TSource : class + // { + // return await CreateConnectionAsync(await pagePromise, createEdgeAsync, createConnection); + // } + + // private static async Task> CreateConnectionAsync( + // Page? page, + // Func, PageEntry, Task>> createEdgeAsync, + // Func>, ConnectionPageInfo, int, Connection> createConnection + // ) + // where TTarget : class + // { + // page ??= Page.Empty; + // var entries = page.Entries; + // IEdge[] edges = entries.IsEmpty + // ? [] + // : await Task.WhenAll( + // entries + // .Select(entry => createEdgeAsync(page, entry)) + // .ToList() + // ); + // return createConnection( + // edges, + // new ConnectionPageInfo( + // page.HasNextPage, + // page.HasPreviousPage, + // page.CreateStartCursor(), + // page.CreateEndCursor()), + // page.TotalCount ?? 0); + // } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Extensions/ResolverContextExtensions.cs b/backend/src/GraphQl/Extensions/ResolverContextExtensions.cs index 9c677115..e2a1a926 100644 --- a/backend/src/GraphQl/Extensions/ResolverContextExtensions.cs +++ b/backend/src/GraphQl/Extensions/ResolverContextExtensions.cs @@ -8,16 +8,28 @@ namespace Database.GraphQl.Extensions; public static class ResolverContextExtensions { // Inspired by https://github.com/ChilliCream/graphql-platform/blob/9ae7220205412203d0a941a6b0cc779e70b02b09/src/HotChocolate/Data/src/Data/QueryContextParameterExpressionBuilder.cs#L76-L86 + // Using `QueryContext queryContext,` in resolvers starts up the projection engine producing many problems public static QueryContext GetQueryContext(this IResolverContext context) { var selection = context.Selection; var filterContext = context.GetFilterContext(); var sortContext = context.GetSortingContext(); - // TODO Make selection work return new QueryContext( null, // selection.AsSelector(), filterContext?.AsPredicate(), sortContext?.AsSortDefinition()); } + + // Using `PagingArguments pagingArguments,` in resolvers results in the parameter `pagingArguments: PagingArgumentsInput` in the GraphQL schema + public static PagingArguments GetPagingArguments(this IResolverContext context) + { + return new( + context.ArgumentValue("first"), + context.ArgumentValue("after"), + context.ArgumentValue("last"), + context.ArgumentValue("before"), + includeTotalCount: true + ); + } } \ No newline at end of file diff --git a/backend/src/GraphQl/Extensions/SortingContextExtensions.cs b/backend/src/GraphQl/Extensions/SortingContextExtensions.cs deleted file mode 100644 index 65b43071..00000000 --- a/backend/src/GraphQl/Extensions/SortingContextExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Linq; -using Database.Data; -using GreenDonut.Data; -using HotChocolate.Data.Sorting; - -namespace Database.GraphQl.Extensions; - -public static class SortingContextExtensions -{ - public static void StabilizeOrder(this ISortingContext sorting) where T : IEntity - { - // this signals that the expression was not handled within the resolver - // and the sorting middleware should take over. - sorting.Handled(false); - sorting.OnAfterSortingApplied>( - static (sortingApplied, query) => - { - if (sortingApplied && query is IOrderedQueryable ordered) - { - return ordered.ThenBy(_ => _.Id); - } - return query.OrderBy(_ => _.Id); - }); - } - - public static SortDefinition StabilizeOrder(this SortDefinition sort) where T : IEntity - { - return sort.AddAscending(_ => _.Id); - } -} \ No newline at end of file diff --git a/backend/src/GraphQl/Filters/INotField.cs b/backend/src/GraphQl/Filters/INotField.cs index 3e2b98ac..9b24e3f3 100644 --- a/backend/src/GraphQl/Filters/INotField.cs +++ b/backend/src/GraphQl/Filters/INotField.cs @@ -1,11 +1,10 @@ -using HotChocolate.Data.Filters; -using HotChocolate.Types; - -namespace Database.GraphQl.Filters; - -public interface INotField - : IInputField - , IHasRuntimeType -{ - new IFilterInputType DeclaringType { get; } -} \ No newline at end of file +// using HotChocolate.Data.Filters; +// +// namespace Database.GraphQl.Filters; +// +// public interface INotField +// : IInputField +// , IHasRuntimeType +// { +// new IFilterInputType DeclaringType { get; } +// } \ No newline at end of file diff --git a/backend/src/GraphQl/Filters/NotField.cs b/backend/src/GraphQl/Filters/NotField.cs index 05d14b3d..d7234d79 100644 --- a/backend/src/GraphQl/Filters/NotField.cs +++ b/backend/src/GraphQl/Filters/NotField.cs @@ -1,44 +1,43 @@ -using HotChocolate.Configuration; -using HotChocolate.Data.Filters; -using HotChocolate.Types; -using HotChocolate.Types.Descriptors; -using HotChocolate.Types.Descriptors.Definitions; - -namespace Database.GraphQl.Filters; - -public sealed class NotField - // : FilterOperationField - : InputField - , INotField -{ - internal NotField(IDescriptorContext context, int index, string? scope) - : base(CreateDefinition(context, scope), index) - { - } - - public new FilterInputType DeclaringType => (FilterInputType)base.DeclaringType; - - IFilterInputType INotField.DeclaringType => DeclaringType; - - protected override void OnCompleteField( - ITypeCompletionContext context, - ITypeSystemMember declaringMember, - InputFieldDefinition definition) - { - definition.Type = TypeReference.Parse( - $"[{context.Type.Name}!]", - TypeContext.Input, - context.Type.Scope); - - base.OnCompleteField(context, declaringMember, definition); - } - - private static FilterOperationFieldDefinition CreateDefinition( - IDescriptorContext context, - string? scope) - { - return FilterOperationFieldDescriptor - .New(context, AdditionalFilterOperations.Not, scope) - .CreateDefinition(); - } -} \ No newline at end of file +// using HotChocolate.Configuration; +// using HotChocolate.Data.Filters; +// using HotChocolate.Types; +// using HotChocolate.Types.Descriptors; +// +// namespace Database.GraphQl.Filters; +// +// public sealed class NotField +// // : FilterOperationField +// : InputField +// , INotField +// { +// internal NotField(IDescriptorContext context, int index, string? scope) +// : base(CreateDefinition(context, scope), index) +// { +// } +// +// public new FilterInputType DeclaringType => (FilterInputType)base.DeclaringType; +// +// IFilterInputType INotField.DeclaringType => DeclaringType; +// +// protected override void OnCompleteField( +// ITypeCompletionContext context, +// ITypeSystemMember declaringMember, +// InputFieldDefinition definition) +// { +// definition.Type = TypeReference.Parse( +// $"[{context.Type.Name}!]", +// TypeContext.Input, +// context.Type.Scope); +// +// base.OnCompleteField(context, declaringMember, definition); +// } +// +// private static FilterOperationFieldDefinition CreateDefinition( +// IDescriptorContext context, +// string? scope) +// { +// return FilterOperationFieldDescriptor +// .New(context, AdditionalFilterOperations.Not, scope) +// .CreateDefinition(); +// } +// } \ No newline at end of file diff --git a/backend/src/GraphQl/Filters/ScalarFilterInputTypes.cs b/backend/src/GraphQl/Filters/ScalarFilterInputTypes.cs index a43f2f96..c14e0b46 100644 --- a/backend/src/GraphQl/Filters/ScalarFilterInputTypes.cs +++ b/backend/src/GraphQl/Filters/ScalarFilterInputTypes.cs @@ -1,8 +1,16 @@ using HotChocolate.Data.Filters; using HotChocolate.Types; +using Database.GraphQl.Scalars; +using DateTimeType = HotChocolate.Types.NodaTime.DateTimeType; +using DurationType = HotChocolate.Types.NodaTime.DurationType; +using LocalDateTimeType = HotChocolate.Types.NodaTime.LocalDateTimeType; +using LocalDateType = HotChocolate.Types.NodaTime.LocalDateType; +using LocalTimeType = HotChocolate.Types.NodaTime.LocalTimeType; namespace Database.GraphQl.Filters; +// https://github.com/ChilliCream/graphql-platform/blob/main/src/HotChocolate/Data/src/Data/Filters/Types/ComparableOperationFilterInputType.cs + public abstract class ExtendedComparableOperationFilterInputType : ComparableOperationFilterInputType where T : notnull @@ -76,15 +84,25 @@ protected override void Configure(IFilterInputTypeDescriptor descriptor) } } -// public sealed class LocalDateFilterInputType -// : ExtendedComparableOperationFilterInputType -// { -// protected override void Configure(IFilterInputTypeDescriptor descriptor) -// { -// descriptor.Name($"LocalDate{GraphQlConstants.FilterInputSuffix}"); -// base.Configure(descriptor); -// } -// } +public sealed class LocalDateFilterInputType + : ExtendedComparableOperationFilterInputType +{ + protected override void Configure(IFilterInputTypeDescriptor descriptor) + { + descriptor.Name($"LocalDate{GraphQlConstants.FilterInputSuffix}"); + base.Configure(descriptor); + } +} + +public sealed class LocalDateTimeFilterInputType + : ExtendedComparableOperationFilterInputType +{ + protected override void Configure(IFilterInputTypeDescriptor descriptor) + { + descriptor.Name($"LocalDateTime{GraphQlConstants.FilterInputSuffix}"); + base.Configure(descriptor); + } +} public sealed class LongFilterInputType : ExtendedComparableOperationFilterInputType @@ -96,15 +114,15 @@ protected override void Configure(IFilterInputTypeDescriptor descriptor) } } -// public sealed class LocalTimeFilterInputType -// : ExtendedComparableOperationFilterInputType -// { -// protected override void Configure(IFilterInputTypeDescriptor descriptor) -// { -// descriptor.Name($"LocalTime{GraphQlConstants.FilterInputSuffix}"); -// base.Configure(descriptor); -// } -// } +public sealed class LocalTimeFilterInputType + : ExtendedComparableOperationFilterInputType +{ + protected override void Configure(IFilterInputTypeDescriptor descriptor) + { + descriptor.Name($"LocalTime{GraphQlConstants.FilterInputSuffix}"); + base.Configure(descriptor); + } +} public sealed class FloatFilterInputType : ExtendedComparableOperationFilterInputType @@ -116,12 +134,12 @@ protected override void Configure(IFilterInputTypeDescriptor descriptor) } } -public sealed class TimeSpanFilterInputType - : ExtendedComparableOperationFilterInputType +public sealed class DurationFilterInputType + : ExtendedComparableOperationFilterInputType { protected override void Configure(IFilterInputTypeDescriptor descriptor) { - descriptor.Name($"TimeSpan{GraphQlConstants.FilterInputSuffix}"); + descriptor.Name($"Duration{GraphQlConstants.FilterInputSuffix}"); base.Configure(descriptor); } } @@ -146,8 +164,8 @@ protected override void Configure(IFilterInputTypeDescriptor descriptor) } } -public sealed class UrlFilterInputType - : ExtendedComparableOperationFilterInputType +public sealed class UriFilterInputType + : ExtendedComparableOperationFilterInputType { protected override void Configure(IFilterInputTypeDescriptor descriptor) { diff --git a/backend/src/GraphQl/GeometricDataX/CreateGeometricDataMutation.cs b/backend/src/GraphQl/GeometricDataX/CreateGeometricDataMutation.cs index b11f44cc..52739ea5 100644 --- a/backend/src/GraphQl/GeometricDataX/CreateGeometricDataMutation.cs +++ b/backend/src/GraphQl/GeometricDataX/CreateGeometricDataMutation.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Database.ApiRequests; @@ -9,8 +8,8 @@ using Database.Data; using Database.Extensions; using Database.GraphQl.DataX; +using Database.GraphQl.Scalars; using Database.Services; -using Database.Utilities; using HotChocolate; using HotChocolate.Types; using NodaTime; @@ -23,7 +22,7 @@ public sealed record CreateGeometricDataInput( string? Name, string? Description, string[] Warnings, - OffsetDateTime CreatedAt, + DateTimeOffset CreatedAt, Guid CreatorId, AppliedMethodInput AppliedMethod, RootGetHttpsResourceInput RootResource, @@ -111,6 +110,7 @@ public async Task CreateGeometricDataAsync( IDataByDatabaseAndIdAndKindDataLoader dataByDatabaseAndIdAndKindDataLoader, IDataFormatByIdDataLoader dataFormatByIdDataLoader, ResponseApprovalService responseApprovalService, + IClock clock, CancellationToken cancellationToken ) { @@ -140,6 +140,7 @@ CancellationToken cancellationToken CreateGeometricDataErrorCode.UNKNOWN_DATA, dataFormatByIdDataLoader, CreateGeometricDataErrorCode.UNKNOWN_DATA_FORMAT, + clock, cancellationToken ) ).Failed(out var dataFormat, out var validateErrorPayload) @@ -172,4 +173,4 @@ CancellationToken cancellationToken return NewPayload(geometricData, null); } -} +} \ No newline at end of file diff --git a/backend/src/GraphQl/GeometricDataX/GeometricDataByIdDataLoader.cs b/backend/src/GraphQl/GeometricDataX/GeometricDataByIdDataLoader.cs deleted file mode 100644 index ef2c94e3..00000000 --- a/backend/src/GraphQl/GeometricDataX/GeometricDataByIdDataLoader.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Database.Data; -using Database.GraphQl.Entities; -using GreenDonut; -using Microsoft.EntityFrameworkCore; - -namespace Database.GraphQl.GeometricDataX; - -public sealed class GeometricDataByIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : EntityByIdDataLoader( - batchScheduler, - options, - dbContextFactory, - dbContext => dbContext.GeometricData - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/GeometricDataX/GeometricDataLoaders.cs b/backend/src/GraphQl/GeometricDataX/GeometricDataLoaders.cs new file mode 100644 index 00000000..703cf473 --- /dev/null +++ b/backend/src/GraphQl/GeometricDataX/GeometricDataLoaders.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut; +using GreenDonut.Data; +using Database.Data; +using Microsoft.EntityFrameworkCore; + +namespace Database.GraphQl.GeometricDataX; + +public sealed class GeometricDataLoaders +: DataLoaders +{ + [DataLoader] + public static ValueTask> GetGeometricDataByIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetEntityByIdAsync( + ids, + databaseContext => databaseContext.GeometricData, + queryContext, + databaseContextFactory, + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/GeometricDataX/GeometricDataQueries.cs b/backend/src/GraphQl/GeometricDataX/GeometricDataQueries.cs index 2520920a..ba3fcfbe 100644 --- a/backend/src/GraphQl/GeometricDataX/GeometricDataQueries.cs +++ b/backend/src/GraphQl/GeometricDataX/GeometricDataQueries.cs @@ -1,14 +1,13 @@ using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Database.Authorization; using Database.Data; using Database.GraphQl.DataX; +using Database.GraphQl.Scalars; using Database.Services; using HotChocolate; using HotChocolate.Data; -using HotChocolate.Data.Sorting; using HotChocolate.Resolvers; using HotChocolate.Types; @@ -21,11 +20,10 @@ public sealed class GeometricDataQueries [UsePaging] [UseFiltering] [UseSorting] - public Task> GetAllGeometricDataAsync( + public Task> GetAllGeometricDataAsync( [GraphQLType] string? locale, ApplicationDbContext context, AccessRightsService accessRightsService, - ISortingContext sorting, IResolverContext resolverContext, CancellationToken cancellationToken ) @@ -34,7 +32,6 @@ CancellationToken cancellationToken context.GeometricData, locale, accessRightsService, - sorting, resolverContext, cancellationToken ); @@ -43,11 +40,10 @@ CancellationToken cancellationToken [UsePaging] [UseFiltering] [UseSorting] - public Task> GetAllPendingGeometricDataAsync( + public Task> GetAllPendingGeometricDataAsync( [GraphQLType] string? locale, ApplicationDbContext context, AccessRightsService accessRightsService, - ISortingContext sorting, IResolverContext resolverContext, CommonAuthorization authorization, CancellationToken cancellationToken @@ -57,7 +53,6 @@ CancellationToken cancellationToken context.GeometricData, locale, accessRightsService, - sorting, resolverContext, authorization, cancellationToken diff --git a/backend/src/GraphQl/GeometricDataX/GeometricDataType.cs b/backend/src/GraphQl/GeometricDataX/GeometricDataType.cs index 978f7481..c1caea06 100644 --- a/backend/src/GraphQl/GeometricDataX/GeometricDataType.cs +++ b/backend/src/GraphQl/GeometricDataX/GeometricDataType.cs @@ -5,7 +5,7 @@ namespace Database.GraphQl.GeometricDataX; public sealed class GeometricDataType - : DataTypeBase + : DataTypeBase { protected override void Configure( IObjectTypeDescriptor descriptor diff --git a/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceByIdDataLoader.cs b/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceByIdDataLoader.cs deleted file mode 100644 index f937d1f3..00000000 --- a/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceByIdDataLoader.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Database.Data; -using Database.GraphQl.Entities; -using GreenDonut; -using Microsoft.EntityFrameworkCore; - -namespace Database.GraphQl.GetHttpsResources; - -public sealed class GetHttpsResourceByIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : EntityByIdDataLoader( - batchScheduler, - options, - dbContextFactory, - dbContext => dbContext.GetHttpsResources - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceChildrenByGetHttpsResourceIdDataLoader.cs b/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceChildrenByGetHttpsResourceIdDataLoader.cs deleted file mode 100644 index 276b992d..00000000 --- a/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceChildrenByGetHttpsResourceIdDataLoader.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Linq; -using Database.Data; -using Database.GraphQl.Entities; -using GreenDonut; -using Microsoft.EntityFrameworkCore; - -namespace Database.GraphQl.GetHttpsResources; - -public sealed class GetHttpsResourceChildrenByGetHttpsResourceIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids) => - dbContext.GetHttpsResources.AsNoTracking().Where(x => - ids.Contains(x.ParentId ?? Guid.Empty) - ), - x => x.Id - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceDataLoaders.cs b/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceDataLoaders.cs new file mode 100644 index 00000000..a04f9b0a --- /dev/null +++ b/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceDataLoaders.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut; +using GreenDonut.Data; +using Database.Data; +using Microsoft.EntityFrameworkCore; + +namespace Database.GraphQl.GetHttpsResources; + +public sealed class GetHttpsResourceDataLoaders +: DataLoaders +{ + [DataLoader] + public static ValueTask> GetGetHttpsResourceByIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetEntityByIdAsync( + ids, + databaseContext => databaseContext.GetHttpsResources, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask> GetHttpsResourceChildrenByGetHttpsResourceIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetManyByOneIdAsync( + ids, + (databaseContext) => databaseContext.GetHttpsResources, + _ => _.ParentId ?? Guid.Empty, + queryContext, + databaseContextFactory, + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceFilterType.cs b/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceFilterType.cs index 6e1a5f92..312f34d0 100644 --- a/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceFilterType.cs +++ b/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceFilterType.cs @@ -5,7 +5,7 @@ namespace Database.GraphQl.GetHttpsResources; public class GetHttpsResourceFilterType - : EntityFilterType + : AuditableEntityFilterType { protected override void Configure( IFilterInputTypeDescriptor descriptor diff --git a/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceQueries.cs b/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceQueries.cs index 95850511..8e2df359 100644 --- a/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceQueries.cs +++ b/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceQueries.cs @@ -6,8 +6,9 @@ using Database.Data; using Database.Enumerations; using Database.GraphQl.Extensions; +using GreenDonut.Data; using HotChocolate.Data; -using HotChocolate.Data.Sorting; +using HotChocolate.Resolvers; using HotChocolate.Types; using Microsoft.EntityFrameworkCore; @@ -17,58 +18,61 @@ namespace Database.GraphQl.GetHttpsResources; public sealed class GetHttpsResourceQueries { [UsePaging] - // [UseProjection] // We disabled projections because when requesting `id` all results had the same `id` and when also requesting `uuid`, the latter was always the empty UUID `000...`. [UseFiltering] [UseSorting] - public IQueryable GetGetHttpsResources( + public ValueTask> GetGetHttpsResources( ApplicationDbContext context, - ISortingContext sorting + IResolverContext resolverContext, + CancellationToken cancellationToken ) { - sorting.StabilizeOrder(); return context.GetHttpsResources.AsNoTracking() .Where(_ => _.CalorimetricData == null || _.CalorimetricData.PublishingState != PublishingState.PENDING) .Where(_ => _.GeometricData == null || _.GeometricData.PublishingState != PublishingState.PENDING) .Where(_ => _.HygrothermalData == null || _.HygrothermalData.PublishingState != PublishingState.PENDING) .Where(_ => _.LifeCycleData == null || _.LifeCycleData.PublishingState != PublishingState.PENDING) .Where(_ => _.OpticalData == null || _.OpticalData.PublishingState != PublishingState.PENDING) - .Where(_ => _.PhotovoltaicData == null || _.PhotovoltaicData.PublishingState != PublishingState.PENDING); + .Where(_ => _.PhotovoltaicData == null || _.PhotovoltaicData.PublishingState != PublishingState.PENDING) + .With(resolverContext.GetQueryContext(), Sorting.DefaultEntityOrder) + .ToPageAsync(resolverContext.GetPagingArguments(), cancellationToken) + .ToConnectionAsync(); } [UsePaging] // [UseProjection] // We disabled projections because when requesting `id` all results had the same `id` and when also requesting `uuid`, the latter was always the empty UUID `000...`. [UseFiltering] [UseSorting] - public async Task> GetPendingGetHttpsResources( + public async ValueTask> GetPendingGetHttpsResources( ApplicationDbContext context, - ISortingContext sorting, CommonAuthorization authorization, + IResolverContext resolverContext, CancellationToken cancellationToken ) { if (!await authorization.IsDatabaseOperator(cancellationToken)) { - return Enumerable.Empty().AsQueryable(); + return await Enumerable.Empty().AsQueryable() + .ToPageAsync(resolverContext.GetPagingArguments(), cancellationToken) + .ToConnectionAsync(); } - sorting.StabilizeOrder(); - return context.GetHttpsResources.AsNoTracking() + return await context.GetHttpsResources.AsNoTracking() .Where(_ => _.CalorimetricData == null || _.CalorimetricData.PublishingState == PublishingState.PENDING) .Where(_ => _.GeometricData == null || _.GeometricData.PublishingState == PublishingState.PENDING) .Where(_ => _.HygrothermalData == null || _.HygrothermalData.PublishingState == PublishingState.PENDING) .Where(_ => _.LifeCycleData == null || _.LifeCycleData.PublishingState == PublishingState.PENDING) .Where(_ => _.OpticalData == null || _.OpticalData.PublishingState == PublishingState.PENDING) - .Where(_ => _.PhotovoltaicData == null || _.PhotovoltaicData.PublishingState == PublishingState.PENDING); + .Where(_ => _.PhotovoltaicData == null || _.PhotovoltaicData.PublishingState == PublishingState.PENDING) + .With(resolverContext.GetQueryContext(), Sorting.DefaultEntityOrder) + .ToPageAsync(resolverContext.GetPagingArguments(), cancellationToken) + .ToConnectionAsync(); } public Task GetGetHttpsResourceAsync( Guid id, - GetHttpsResourceByIdDataLoader byId, + IGetHttpsResourceByIdDataLoader byId, CancellationToken cancellationToken ) { - return byId.LoadAsync( - id, - cancellationToken - ); + return byId.LoadAsync(id, cancellationToken); } } \ No newline at end of file diff --git a/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceResolvers.cs b/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceResolvers.cs index d6801662..aca20aa5 100644 --- a/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceResolvers.cs +++ b/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceResolvers.cs @@ -16,12 +16,12 @@ public sealed class GetHttpsResourceResolvers { public async Task GetData( [Parent] GetHttpsResource getHttpsResource, - CalorimetricDataByIdDataLoader calorimetricDataById, - HygrothermalDataByIdDataLoader hygrothermalDataById, - LifeCycleDataByIdDataLoader lifeCycleDataById, - OpticalDataByIdDataLoader opticalDataById, - PhotovoltaicDataByIdDataLoader photovoltaicDataById, - GeometricDataByIdDataLoader geometricDataById, + ICalorimetricDataByIdDataLoader calorimetricDataById, + IHygrothermalDataByIdDataLoader hygrothermalDataById, + ILifeCycleDataByIdDataLoader lifeCycleDataById, + IOpticalDataByIdDataLoader opticalDataById, + IPhotovoltaicDataByIdDataLoader photovoltaicDataById, + IGeometricDataByIdDataLoader geometricDataById, CancellationToken cancellationToken ) { @@ -83,7 +83,7 @@ AppSettings appSettings public async Task GetParent( [Parent] GetHttpsResource getHttpsResource, - GetHttpsResourceByIdDataLoader byId, + IGetHttpsResourceByIdDataLoader byId, CancellationToken cancellationToken ) { @@ -94,7 +94,7 @@ CancellationToken cancellationToken public Task GetChildren( [Parent] GetHttpsResource getHttpsResource, - GetHttpsResourceChildrenByGetHttpsResourceIdDataLoader byId, + IHttpsResourceChildrenByGetHttpsResourceIdDataLoader byId, CancellationToken cancellationToken ) { diff --git a/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceSortType.cs b/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceSortType.cs index c41a6e9f..9504cc60 100644 --- a/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceSortType.cs +++ b/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceSortType.cs @@ -5,7 +5,7 @@ namespace Database.GraphQl.GetHttpsResources; public class GetHttpsResourceSortType - : EntitySortType + : AuditableEntitySortType { protected override void Configure( ISortInputTypeDescriptor descriptor diff --git a/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceType.cs b/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceType.cs index f93cac99..01bc6cab 100644 --- a/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceType.cs +++ b/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceType.cs @@ -1,10 +1,11 @@ using Database.Data; +using Database.GraphQl.Entities; using HotChocolate.Types; namespace Database.GraphQl.GetHttpsResources; public sealed class GetHttpsResourceType - : EntityType + : EntityType { protected override void Configure( IObjectTypeDescriptor descriptor diff --git a/backend/src/GraphQl/GetHttpsResources/RecomputeGetHttpsResourceHashValuesMutation.cs b/backend/src/GraphQl/GetHttpsResources/RecomputeGetHttpsResourceHashValuesMutation.cs index 00e5503a..6e232dc3 100644 --- a/backend/src/GraphQl/GetHttpsResources/RecomputeGetHttpsResourceHashValuesMutation.cs +++ b/backend/src/GraphQl/GetHttpsResources/RecomputeGetHttpsResourceHashValuesMutation.cs @@ -88,8 +88,6 @@ public async Task RecomputeGetHttpsR CancellationToken cancellationToken ) { - var queryContext = resolverContext.GetQueryContext(); - if ((await AuthorizeAsync( RecomputeGetHttpsResourceHashValuesErrorCode.UNAUTHENTICATED, RecomputeGetHttpsResourceHashValuesErrorCode.UNAUTHORIZED, @@ -101,10 +99,9 @@ CancellationToken cancellationToken { return authorizeErrorPayload; } - var resources = await context.GetHttpsResourcesWithData - .With(queryContext, sort => sort.StabilizeOrder()) + .With(resolverContext.GetQueryContext(), Sorting.DefaultEntityOrder) .ToListAsync(cancellationToken); var errors = new ConcurrentBag(); await Parallel.ForEachAsync( diff --git a/backend/src/GraphQl/GraphQlConstants.cs b/backend/src/GraphQl/GraphQlConstants.cs index 5854e8a3..b8ce2ece 100644 --- a/backend/src/GraphQl/GraphQlConstants.cs +++ b/backend/src/GraphQl/GraphQlConstants.cs @@ -2,11 +2,13 @@ namespace Database.GraphQl; internal static class GraphQlConstants { + internal const uint MaximumPageSize = 100; internal const string EndpointPath = "/graphql"; internal const string CorsPolicy = "GraphQlCorsPolicy"; internal const string TypeDiscriminatorPropertyName = "__typename"; internal const string FilterInputSuffix = "PropositionInput"; internal const string SortInputSuffix = "SortInput"; + internal const string PendingPrefix = "pending"; internal const string UuidFieldName = "uuid"; internal const string VersionFieldName = "version"; } \ No newline at end of file diff --git a/backend/src/GraphQl/GraphQlThrowHelper.cs b/backend/src/GraphQl/GraphQlThrowHelper.cs new file mode 100644 index 00000000..45fdff6d --- /dev/null +++ b/backend/src/GraphQl/GraphQlThrowHelper.cs @@ -0,0 +1,52 @@ +using System; +using System.Text.Json; +using HotChocolate; +using HotChocolate.Language; +using HotChocolate.Types; + +namespace Database.GraphQl; + +// https://github.com/ChilliCream/graphql-platform/blob/main/src/HotChocolate/Core/src/Types/Utilities/ThrowHelper.cs +public static class GraphQlThrowHelper +{ + public static LeafCoercionException ScalarCannotCoerceInputLiteral( + ITypeDefinition scalarType, + IValueNode? valueLiteral, + Exception? error = null) + { + valueLiteral ??= NullValueNode.Default; + var errorBuilder = + ErrorBuilder.New() + .SetMessage( + GraphQlTypeResources.ScalarCannotCoerceInputLiteral, + scalarType.Name, + valueLiteral.Kind); + if (error is not null) + { + errorBuilder.SetException(error); + } + return new LeafCoercionException( + errorBuilder.Build(), + scalarType); + } + + public static LeafCoercionException ScalarCannotCoerceInputValue( + ITypeDefinition scalarType, + JsonElement inputValue, + Exception? error = null) + { + var errorBuilder = + ErrorBuilder.New() + .SetMessage( + GraphQlTypeResources.ScalarCannotCoerceInputValue, + scalarType.Name, + inputValue.ValueKind); + if (error is not null) + { + errorBuilder.SetException(error); + } + return new LeafCoercionException( + errorBuilder.Build(), + scalarType); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/GraphQlTypeResources.cs b/backend/src/GraphQl/GraphQlTypeResources.cs new file mode 100644 index 00000000..d51cf4ca --- /dev/null +++ b/backend/src/GraphQl/GraphQlTypeResources.cs @@ -0,0 +1,8 @@ +namespace Database.GraphQl; + +// Inspired by https://github.com/ChilliCream/graphql-platform/blob/main/src/HotChocolate/Core/src/Types/Properties/TypeResources.resx +public static class GraphQlTypeResources +{ + public const string ScalarCannotCoerceInputLiteral = "{0} cannot coerce the given literal of type `{1}` to a runtime value."; + public const string ScalarCannotCoerceInputValue = "{0} cannot coerce the given value JSON element of type `{1}` to a runtime value."; +} \ No newline at end of file diff --git a/backend/src/GraphQl/HygrothermalDataX/CreateHygrothermalDataMutation.cs b/backend/src/GraphQl/HygrothermalDataX/CreateHygrothermalDataMutation.cs index c182836a..0803a74e 100644 --- a/backend/src/GraphQl/HygrothermalDataX/CreateHygrothermalDataMutation.cs +++ b/backend/src/GraphQl/HygrothermalDataX/CreateHygrothermalDataMutation.cs @@ -8,6 +8,7 @@ using Database.Data; using Database.Extensions; using Database.GraphQl.DataX; +using Database.GraphQl.Scalars; using Database.Services; using HotChocolate; using HotChocolate.Types; @@ -21,7 +22,7 @@ public sealed record CreateHygrothermalDataInput( string? Name, string? Description, string[] Warnings, - OffsetDateTime CreatedAt, + DateTimeOffset CreatedAt, Guid CreatorId, AppliedMethodInput AppliedMethod, RootGetHttpsResourceInput RootResource @@ -102,6 +103,7 @@ public async Task CreateHygrothermalDataAsync( IDataByDatabaseAndIdAndKindDataLoader dataByDatabaseAndIdAndKindDataLoader, IDataFormatByIdDataLoader dataFormatByIdDataLoader, ResponseApprovalService responseApprovalService, + IClock clock, CancellationToken cancellationToken ) { @@ -131,6 +133,7 @@ CancellationToken cancellationToken CreateHygrothermalDataErrorCode.UNKNOWN_DATA, dataFormatByIdDataLoader, CreateHygrothermalDataErrorCode.UNKNOWN_DATA_FORMAT, + clock, cancellationToken ) ).Failed(out var dataFormat, out var validateErrorPayload) @@ -163,4 +166,4 @@ CancellationToken cancellationToken return NewPayload(hygrothermalData, null); } -} +} \ No newline at end of file diff --git a/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataByIdDataLoader.cs b/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataByIdDataLoader.cs deleted file mode 100644 index deac4fcf..00000000 --- a/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataByIdDataLoader.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Database.Data; -using Database.GraphQl.Entities; -using GreenDonut; -using Microsoft.EntityFrameworkCore; - -namespace Database.GraphQl.HygrothermalDataX; - -public sealed class HygrothermalDataByIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : EntityByIdDataLoader( - batchScheduler, - options, - dbContextFactory, - dbContext => dbContext.HygrothermalData - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataLoaders.cs b/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataLoaders.cs new file mode 100644 index 00000000..b21bfca6 --- /dev/null +++ b/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataLoaders.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut; +using GreenDonut.Data; +using Database.Data; +using Microsoft.EntityFrameworkCore; + +namespace Database.GraphQl.HygrothermalDataX; + +public sealed class HygrothermalDataLoaders +: DataLoaders +{ + [DataLoader] + public static ValueTask> GetHygrothermalDataByIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetEntityByIdAsync( + ids, + databaseContext => databaseContext.HygrothermalData, + queryContext, + databaseContextFactory, + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataQueries.cs b/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataQueries.cs index 6f22f619..335dc032 100644 --- a/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataQueries.cs +++ b/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataQueries.cs @@ -1,14 +1,13 @@ using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Database.Authorization; using Database.Data; using Database.GraphQl.DataX; +using Database.GraphQl.Scalars; using Database.Services; using HotChocolate; using HotChocolate.Data; -using HotChocolate.Data.Sorting; using HotChocolate.Resolvers; using HotChocolate.Types; @@ -23,12 +22,11 @@ public sealed class HygrothermalDataQueries // same `id` and when also requesting `uuid`, the latter was always the empty UUID `000...`. [UseFiltering] [UseSorting] - public Task> GetAllHygrothermalDataAsync( + public Task> GetAllHygrothermalDataAsync( [GraphQLType] string? locale, ApplicationDbContext context, AccessRightsService accessRightsService, IResolverContext resolverContext, - ISortingContext sorting, CancellationToken cancellationToken ) { @@ -36,7 +34,6 @@ CancellationToken cancellationToken context.HygrothermalData, locale, accessRightsService, - sorting, resolverContext, cancellationToken ); @@ -47,12 +44,11 @@ CancellationToken cancellationToken // same `id` and when also requesting `uuid`, the latter was always the empty UUID `000...`. [UseFiltering] [UseSorting] - public Task> GetAllPendingHygrothermalDataAsync( + public Task> GetAllPendingHygrothermalDataAsync( [GraphQLType] string? locale, ApplicationDbContext context, AccessRightsService accessRightsService, IResolverContext resolverContext, - ISortingContext sorting, CommonAuthorization authorization, CancellationToken cancellationToken ) @@ -61,7 +57,6 @@ CancellationToken cancellationToken context.HygrothermalData, locale, accessRightsService, - sorting, resolverContext, authorization, cancellationToken diff --git a/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataType.cs b/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataType.cs index 78a411af..9f5ec67d 100644 --- a/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataType.cs +++ b/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataType.cs @@ -5,7 +5,7 @@ namespace Database.GraphQl.HygrothermalDataX; public sealed class HygrothermalDataType - : DataTypeBase + : DataTypeBase { protected override void Configure( IObjectTypeDescriptor descriptor diff --git a/backend/src/GraphQl/LifeCycleDataX/CreateLifeCycleDataMutation.cs b/backend/src/GraphQl/LifeCycleDataX/CreateLifeCycleDataMutation.cs index 0880b08d..a276eba6 100644 --- a/backend/src/GraphQl/LifeCycleDataX/CreateLifeCycleDataMutation.cs +++ b/backend/src/GraphQl/LifeCycleDataX/CreateLifeCycleDataMutation.cs @@ -8,6 +8,7 @@ using Database.Data; using Database.Extensions; using Database.GraphQl.DataX; +using Database.GraphQl.Scalars; using Database.Services; using HotChocolate; using HotChocolate.Types; @@ -21,7 +22,7 @@ public sealed record CreateLifeCycleDataInput( string? Name, string? Description, string[] Warnings, - OffsetDateTime CreatedAt, + DateTimeOffset CreatedAt, Guid CreatorId, AppliedMethodInput AppliedMethod, RootGetHttpsResourceInput RootResource @@ -102,6 +103,7 @@ public async Task CreateLifeCycleDataAsync( IDataByDatabaseAndIdAndKindDataLoader dataByDatabaseAndIdAndKindDataLoader, IDataFormatByIdDataLoader dataFormatByIdDataLoader, ResponseApprovalService responseApprovalService, + IClock clock, CancellationToken cancellationToken ) { @@ -131,6 +133,7 @@ CancellationToken cancellationToken CreateLifeCycleDataErrorCode.UNKNOWN_DATA, dataFormatByIdDataLoader, CreateLifeCycleDataErrorCode.UNKNOWN_DATA_FORMAT, + clock, cancellationToken ) ).Failed(out var dataFormat, out var validateErrorPayload) @@ -163,4 +166,4 @@ CancellationToken cancellationToken return NewPayload(lifeCycleData, null); } -} +} \ No newline at end of file diff --git a/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataByIdDataLoader.cs b/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataByIdDataLoader.cs deleted file mode 100644 index c3bd0833..00000000 --- a/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataByIdDataLoader.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Database.Data; -using Database.GraphQl.Entities; -using GreenDonut; -using Microsoft.EntityFrameworkCore; - -namespace Database.GraphQl.LifeCycleDataX; - -public sealed class LifeCycleDataByIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : EntityByIdDataLoader( - batchScheduler, - options, - dbContextFactory, - dbContext => dbContext.LifeCycleData - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataLoaders.cs b/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataLoaders.cs new file mode 100644 index 00000000..a0c8dbb7 --- /dev/null +++ b/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataLoaders.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut; +using GreenDonut.Data; +using Database.Data; +using Microsoft.EntityFrameworkCore; + +namespace Database.GraphQl.LifeCycleDataX; + +public sealed class LifeCycleDataLoaders +: DataLoaders +{ + [DataLoader] + public static ValueTask> GetLifeCycleDataByIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetEntityByIdAsync( + ids, + databaseContext => databaseContext.LifeCycleData, + queryContext, + databaseContextFactory, + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataQueries.cs b/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataQueries.cs index 01ffe18e..1f088c6d 100644 --- a/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataQueries.cs +++ b/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataQueries.cs @@ -1,14 +1,13 @@ using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Database.Authorization; using Database.Data; using Database.GraphQl.DataX; +using Database.GraphQl.Scalars; using Database.Services; using HotChocolate; using HotChocolate.Data; -using HotChocolate.Data.Sorting; using HotChocolate.Resolvers; using HotChocolate.Types; @@ -23,12 +22,11 @@ public sealed class LifeCycleDataQueries // same `id` and when also requesting `uuid`, the latter was always the empty UUID `000...`. [UseFiltering] [UseSorting] - public Task> GetAllLifeCycleDataAsync( + public Task> GetAllLifeCycleDataAsync( [GraphQLType] string? locale, ApplicationDbContext context, AccessRightsService accessRightsService, IResolverContext resolverContext, - ISortingContext sorting, CancellationToken cancellationToken ) { @@ -36,7 +34,6 @@ CancellationToken cancellationToken context.LifeCycleData, locale, accessRightsService, - sorting, resolverContext, cancellationToken ); @@ -47,12 +44,11 @@ CancellationToken cancellationToken // same `id` and when also requesting `uuid`, the latter was always the empty UUID `000...`. [UseFiltering] [UseSorting] - public Task> GetAllPendingLifeCycleDataAsync( + public Task> GetAllPendingLifeCycleDataAsync( [GraphQLType] string? locale, ApplicationDbContext context, AccessRightsService accessRightsService, IResolverContext resolverContext, - ISortingContext sorting, CommonAuthorization authorization, CancellationToken cancellationToken ) @@ -61,7 +57,6 @@ CancellationToken cancellationToken context.LifeCycleData, locale, accessRightsService, - sorting, resolverContext, authorization, cancellationToken diff --git a/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataType.cs b/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataType.cs index 3498cec9..0aebaf0e 100644 --- a/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataType.cs +++ b/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataType.cs @@ -5,7 +5,7 @@ namespace Database.GraphQl.LifeCycleDataX; public sealed class LifeCycleDataType - : DataTypeBase + : DataTypeBase { protected override void Configure( IObjectTypeDescriptor descriptor diff --git a/backend/src/GraphQl/OpticalDataX/CreateOpticalDataMutation.cs b/backend/src/GraphQl/OpticalDataX/CreateOpticalDataMutation.cs index 8f71758c..7802a3c9 100644 --- a/backend/src/GraphQl/OpticalDataX/CreateOpticalDataMutation.cs +++ b/backend/src/GraphQl/OpticalDataX/CreateOpticalDataMutation.cs @@ -10,6 +10,7 @@ using Database.Enumerations; using Database.Extensions; using Database.GraphQl.DataX; +using Database.GraphQl.Scalars; using Database.Services; using HotChocolate; using HotChocolate.Types; @@ -23,7 +24,7 @@ public sealed record CreateOpticalDataInput( string? Name, string? Description, string[] Warnings, - OffsetDateTime CreatedAt, + DateTimeOffset CreatedAt, Guid CreatorId, OpticalComponentType? Type, OpticalComponentSubtype? Subtype, @@ -124,6 +125,7 @@ public async Task CreateOpticalDataAsync( IDataByDatabaseAndIdAndKindDataLoader dataByDatabaseAndIdAndKindDataLoader, IDataFormatByIdDataLoader dataFormatByIdDataLoader, ResponseApprovalService responseApprovalService, + IClock clock, CancellationToken cancellationToken ) { @@ -153,6 +155,7 @@ CancellationToken cancellationToken CreateOpticalDataErrorCode.UNKNOWN_DATA, dataFormatByIdDataLoader, CreateOpticalDataErrorCode.UNKNOWN_DATA_FORMAT, + clock, cancellationToken ) ).Failed(out var dataFormat, out var validateErrorPayload) @@ -185,4 +188,4 @@ CancellationToken cancellationToken return NewPayload(opticalData, null); } -} +} \ No newline at end of file diff --git a/backend/src/GraphQl/OpticalDataX/OpticalDataByIdDataLoader.cs b/backend/src/GraphQl/OpticalDataX/OpticalDataByIdDataLoader.cs deleted file mode 100644 index e1a27bc1..00000000 --- a/backend/src/GraphQl/OpticalDataX/OpticalDataByIdDataLoader.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Database.Data; -using Database.GraphQl.Entities; -using GreenDonut; -using Microsoft.EntityFrameworkCore; - -namespace Database.GraphQl.OpticalDataX; - -public sealed class OpticalDataByIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : EntityByIdDataLoader( - batchScheduler, - options, - dbContextFactory, - dbContext => dbContext.OpticalData - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/OpticalDataX/OpticalDataLoaders.cs b/backend/src/GraphQl/OpticalDataX/OpticalDataLoaders.cs new file mode 100644 index 00000000..08ec8081 --- /dev/null +++ b/backend/src/GraphQl/OpticalDataX/OpticalDataLoaders.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut; +using GreenDonut.Data; +using Database.Data; +using Microsoft.EntityFrameworkCore; + +namespace Database.GraphQl.OpticalDataX; + +public sealed class OpticalDataLoaders +: DataLoaders +{ + [DataLoader] + public static ValueTask> GetOpticalDataByIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetEntityByIdAsync( + ids, + databaseContext => databaseContext.OpticalData, + queryContext, + databaseContextFactory, + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/OpticalDataX/OpticalDataQueries.cs b/backend/src/GraphQl/OpticalDataX/OpticalDataQueries.cs index 24196d2f..b9bd9864 100644 --- a/backend/src/GraphQl/OpticalDataX/OpticalDataQueries.cs +++ b/backend/src/GraphQl/OpticalDataX/OpticalDataQueries.cs @@ -1,14 +1,13 @@ using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Database.Authorization; using Database.Data; using Database.GraphQl.DataX; +using Database.GraphQl.Scalars; using Database.Services; using HotChocolate; using HotChocolate.Data; -using HotChocolate.Data.Sorting; using HotChocolate.Resolvers; using HotChocolate.Types; @@ -23,11 +22,10 @@ public sealed class OpticalDataQueries // same `id` and when also requesting `uuid`, the latter was always the empty UUID `000...`. [UseFiltering] [UseSorting] - public Task> GetAllOpticalDataAsync( + public Task> GetAllOpticalDataAsync( [GraphQLType] string? locale, ApplicationDbContext context, AccessRightsService accessRightsService, - ISortingContext sorting, IResolverContext resolverContext, CancellationToken cancellationToken ) @@ -36,7 +34,6 @@ CancellationToken cancellationToken context.OpticalData, locale, accessRightsService, - sorting, resolverContext, cancellationToken ); @@ -47,11 +44,10 @@ CancellationToken cancellationToken // same `id` and when also requesting `uuid`, the latter was always the empty UUID `000...`. [UseFiltering] [UseSorting] - public Task> GetAllPendingOpticalDataAsync( + public Task> GetAllPendingOpticalDataAsync( [GraphQLType] string? locale, ApplicationDbContext context, AccessRightsService accessRightsService, - ISortingContext sorting, IResolverContext resolverContext, CommonAuthorization authorization, CancellationToken cancellationToken @@ -61,7 +57,6 @@ CancellationToken cancellationToken context.OpticalData, locale, accessRightsService, - sorting, resolverContext, authorization, cancellationToken diff --git a/backend/src/GraphQl/OpticalDataX/OpticalDataType.cs b/backend/src/GraphQl/OpticalDataX/OpticalDataType.cs index b1e97cf3..5adb469b 100644 --- a/backend/src/GraphQl/OpticalDataX/OpticalDataType.cs +++ b/backend/src/GraphQl/OpticalDataX/OpticalDataType.cs @@ -5,7 +5,7 @@ namespace Database.GraphQl.OpticalDataX; public sealed class OpticalDataType - : DataTypeBase + : DataTypeBase { protected override void Configure( IObjectTypeDescriptor descriptor diff --git a/backend/src/GraphQl/PaginatedConnection.cs b/backend/src/GraphQl/PaginatedConnection.cs new file mode 100644 index 00000000..cfb462d4 --- /dev/null +++ b/backend/src/GraphQl/PaginatedConnection.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut; +using GreenDonut.Data; +using HotChocolate.CostAnalysis.Types; +using HotChocolate.Types.Pagination; +using Database.Data; +using Database.GraphQl.Extensions; + +namespace Database.GraphQl; + +public abstract class PaginatedConnection< + TSubject, + TAssociation, + TEdge, + TAssociationsByOneIdDataLoader +>( + TSubject subject, + Func createEdge, + PagingArguments pagingArguments, + QueryContext queryContext +) + where TSubject : IEntity + where TAssociation : class + where TAssociationsByOneIdDataLoader : IDataLoader> +{ + protected TSubject Subject { get; } = subject; + + [Cost(0)] + public ValueTask GetTotalCountAsync( + TAssociationsByOneIdDataLoader dataLoader, + CancellationToken cancellationToken + ) + { + return dataLoader + .With(pagingArguments, queryContext) + .LoadAsync(Subject.Id, cancellationToken) + .GetTotalCountAsync(); + } + + [Cost(0)] + public ValueTask GetPageInfoAsync( + TAssociationsByOneIdDataLoader dataLoader, + CancellationToken cancellationToken + ) + { + return dataLoader + .With(pagingArguments, queryContext) + .LoadAsync(Subject.Id, cancellationToken) + .GetPageInfoAsync(); + } + + [Cost(0)] + public async IAsyncEnumerable GetEdgesAsync( + TAssociationsByOneIdDataLoader dataLoader, + [EnumeratorCancellation] CancellationToken cancellationToken + ) + { + var page = + await dataLoader + .With(pagingArguments, queryContext) + .LoadAsync(Subject.Id, cancellationToken); + if (page is null) + { + yield break; + } + foreach (var entry in page.Entries) + { + yield return createEdge(entry.Item, page.CreateCursor(entry)); + } + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/PaginatedEdge.cs b/backend/src/GraphQl/PaginatedEdge.cs new file mode 100644 index 00000000..07678c18 --- /dev/null +++ b/backend/src/GraphQl/PaginatedEdge.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut; +using HotChocolate.CostAnalysis.Types; + +namespace Database.GraphQl; + +public abstract record PaginatedEdge( + TNode Node, + string Cursor +); + +public abstract class PaginatedEdge( + Guid nodeId, + string cursor +) + where TNodeByIdDataLoader : IDataLoader + where TNode : notnull +{ + [Cost(0)] + public Task GetNodeAsync( + TNodeByIdDataLoader byId, + CancellationToken cancellationToken + ) + { + return byId.LoadRequiredAsync(nodeId, cancellationToken); + } + + public string Cursor => cursor; +} \ No newline at end of file diff --git a/backend/src/GraphQl/PhotovoltaicDataX/CreatePhotovoltaicDataMutation.cs b/backend/src/GraphQl/PhotovoltaicDataX/CreatePhotovoltaicDataMutation.cs index 42014c7b..2729dd9a 100644 --- a/backend/src/GraphQl/PhotovoltaicDataX/CreatePhotovoltaicDataMutation.cs +++ b/backend/src/GraphQl/PhotovoltaicDataX/CreatePhotovoltaicDataMutation.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Database.ApiRequests; @@ -9,8 +8,8 @@ using Database.Data; using Database.Extensions; using Database.GraphQl.DataX; +using Database.GraphQl.Scalars; using Database.Services; -using Database.Utilities; using HotChocolate; using HotChocolate.Types; using NodaTime; @@ -23,7 +22,7 @@ public sealed record CreatePhotovoltaicDataInput( string? Name, string? Description, string[] Warnings, - OffsetDateTime CreatedAt, + DateTimeOffset CreatedAt, Guid CreatorId, AppliedMethodInput AppliedMethod, RootGetHttpsResourceInput RootResource @@ -104,6 +103,7 @@ public async Task CreatePhotovoltaicDataAsync( IDataByDatabaseAndIdAndKindDataLoader dataByDatabaseAndIdAndKindDataLoader, IDataFormatByIdDataLoader dataFormatByIdDataLoader, ResponseApprovalService responseApprovalService, + IClock clock, CancellationToken cancellationToken ) { @@ -133,6 +133,7 @@ CancellationToken cancellationToken CreatePhotovoltaicDataErrorCode.UNKNOWN_DATA, dataFormatByIdDataLoader, CreatePhotovoltaicDataErrorCode.UNKNOWN_DATA_FORMAT, + clock, cancellationToken ) ).Failed(out var dataFormat, out var validateErrorPayload) @@ -165,4 +166,4 @@ CancellationToken cancellationToken return NewPayload(photovoltaicData, null); } -} +} \ No newline at end of file diff --git a/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataByIdDataLoader.cs b/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataByIdDataLoader.cs deleted file mode 100644 index 79e00ab5..00000000 --- a/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataByIdDataLoader.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Database.Data; -using Database.GraphQl.Entities; -using GreenDonut; -using Microsoft.EntityFrameworkCore; - -namespace Database.GraphQl.PhotovoltaicDataX; - -public sealed class PhotovoltaicDataByIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : EntityByIdDataLoader( - batchScheduler, - options, - dbContextFactory, - dbContext => dbContext.PhotovoltaicData - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataLoaders.cs b/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataLoaders.cs new file mode 100644 index 00000000..cf51e490 --- /dev/null +++ b/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataLoaders.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut; +using GreenDonut.Data; +using Database.Data; +using Microsoft.EntityFrameworkCore; + +namespace Database.GraphQl.PhotovoltaicDataX; + +public sealed class PhotovoltaicDataLoaders +: DataLoaders +{ + [DataLoader] + public static ValueTask> GetPhotovoltaicDataByIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetEntityByIdAsync( + ids, + databaseContext => databaseContext.PhotovoltaicData, + queryContext, + databaseContextFactory, + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataQueries.cs b/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataQueries.cs index d64b19eb..1f377806 100644 --- a/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataQueries.cs +++ b/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataQueries.cs @@ -1,16 +1,16 @@ using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Database.Authorization; using Database.Data; using Database.GraphQl.DataX; +using Database.GraphQl.Scalars; using Database.Services; using HotChocolate; using HotChocolate.Data; -using HotChocolate.Data.Sorting; using HotChocolate.Resolvers; using HotChocolate.Types; +using NodaTime; namespace Database.GraphQl.PhotovoltaicDataX; @@ -23,11 +23,10 @@ public sealed class PhotovoltaicDataQueries // same `id` and when also requesting `uuid`, the latter was always the empty UUID `000...`. [UseFiltering] [UseSorting] - public Task> GetAllPhotovoltaicDataAsync( + public Task> GetAllPhotovoltaicDataAsync( [GraphQLType] string? locale, ApplicationDbContext context, AccessRightsService accessRightsService, - ISortingContext sorting, IResolverContext resolverContext, CancellationToken cancellationToken ) @@ -36,7 +35,6 @@ CancellationToken cancellationToken context.PhotovoltaicData, locale, accessRightsService, - sorting, resolverContext, cancellationToken ); @@ -47,11 +45,10 @@ CancellationToken cancellationToken // same `id` and when also requesting `uuid`, the latter was always the empty UUID `000...`. [UseFiltering] [UseSorting] - public Task> GetAllPendingPhotovoltaicDataAsync( + public Task> GetAllPendingPhotovoltaicDataAsync( [GraphQLType] string? locale, ApplicationDbContext context, AccessRightsService accessRightsService, - ISortingContext sorting, IResolverContext resolverContext, CommonAuthorization authorization, CancellationToken cancellationToken @@ -61,7 +58,6 @@ CancellationToken cancellationToken context.PhotovoltaicData, locale, accessRightsService, - sorting, resolverContext, authorization, cancellationToken @@ -89,6 +85,7 @@ CancellationToken cancellationToken [GraphQLType] string? locale, PhotovoltaicDataByIdDataLoader byId, AccessRightsService accessRightsService, + IClock clock, CancellationToken cancellationToken ) { diff --git a/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataType.cs b/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataType.cs index 2a445324..ee2a79e9 100644 --- a/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataType.cs +++ b/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataType.cs @@ -5,7 +5,7 @@ namespace Database.GraphQl.PhotovoltaicDataX; public sealed class PhotovoltaicDataType - : DataTypeBase + : DataTypeBase { protected override void Configure( IObjectTypeDescriptor descriptor diff --git a/backend/src/GraphQl/ResponseApprovals/CreateResponseApprovalsMutation.cs b/backend/src/GraphQl/ResponseApprovals/CreateResponseApprovalsMutation.cs index 09643128..e10dd56d 100644 --- a/backend/src/GraphQl/ResponseApprovals/CreateResponseApprovalsMutation.cs +++ b/backend/src/GraphQl/ResponseApprovals/CreateResponseApprovalsMutation.cs @@ -88,7 +88,6 @@ public async Task CreateResponseApprovalsAsync( CancellationToken cancellationToken ) { - var queryContext = resolverContext.GetQueryContext(); if ((await AuthorizeAsync( CreateResponseApprovalsErrorCode.UNAUTHENTICATED, CreateResponseApprovalsErrorCode.UNAUTHORIZED, @@ -103,7 +102,7 @@ CancellationToken cancellationToken var dataSets = new ConcurrentBag(); var errors = new ConcurrentBag(); await Parallel.ForEachAsync( - context.GetAllDataAsync(_ => _.Approval == null, queryContext), + context.GetAllDataAsync(_ => _.Approval == null, resolverContext.GetQueryContext()), cancellationToken, async (data, cancellationToken) => { diff --git a/backend/src/GraphQl/ResponseApprovals/ResponseApprovalFilterType.cs b/backend/src/GraphQl/ResponseApprovals/ResponseApprovalFilterType.cs index 9f2fc9c8..ca198b36 100644 --- a/backend/src/GraphQl/ResponseApprovals/ResponseApprovalFilterType.cs +++ b/backend/src/GraphQl/ResponseApprovals/ResponseApprovalFilterType.cs @@ -5,7 +5,7 @@ namespace Database.GraphQl.ResponseApprovals; public abstract class ResponseApprovalFilterType -: EntityFilterType +: AuditableEntityFilterType { protected override void Configure( IFilterInputTypeDescriptor descriptor @@ -19,7 +19,6 @@ IFilterInputTypeDescriptor descriptor descriptor.Field(x => x.Description); descriptor.Field(x => x.ComponentId); descriptor.Field(x => x.CreatorId); - descriptor.Field(x => x.CreatedAt); descriptor.Field(x => x.AppliedMethod); descriptor.Field(x => x.Approvals); descriptor.Field(x => x.Resources); diff --git a/backend/src/GraphQl/ResponseApprovals/UpdateResponseApprovalsMutation.cs b/backend/src/GraphQl/ResponseApprovals/UpdateResponseApprovalsMutation.cs index 8903d6fa..7c59e799 100644 --- a/backend/src/GraphQl/ResponseApprovals/UpdateResponseApprovalsMutation.cs +++ b/backend/src/GraphQl/ResponseApprovals/UpdateResponseApprovalsMutation.cs @@ -86,7 +86,6 @@ public async Task UpdateResponseApprovalsAsync( CancellationToken cancellationToken ) { - var queryContext = resolverContext.GetQueryContext(); if ((await AuthorizeAsync( UpdateResponseApprovalsErrorCode.UNAUTHENTICATED, UpdateResponseApprovalsErrorCode.UNAUTHORIZED, @@ -102,7 +101,7 @@ CancellationToken cancellationToken var dataSets = new ConcurrentBag(); var errors = new ConcurrentBag(); await Parallel.ForEachAsync( - context.GetAllDataAsync(_ => _.Approval != null, queryContext), + context.GetAllDataAsync(_ => _.Approval != null, resolverContext.GetQueryContext()), cancellationToken, async (data, cancellationToken) => { diff --git a/backend/src/GraphQl/LocaleType.cs b/backend/src/GraphQl/Scalars/LocaleType.cs similarity index 94% rename from backend/src/GraphQl/LocaleType.cs rename to backend/src/GraphQl/Scalars/LocaleType.cs index 0c337de6..1b6f9c86 100644 --- a/backend/src/GraphQl/LocaleType.cs +++ b/backend/src/GraphQl/Scalars/LocaleType.cs @@ -2,7 +2,7 @@ using HotChocolate.Types; // TODO Maybe use an enumeration as runtime type instead of string (and fallback to english when the given one does not exist).namespace Database.GraphQl -namespace Database.GraphQl; +namespace Database.GraphQl.Scalars; /// /// [BCP 47](https://tools.ietf.org/html/bcp47) @@ -29,12 +29,12 @@ public sealed class LocaleType( BindingBehavior bind = BindingBehavior.Explicit) : RegexType( name, - _validationPattern, + ValidationPattern, description, RegexOptions.Compiled | RegexOptions.IgnoreCase, bind) { - private const string _validationPattern = + private const string ValidationPattern = "^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)?(-[a-zA-Z0-9]+)?$"; /// diff --git a/backend/src/GraphQl/Scalars/MyUriType.cs b/backend/src/GraphQl/Scalars/MyUriType.cs new file mode 100644 index 00000000..0d6c2e41 --- /dev/null +++ b/backend/src/GraphQl/Scalars/MyUriType.cs @@ -0,0 +1,110 @@ +using System; +using System.Text.Json; +using HotChocolate.Features; +using HotChocolate.Language; +using HotChocolate.Text.Json; +using Database.GraphQl; +using Microsoft.Extensions.DependencyInjection; +using HotChocolate.Types; + +namespace Database.GraphQl.Scalars; + +// Inspired by https://github.com/ChilliCream/graphql-platform/blob/main/src/HotChocolate/Core/src/Types/Types/Scalars/UriType.cs +/// +/// [RFC 3986](https://tools.ietf.org/html/rfc3986) +/// and +/// [RFC 3987](https://tools.ietf.org/html/rfc3987) +/// compliant +/// [absolute Uniform Resource Locator (URL)](https://tools.ietf.org/html/rfc3986#section-4.3) +/// string with optional +/// [fragment identifier](https://tools.ietf.org/html/rfc3986#section-3.5). +/// [Valid values are for example](https://datatracker.ietf.org/doc/html/rfc3986#section-1.1.2) +/// `ftp://ftp.is.co.za/rfc/rfc1808.txt`, `http://www.ietf.org/rfc/rfc2396.txt`, +/// `ldap://[2001:db8::7]/c=GB?objectClass?one`, `mailto:John.Doe@example.com`, +/// `news:comp.infosystems.www.servers.unix`, `tel:+1-816-555-1212`, +/// `telnet://192.0.2.16:80/`, +/// `urn:oasis:names:specification:docbook:dtd:xml:4.1.2` +/// +/// See also +/// [URL Living Standard](https://url.spec.whatwg.org/#absolute-url-with-fragment-string) +/// and +/// [Identifying resources on the Web](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Identifying_resources_on_the_Web). +/// +/// Specification +public sealed class MyUriType : ScalarType +{ + private const string ScalarName = "Url"; + + private const string SpecifiedByUri = "https://tools.ietf.org/html/rfc3986"; + + public MyUriType( + string name, + string? description = null, + BindingBehavior bind = BindingBehavior.Explicit) + : base(name, bind) + { + Description = description; + SpecifiedBy = new Uri(SpecifiedByUri); + } + + /// + [ActivatorUtilitiesConstructor] + public MyUriType() + : this( + ScalarName, + $"The `{ScalarName}` scalar type represents a Uniform Resource Identifier (URI) as defined by RFC 3986.", + BindingBehavior.Implicit) + { + } + + /// + protected override Uri OnCoerceInputLiteral(StringValueNode valueLiteral) + { + if (TryParseUri(valueLiteral.Value, out var value)) + { + return value; + } + + throw GraphQlThrowHelper.ScalarCannotCoerceInputLiteral(this, valueLiteral); + } + + /// + protected override Uri OnCoerceInputValue(JsonElement inputValue, IFeatureProvider context) + { + if (TryParseUri(inputValue.GetString()!, out var value)) + { + return value; + } + + throw GraphQlThrowHelper.ScalarCannotCoerceInputValue(this, inputValue); + } + + /// + protected override void OnCoerceOutputValue(Uri runtimeValue, ResultElement resultValue) + { + var serialized = runtimeValue.IsAbsoluteUri + ? runtimeValue.AbsoluteUri + : runtimeValue.ToString(); + resultValue.SetStringValue(serialized); + } + + /// + protected override StringValueNode OnValueToLiteral(Uri runtimeValue) + { + var value = runtimeValue.IsAbsoluteUri + ? runtimeValue.AbsoluteUri + : runtimeValue.ToString(); + return new StringValueNode(value); + } + + private static bool TryParseUri(string value, out Uri uri) + { + if (!Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out var parsedUri)) + { + uri = null!; + return false; + } + uri = parsedUri; + return true; + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Scalars/NonNegativeIntType.cs b/backend/src/GraphQl/Scalars/NonNegativeIntType.cs new file mode 100644 index 00000000..480014f5 --- /dev/null +++ b/backend/src/GraphQl/Scalars/NonNegativeIntType.cs @@ -0,0 +1,72 @@ +using System.Text.Json; +using HotChocolate.Language; +using HotChocolate.Text.Json; +using HotChocolate.Types; +using Microsoft.Extensions.DependencyInjection; + +namespace Database.GraphQl.Scalars; + +/// +/// +/// The NonNegativeInt scalar type represents an unsigned 32‐bit numeric +/// non‐fractional value. Response formats that support an unsigned 32‐bit +/// integer or a number type should use that type to represent this scalar. +/// +/// +public sealed class NonNegativeIntType +: IntegerTypeBase +{ + public const string ScalarName = "NonNegativeInt"; + + /// + /// Initializes a new instance of the class. + /// + public NonNegativeIntType(uint min, uint max) + : this( + ScalarName, + $"The `{ScalarName}` scalar type represents an unsigned 32-bit numeric non-fractional value.", + min, + max, + BindingBehavior.Implicit) + { + } + + /// + /// Initializes a new instance of the class. + /// + public NonNegativeIntType( + string name, + string? description = null, + uint min = uint.MinValue, + uint max = uint.MaxValue, + BindingBehavior bind = BindingBehavior.Explicit) + : base(name, min, max, bind) + { + Description = description; + } + + /// + /// Initializes a new instance of the class. + /// + [ActivatorUtilitiesConstructor] + public NonNegativeIntType() + : this(uint.MinValue, uint.MaxValue) + { + } + + /// + protected override uint OnCoerceInputLiteral(IntValueNode valueLiteral) + => valueLiteral.ToUInt32(); + + /// + protected override uint OnCoerceInputValue(JsonElement inputValue) + => inputValue.GetUInt32(); + + /// + protected override void OnCoerceOutputValue(uint runtimeValue, ResultElement resultValue) + => resultValue.SetNumberValue(runtimeValue); + + /// + protected override IValueNode OnValueToLiteral(uint runtimeValue) + => new IntValueNode(runtimeValue); +} \ No newline at end of file diff --git a/backend/src/GraphQl/Sorting.cs b/backend/src/GraphQl/Sorting.cs new file mode 100644 index 00000000..91a8770d --- /dev/null +++ b/backend/src/GraphQl/Sorting.cs @@ -0,0 +1,18 @@ +using GreenDonut.Data; +using Database.Data; + +namespace Database.GraphQl; + +public static class Sorting +{ + public static SortDefinition DefaultEntityOrder( + SortDefinition sort + ) + where TEntity : class, IEntity//, IAuditable + { + // always sort by primary key to make pagination cursors unique + return sort + // .IfEmpty(_ => _.AddDescending(_ => _.CreatedAt)) + .AddDescending(_ => _.Id); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Users/UserByIdDataLoader.cs b/backend/src/GraphQl/Users/UserByIdDataLoader.cs deleted file mode 100644 index afc10301..00000000 --- a/backend/src/GraphQl/Users/UserByIdDataLoader.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Database.Data; -using Database.GraphQl.Entities; -using GreenDonut; -using Microsoft.EntityFrameworkCore; - -namespace Database.GraphQl.Users; - -public sealed class UserByIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : EntityByIdDataLoader( - batchScheduler, - options, - dbContextFactory, - dbContext => dbContext.Users - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Users/UserDataLoaders.cs b/backend/src/GraphQl/Users/UserDataLoaders.cs new file mode 100644 index 00000000..4a4db545 --- /dev/null +++ b/backend/src/GraphQl/Users/UserDataLoaders.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut; +using GreenDonut.Data; +using Database.Data; +using Microsoft.EntityFrameworkCore; + +namespace Database.GraphQl.Users; + +public sealed class UserDataLoaders +: DataLoaders +{ + [DataLoader] + public static ValueTask> GetUserByIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetEntityByIdAsync( + ids, + databaseContext => databaseContext.Users, + queryContext, + databaseContextFactory, + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Users/UserType.cs b/backend/src/GraphQl/Users/UserType.cs index 08a24a23..d05d62c0 100644 --- a/backend/src/GraphQl/Users/UserType.cs +++ b/backend/src/GraphQl/Users/UserType.cs @@ -1,10 +1,11 @@ using Database.Data; +using Database.GraphQl.Entities; using HotChocolate.Types; namespace Database.GraphQl.Users; public sealed class UserType - : EntityType + : EntityType { protected override void Configure( IObjectTypeDescriptor descriptor diff --git a/backend/src/Jobs/JwtSigningAndEncryptionCertificateRotationJob.cs b/backend/src/Jobs/JwtSigningAndEncryptionCertificateRotationJob.cs index 580efe5f..3165f2ec 100644 --- a/backend/src/Jobs/JwtSigningAndEncryptionCertificateRotationJob.cs +++ b/backend/src/Jobs/JwtSigningAndEncryptionCertificateRotationJob.cs @@ -1,10 +1,11 @@ using System; -using System.Runtime.ConstrainedExecution; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Database.Authentication; +using Database.Extensions; using Microsoft.Extensions.Logging; +using NodaTime; using Quartz; namespace Database.Jobs; @@ -48,6 +49,7 @@ Exception exception } public sealed class JwtSigningAndEncryptionCertificateRotationJob( + IClock clock, ILogger logger ) : IJob @@ -71,7 +73,7 @@ public async Task Execute(IJobExecutionContext context) // TODO: Trigger OpenIddict reload. Currently done dialy with a cron job that restart all services. } - public static X509Certificate2 CreateSigningCertificate(string distinguishedName) + public static X509Certificate2 CreateSigningCertificate(string distinguishedName, IClock clock) { // In the future use ECDSA. // using var algorithm = ECDsa.Create(ECCurve.NamedCurves.nistP256); @@ -93,7 +95,7 @@ public static X509Certificate2 CreateSigningCertificate(string distinguishedName critical: true ) ); - var now = TimeProvider.System.GetUtcNow(); + var now = clock.GetUtcNow().ToDateTimeOffset(); var ephemeralCertificate = request.CreateSelfSigned( notBefore: now.Add(s_notBeforeOffset), notAfter: now.Add(s_notAfterOffset) @@ -110,7 +112,7 @@ public static X509Certificate2 CreateSigningCertificate(string distinguishedName { try { - return CreateSigningCertificate(distinguishedName); + return CreateSigningCertificate(distinguishedName, clock); } catch (Exception exception) { @@ -119,7 +121,7 @@ public static X509Certificate2 CreateSigningCertificate(string distinguishedName } } - public static X509Certificate2 CreateEncryptionCertificate(string distinguishedName) + public static X509Certificate2 CreateEncryptionCertificate(string distinguishedName, IClock clock) { // In the furture use `ML-KEM`. using var algorithm = RSA.Create(keySizeInBits: 3072); @@ -135,7 +137,7 @@ public static X509Certificate2 CreateEncryptionCertificate(string distinguishedN critical: true ) ); - var now = TimeProvider.System.GetUtcNow(); + var now = clock.GetUtcNow().ToDateTimeOffset(); var ephemeralCertificate = request.CreateSelfSigned( notBefore: now.Add(s_notBeforeOffset), notAfter: now.Add(s_notAfterOffset) @@ -152,7 +154,7 @@ public static X509Certificate2 CreateEncryptionCertificate(string distinguishedN { try { - return CreateEncryptionCertificate(distinguishedName); + return CreateEncryptionCertificate(distinguishedName, clock); } catch (Exception exception) { @@ -198,10 +200,10 @@ public void CleanupLongExpiredCertificatesWithErrorHandling(params string[] dist distinguishedName, validOnly: false ); - var now = TimeProvider.System.GetUtcNow(); + var now = clock.GetUtcNow().ToDateTimeOffset(); foreach (var certificate in certificates) { - // Use `NotAfterDaysOffset` as overlap period. + // Use `RefreshTokenLifetime` as overlap period. if (certificate.NotAfter.Add(OpenIdConnectConstants.RefreshTokenLifetime) < now) { try diff --git a/backend/src/Methods/SpectralToIntegral/SpectralToIntegralMethod.cs b/backend/src/Methods/SpectralToIntegral/SpectralToIntegralMethod.cs index 117db51e..367dc4ff 100644 --- a/backend/src/Methods/SpectralToIntegral/SpectralToIntegralMethod.cs +++ b/backend/src/Methods/SpectralToIntegral/SpectralToIntegralMethod.cs @@ -226,7 +226,7 @@ public double Calculate( ImmutableArray<(int wavelength, double weight, double deltaWavelength)> wavelengthsWeights ) { - if (spectralDataPoints == null || spectralDataPoints.Count == 0) + if (spectralDataPoints is null || spectralDataPoints.Count is 0) { throw new ArgumentException("The list `spectralDataPoints` is empty."); } diff --git a/backend/src/Program.cs b/backend/src/Program.cs index 3f99d381..2f644af0 100644 --- a/backend/src/Program.cs +++ b/backend/src/Program.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using HotChocolate.AspNetCore; using Database.Data; using Database.Services; using Microsoft.AspNetCore.Builder; diff --git a/backend/src/Services/AccessRightsService.cs b/backend/src/Services/AccessRightsService.cs index 6c66b454..2f3c1f5d 100644 --- a/backend/src/Services/AccessRightsService.cs +++ b/backend/src/Services/AccessRightsService.cs @@ -6,6 +6,7 @@ using Database.Data; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using NodaTime; using static Database.ApiRequests.QueryCurrentUserOrInstitution; namespace Database.Services; @@ -27,7 +28,9 @@ public sealed class AccessRightsService( IDbContextFactory dbContextFactory, UserService userService, CacheService cacheService, - ILogger logger) + IClock clock, + ILogger logger +) { public async Task ApplyAccessRightsOnData(T data, CancellationToken cancellationToken) where T : IData @@ -65,7 +68,7 @@ CancellationToken cancellationToken institutionAccessRights = await GetInstitutionAccessRightsAsync(institutionIds, context, cancellationToken); } - var result = ProcessData(data, currentUserOrInstitution.CurrentUser, openIdConnectClientId, institutionIds, institutionAccessRights, alreadyAccessedByUserCount, cacheService); + var result = ProcessData(data, currentUserOrInstitution.CurrentUser, openIdConnectClientId, institutionIds, institutionAccessRights, alreadyAccessedByUserCount); if (context is not null) { @@ -95,8 +98,7 @@ private IEnumerable ProcessData( string? openIdConnectClientId, IReadOnlyList? institutionIds, IReadOnlyList? institutionAccessRights, - uint? alreadyAccessedByUserCount, - CacheService cacheService + uint? alreadyAccessedByUserCount ) where T : IData { @@ -145,7 +147,7 @@ currentUser is null && institutionAccessRights.Any(x => ( x.HasRestrictionsByTime - && x.IsDataRestrictedByTime(dataItem, cacheService, out reason) + && x.IsDataRestrictedByTime(dataItem, clock, cacheService, out reason) ) || ( x.HasRestrictionsByUser diff --git a/backend/src/Services/CacheService.cs b/backend/src/Services/CacheService.cs index 77dca2e3..0a4dc806 100644 --- a/backend/src/Services/CacheService.cs +++ b/backend/src/Services/CacheService.cs @@ -10,7 +10,9 @@ namespace Database.Services; public sealed class CacheService( IMemoryCache currentUserOrInstitutionCache, IMemoryCache accessCountCache, - IMemoryCache timePeriodCountCache) + IMemoryCache timePeriodCountCache, + IClock clock +) { public CurrentUserOrInstitution? SetCurrentUserOrInstitution(string token, CurrentUserOrInstitution cachedUserOrInstitution) { @@ -43,7 +45,7 @@ public uint SetAccessCountForUser(Guid userId, uint count) { if (!timePeriodCountCache.TryGetValue(institutionId, out (OffsetDateTime StartTime, uint Count) accessesPerPeriod)) { - return timePeriodCountCache.Set(institutionId, (OffsetDateTime.UtcNow, (uint)0)); + return timePeriodCountCache.Set(institutionId, (clock.GetUtcNow(), (uint)0)); } return accessesPerPeriod; } @@ -57,6 +59,6 @@ public uint SetAccessCountForUser(Guid userId, uint count) public (OffsetDateTime StartTime, uint Count) SetNewTimePeriod(Guid institutionId) { - return timePeriodCountCache.Set(institutionId, (OffsetDateTime.UtcNow, (uint)0)); + return timePeriodCountCache.Set(institutionId, (clock.GetUtcNow(), (uint)0)); } -} +} \ No newline at end of file diff --git a/backend/src/Services/ResponseApprovalService.cs b/backend/src/Services/ResponseApprovalService.cs index 7d78b304..a5840469 100644 --- a/backend/src/Services/ResponseApprovalService.cs +++ b/backend/src/Services/ResponseApprovalService.cs @@ -32,7 +32,8 @@ public static partial class Log public sealed class ResponseApprovalService( AppSettings appSettings, SigningService signingService, - IRequestExecutorResolver requestExecutorResolver, + IRequestExecutorProvider requestExecutorProvider, + IClock clock, ILogger logger ) { @@ -97,7 +98,7 @@ public async Task CreateResponseApproval(IData dataObject, Can } var (signature, fingerprint) = await signingService.SignData(response); return new ResponseApproval( - OffsetDateTime.UtcNow, + clock.GetUtcNow(), signature, fingerprint, query, @@ -134,7 +135,7 @@ CancellationToken cancellationToken .SetDocument(query) .SetVariableValues(variables) .Build(); - var requestExecutor = await requestExecutorResolver.GetRequestExecutorAsync(cancellationToken: cancellationToken); + var requestExecutor = await requestExecutorProvider.GetExecutorAsync(cancellationToken: cancellationToken); var executionResult = await requestExecutor.ExecuteAsync(operationRequest, cancellationToken); var response = executionResult.ToJson(withIndentations: false); return (query, JsonSerializer.SerializeToElement(variables), response); diff --git a/backend/src/Services/SigningService.cs b/backend/src/Services/SigningService.cs index 299411a2..08b297d1 100644 --- a/backend/src/Services/SigningService.cs +++ b/backend/src/Services/SigningService.cs @@ -171,6 +171,6 @@ private async Task ReceiveKey(string fingerprint) logger.ExecuteCommandOutput(output); logger.ExecuteCommandDiagnostics(diagnostics); logger.ExecuteCommandExitCode(process.ExitCode); - return (process.ExitCode == 0, output, diagnostics); + return (process.ExitCode is 0, output, diagnostics); } } \ No newline at end of file diff --git a/backend/src/Startup.cs b/backend/src/Startup.cs index c1f2c3ca..ab086e01 100644 --- a/backend/src/Startup.cs +++ b/backend/src/Startup.cs @@ -13,7 +13,6 @@ using Database.Enumerations; using Database.GraphQl; using Database.Services; -using HotChocolate.AspNetCore; using Laraue.EfCoreTriggers.PostgreSql.Extensions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.DataProtection; @@ -29,6 +28,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.OpenApi; +using NodaTime; using Npgsql; using OpenTelemetry.Exporter; using OpenTelemetry.Logs; @@ -52,10 +52,10 @@ IConfiguration configuration }) ?? throw new InvalidOperationException("Failed to get application settings from configuration."); + private readonly IClock _clock = SystemClock.Instance; + public void ConfigureServices(IServiceCollection services) { - AuthConfiguration.ConfigureServices(services, environment, _appSettings); - GraphQlConfiguration.ConfigureServices(services, environment); ConfigureDatabaseServices(services); ConfigureRequestResponseServices(services); // ConfigureSessionServices(services); // Not used @@ -75,9 +75,12 @@ public void ConfigureServices(IServiceCollection services) .AddDbContextCheck(); services.AddSingleton(_appSettings); services.AddSingleton(environment); + services.AddSingleton(_clock); // services.AddDatabaseDeveloperPageExceptionFilter(); ConfigureCustomServices(services); ConfigureApiRequests(services); + AuthConfiguration.ConfigureServices(services, environment, _appSettings, _clock); + GraphQlConfiguration.ConfigureServices(services, environment); } private static void ConfigureRequestResponseServices(IServiceCollection services) @@ -85,7 +88,6 @@ private static void ConfigureRequestResponseServices(IServiceCollection services // https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer#forwarded-headers-middleware-order services.Configure(_ => { - // TODO _.AllowedHosts = ... _.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | @@ -169,6 +171,7 @@ IServiceCollection services // { // _.AddAspNetCoreInstrumentation(); // _.AddHttpClientInstrumentation(); + // _.AddHotChocolateInstrumentation(); // _.AddOtlpExporter(_ => // { // _.Endpoint = _appSettings.OpenTelemetry.GrpcUri; @@ -251,6 +254,7 @@ private void ConfigureDatabaseServices(IServiceCollection services) // Configure the database-context options only once in // `AddPooledDbContextFactory` and not a second time in `AddDbContext` // as suggested in + // https://github.com/npgsql/efcore.pg/issues/3375#issuecomment-2509746639 services.AddPooledDbContextFactory(ConfigureDatabaseContext); // Database context as services are used by `OpenIddict`, see in // particular `AuthConfiguration`. @@ -330,11 +334,11 @@ public void Configure(WebApplication app) app.UseStaticFiles(); app.UseCookiePolicy(); // [SameSite cookies](https://learn.microsoft.com/en-us/aspnet/core/security/samesite) app.UseRouting(); - // TODO Do we really want this? See https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization?view=aspnetcore-5.0 + // [Localization](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization) app.UseRequestLocalization(_ => { - _.AddSupportedCultures("en-US", "de-DE"); - _.AddSupportedUICultures("en-US", "de-DE"); + _.AddSupportedCultures("en-US"); + _.AddSupportedUICultures("en-US"); _.SetDefaultCulture("en-US"); }); app.UseCors(); @@ -357,25 +361,6 @@ public void Configure(WebApplication app) _.WithOpenApiRoutePattern(OpenApiConstants.RoutePattern); }); app.MapGraphQL() - .WithOptions( - // https://chillicream.com/docs/hotchocolate/server/middleware - new GraphQLServerOptions - { - EnableSchemaRequests = true, - EnableGetRequests = false, - // AllowedGetOperations = AllowedGetOperations.Query - EnableMultipartRequests = false, - Tool = - { - DisableTelemetry = true, - Enable = true, // environment.IsDevelopment() - IncludeCookies = false, - GraphQLEndpoint = GraphQlConstants.EndpointPath, - HttpMethod = DefaultHttpMethod.Post, - Title = "GraphQL" - } - } - ) .RequireCors(GraphQlConstants.CorsPolicy); app.MapControllers(); app.MapHealthChecks("/health", diff --git a/backend/src/Utilities/FileHelpers.cs b/backend/src/Utilities/FileHelpers.cs index 42b4013b..1bca5e53 100644 --- a/backend/src/Utilities/FileHelpers.cs +++ b/backend/src/Utilities/FileHelpers.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.IO; -using System.Linq; using System.Net; using System.Reflection; using System.Threading.Tasks; @@ -42,7 +40,7 @@ ModelStateDictionary modelState formFile.FileName); // Check the file length. This check doesn't catch files that only have // a BOM as their content. - if (formFile.Length == 0) + if (formFile.Length is 0) { modelState.AddModelError(formFile.Name, $"{fieldDisplayName}({trustedFileNameForDisplay}) is empty."); @@ -55,7 +53,7 @@ ModelStateDictionary modelState // Check the content length in case the file's only // content was a BOM and the content is actually // empty after removing the BOM. - if (memoryStream.Length == 0) + if (memoryStream.Length is 0) { modelState.AddModelError(formFile.Name, $"{fieldDisplayName}({trustedFileNameForDisplay}) is empty."); @@ -85,7 +83,7 @@ ModelStateDictionary modelState { using var memoryStream = new MemoryStream(); await section.Body.CopyToAsync(memoryStream); - if (memoryStream.Length == 0) + if (memoryStream.Length is 0) { modelState.AddModelError("File", "The file is empty."); } diff --git a/backend/test/AuditableTests.cs b/backend/test/AuditableTests.cs new file mode 100644 index 00000000..a699a184 --- /dev/null +++ b/backend/test/AuditableTests.cs @@ -0,0 +1,75 @@ +using NodaTime; +using NodaTime.Testing; +using Microsoft.EntityFrameworkCore; +using NUnit.Framework; +using Database.Data; +using System; +using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; + +namespace Database.Tests; + +[TestFixture] +public sealed class AuditableTests +{ + private DbContextOptions _options = default!; + + [SetUp] + public void Setup() + { + _options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + } + + [Test] + [SuppressMessage("Naming", "CA1707")] + public async Task SaveChanges_SetsAndUpdatesTimestamps_UsingFakeClock() + { + // Arrange + var startInstant = Instant.FromUtc(2024, 1, 1, 10, 0); + var fakeClock = new FakeClock(startInstant); + using var context = new ApplicationDbContext(_options, fakeClock); + var entity = new User("Subject", "Name"); + // Act + context.Add(entity); + await context.SaveChangesAsync(); + // Assert + Assert.Multiple(() => + { + Assert.That(entity.CreatedAt, Is.EqualTo(startInstant.WithOffset(Offset.Zero).ToDateTimeOffset())); + Assert.That(entity.UpdatedAt, Is.EqualTo(startInstant.WithOffset(Offset.Zero).ToDateTimeOffset())); + }); + // Act + var duration = Duration.FromHours(1); + fakeClock.Advance(duration); + var updatedInstant = startInstant.Plus(duration); + entity.Update("New Name"); + await context.SaveChangesAsync(); + // Assert + Assert.Multiple(() => + { + Assert.That(entity.CreatedAt, Is.EqualTo(startInstant.WithOffset(Offset.Zero).ToDateTimeOffset()), "CreatedAt should not change on update."); + Assert.That(entity.UpdatedAt, Is.EqualTo(updatedInstant.WithOffset(Offset.Zero).ToDateTimeOffset()), "UpdatedAt should reflect the new fake time."); + }); + } + + // [Test] + // public async Task Remove_PerformsSoftDelete_AndSetsDeletedAt() + // { + // var deleteTime = Instant.FromUtc(2024, 1, 1, 15, 0); + // var fakeClock = new FakeClock(deleteTime); + // using var context = new ApplicationDbContext(_options, fakeClock); + // var entity = new YourModel { Name = "To Be Deleted" }; + // context.Add(entity); + // await context.SaveChangesAsync(); + // // Act + // context.Remove(entity); + // await context.SaveChangesAsync(); + // // Assert + // Assert.That(entity.DeletedAt, Is.EqualTo(deleteTime)); + // // Ensure it's hidden from normal queries + // var count = await context.YourModels.CountAsync(); + // Assert.That(count, Is.Zero); + // } +} \ No newline at end of file diff --git a/backend/test/Database.Tests.csproj b/backend/test/Database.Tests.csproj index 01e4126b..e052358d 100644 --- a/backend/test/Database.Tests.csproj +++ b/backend/test/Database.Tests.csproj @@ -9,21 +9,23 @@ - + - - - + + + + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + From 1f7329965198d30ff41006ce234358dc022627f5 Mon Sep 17 00:00:00 2001 From: Simon Wacker Date: Wed, 27 May 2026 19:27:21 +0200 Subject: [PATCH 2/8] Level-up the user interface --- backend/dotnet-tools.json | 4 +- .../src/ApiRequests/ComponentDataLoader.cs | 9 +- .../src/ApiRequests/DataFormatDataLoader.cs | 8 +- backend/src/ApiRequests/DatabaseDataLoader.cs | 64 + .../src/ApiRequests/InstitutionDataLoader.cs | 9 +- backend/src/ApiRequests/MethodDataLoader.cs | 9 +- .../OpenIdConnectApplicationDataLoader.cs | 9 +- .../ApiRequests/Queries/Components.graphql | 2 +- .../src/ApiRequests/Queries/Database.graphql | 19 - .../src/ApiRequests/Queries/Databases.graphql | 22 + .../ApiRequests/Queries/Institutions.graphql | 2 +- .../src/ApiRequests/Queries/Methods.graphql | 2 +- .../Queries/OpenIdConnectApplications.graphql | 1 + .../Queries/UpdateDatabase.graphql | 49 +- backend/src/ApiRequests/Queries/Users.graphql | 1 + .../Queries/VerifyDatabase.graphql | 27 + backend/src/ApiRequests/QueryDatabase.cs | 85 - backend/src/ApiRequests/UpdateDatabase.cs | 2 +- backend/src/ApiRequests/UserDataLoader.cs | 9 +- backend/src/ApiRequests/VerifyDatabase.cs | 86 + .../src/Configuration/GraphQlConfiguration.cs | 4 +- backend/src/Data/GetHttpsResource.cs | 2 +- backend/src/Database.csproj | 26 +- backend/src/GraphQl/AppliedMethodType.cs | 35 + .../CalorimetricDataFilterType.cs | 17 + .../Common/OpenEndedDateTimeRangeType.cs | 7 +- .../GraphQl/CrossDatabaseDataReferenceType.cs | 35 + .../GraphQl/DataApprovals/DataApprovalType.cs | 22 +- backend/src/GraphQl/DataX/DataDataLoaders.cs | 69 - backend/src/GraphQl/DataX/DataFilterType.cs | 17 + .../src/GraphQl/DataX/DataFilterTypeBase.cs | 2 +- backend/src/GraphQl/DataX/DataResolvers.cs | 26 + backend/src/GraphQl/DataX/DataType.cs | 16 + backend/src/GraphQl/DataX/DataTypeBase.cs | 30 +- .../src/GraphQl/DataX/GetHttpsResourceTree.cs | 5 +- .../src/GraphQl/Databases/DatabaseQueries.cs | 8 +- .../Databases/VerifyDatabaseMutation.cs | 42 + .../src/GraphQl/FileMetaInformationType.cs | 35 + .../GeometricDataX/GeometricDataFilterType.cs | 17 + .../GetHttpsResourceDataLoaders.cs | 55 + .../GetHttpsResourceResolvers.cs | 9 + .../GetHttpsResources/GetHttpsResourceType.cs | 11 + backend/src/GraphQl/GraphQlConstants.cs | 1 + .../HygrothermalDataFilterType.cs | 17 + .../LifeCycleDataX/LifeCycleDataFilterType.cs | 17 + backend/src/GraphQl/Methods/MethodQueries.cs | 6 +- .../OpticalDataX/OpticalDataFilterType.cs | 17 + .../PhotovoltaicDataFilterType.cs | 17 + .../src/GraphQl/References/ReferenceType.cs | 3 - ...ToTreeVertexAppliedConversionMethodType.cs | 35 + ...27161543_MakeEntitiesAuditable.Designer.cs | 3226 ++++++++++++++ .../20260527161543_MakeEntitiesAuditable.cs | 407 ++ .../ApplicationDbContextModelSnapshot.cs | 137 +- backend/src/Migrations/migrate.sql | 200 + ...o_20260527161543_MakeEntitiesAuditable.sql | 200 + ...ble_to_20260316165622_AddLifeCycleData.sql | 172 + backend/src/Startup.cs | 3 +- backend/test/Database.Tests.csproj | 2 +- .../GraphQlSchemaTests.IsUnchanged.snap | 1661 ++++--- frontend/codegen.ts | 95 +- .../components/ActiveFilterAndSortBar.tsx | 132 + frontend/components/CielabColorView.tsx | 19 + frontend/components/CodeView.tsx | 32 + frontend/components/CopyButton.tsx | 85 + frontend/components/Copyable.tsx | 26 + frontend/components/CopyableBlock.tsx | 53 + frontend/components/DateTimeX.tsx | 34 + frontend/components/DeleteButton.tsx | 63 + frontend/components/EditButton.tsx | 34 + frontend/components/EnumSelect.tsx | 52 + frontend/components/EnumTag.tsx | 16 + frontend/components/Float.tsx | 63 + frontend/components/Footer.tsx | 11 +- frontend/components/GnuPgKeyLink.tsx | 24 + frontend/components/Highlight.tsx | 19 - frontend/components/Iconize.tsx | 16 + frontend/components/Id.tsx | 5 + frontend/components/IdentifierItem.tsx | 95 + frontend/components/InlineList.tsx | 35 + frontend/components/JsonView.tsx | 36 + frontend/components/JumpToId.tsx | 47 + frontend/components/Layout.tsx | 49 +- frontend/components/NavBar.tsx | 191 +- frontend/components/PaginatedIdSelect.tsx | 163 + frontend/components/Pagination.tsx | 82 + frontend/components/QueryToolbar.tsx | 47 + frontend/components/Reference.tsx | 108 + frontend/components/SlideDown.tsx | 51 + frontend/components/UuidFormItem.tsx | 38 + frontend/components/data/DataPageHeader.tsx | 69 - frontend/components/data/DataSummary.tsx | 316 ++ .../data/calorimetric/CalorimetricData.tsx | 68 +- .../calorimetric/CalorimetricDataList.tsx | 24 + .../calorimetric/CalorimetricDataSummary.tsx | 47 + .../PaginatedCalorimetricData.tsx | 76 + .../data/geometric/GeometricData.tsx | 60 +- .../data/geometric/GeometricDataList.tsx | 24 + .../data/geometric/GeometricDataSummary.tsx | 32 + .../data/geometric/PaginatedGeometricData.tsx | 69 + .../data/hygrothermal/HygrothermalData.tsx | 57 +- .../hygrothermal/HygrothermalDataList.tsx | 24 + .../hygrothermal/HygrothermalDataSummary.tsx | 13 + .../PaginatedHygrothermalData.tsx | 62 + .../data/lifeCycle/LifeCycleData.tsx | 52 +- .../data/lifeCycle/LifeCycleDataList.tsx | 24 + .../data/lifeCycle/LifeCycleDataSummary.tsx | 13 + .../data/lifeCycle/PaginatedLifeCycleData.tsx | 62 + .../components/data/optical/OpticalData.tsx | 101 +- .../data/optical/OpticalDataList.tsx | 27 + .../data/optical/OpticalDataRibbon.tsx | 26 + .../data/optical/OpticalDataSummary.tsx | 106 + .../data/optical/PaginatedOpticalData.tsx | 140 + .../PaginatedPhotovoltaicData.tsx | 62 + .../data/photovoltaic/PhotovoltaicData.tsx | 57 +- .../photovoltaic/PhotovoltaicDataList.tsx | 24 + .../photovoltaic/PhotovoltaicDataSummary.tsx | 13 + frontend/components/databases/Database.tsx | 82 +- .../components/databases/DatabaseSummary.tsx | 86 + .../components/databases/UpdateDatabase.tsx | 127 +- .../components/databases/VerifyDatabase.tsx | 47 + frontend/components/entities/EntityItem.tsx | 10 + frontend/components/entities/EntityLink.tsx | 31 + frontend/components/entities/EntityList.tsx | 58 + .../components/entities/EntitySummary.tsx | 114 + .../components/entities/PaginatedEntities.tsx | 379 ++ .../filtering/BaseFilterSubform.tsx | 56 + .../filtering/EnumFilterSubform.tsx | 58 + .../components/filtering/FilterSubform.tsx | 67 + .../filtering/FloatFilterSubform.tsx | 78 + .../components/filtering/IntFilterSubform.tsx | 77 + .../filtering/ListFilterSubform.tsx | 35 + .../filtering/ObjectFilterSubform.tsx | 58 + .../filtering/StringFilterSubform.tsx | 52 + .../components/filtering/UrlFilterSubform.tsx | 50 + .../filtering/UuidFilterSubform.tsx | 40 + .../components/methods/AppliedMethodView.tsx | 82 + frontend/components/sorting/SortSubform.tsx | 35 + frontend/jest.config.js | 13 - frontend/lib/apollo.ts | 23 + frontend/lib/array.ts | 59 + frontend/lib/assert.ts | 3 + frontend/lib/connection.ts | 12 + frontend/lib/filter.ts | 507 +++ frontend/lib/form.ts | 60 +- frontend/lib/freeTextFilter.tsx | 107 - frontend/lib/hooks/useDebounce.ts | 26 + frontend/lib/hooks/useMutationHandler.ts | 124 + frontend/lib/hooks/usePaginatedQuery.ts | 178 + frontend/lib/hooks/usePreviousNonNull.ts | 9 + frontend/lib/hooks/useQueryHandler.ts | 18 + frontend/lib/sort.ts | 36 + frontend/lib/string.ts | 69 + frontend/lib/table.tsx | 691 --- frontend/next.config.ts | 2 +- frontend/package.json | 43 +- frontend/pages/_app.tsx | 1 + frontend/pages/data/calorimetric/[uuid].tsx | 11 +- frontend/pages/data/calorimetric/index.tsx | 328 +- frontend/pages/data/create.tsx | 2 +- frontend/pages/data/geometric/[uuid].tsx | 13 +- frontend/pages/data/geometric/index.tsx | 265 +- frontend/pages/data/hygrothermal/[uuid].tsx | 11 +- frontend/pages/data/hygrothermal/index.tsx | 250 +- frontend/pages/data/life-cycle/[uuid].tsx | 11 +- frontend/pages/data/life-cycle/index.tsx | 250 +- frontend/pages/data/optical/[uuid].tsx | 11 +- frontend/pages/data/optical/index.tsx | 465 +- frontend/pages/data/photovoltaic/[uuid].tsx | 11 +- frontend/pages/data/photovoltaic/index.tsx | 250 +- frontend/pages/index.tsx | 24 +- frontend/pages/unauthorized.tsx | 16 +- frontend/pages/upload-file.tsx | 2 +- frontend/pages/user-info.tsx | 13 +- frontend/paths.ts | 145 +- frontend/queries/common.graphql | 25 + frontend/queries/currentUser.graphql | 1 + frontend/queries/data.graphql | 499 ++- frontend/queries/databases.graphql | 48 +- .../test/__snapshots__/index.test.tsx.snap | 33 - frontend/test/index.test.tsx | 37 - frontend/type-defs.graphqls | 2573 ++++++----- frontend/yarn.lock | 3938 ++++++----------- 182 files changed, 14810 insertions(+), 8424 deletions(-) create mode 100644 backend/src/ApiRequests/DatabaseDataLoader.cs delete mode 100644 backend/src/ApiRequests/Queries/Database.graphql create mode 100644 backend/src/ApiRequests/Queries/Databases.graphql create mode 100644 backend/src/ApiRequests/Queries/VerifyDatabase.graphql delete mode 100644 backend/src/ApiRequests/QueryDatabase.cs create mode 100644 backend/src/ApiRequests/VerifyDatabase.cs create mode 100644 backend/src/GraphQl/AppliedMethodType.cs create mode 100644 backend/src/GraphQl/CrossDatabaseDataReferenceType.cs delete mode 100644 backend/src/GraphQl/DataX/DataDataLoaders.cs create mode 100644 backend/src/GraphQl/Databases/VerifyDatabaseMutation.cs create mode 100644 backend/src/GraphQl/FileMetaInformationType.cs create mode 100644 backend/src/GraphQl/ToTreeVertexAppliedConversionMethodType.cs create mode 100644 backend/src/Migrations/20260527161543_MakeEntitiesAuditable.Designer.cs create mode 100644 backend/src/Migrations/20260527161543_MakeEntitiesAuditable.cs create mode 100644 backend/src/Migrations/migrate_from_20260316165622_AddLifeCycleData_to_20260527161543_MakeEntitiesAuditable.sql create mode 100644 backend/src/Migrations/rollback_from_20260527161543_MakeEntitiesAuditable_to_20260316165622_AddLifeCycleData.sql create mode 100644 frontend/components/ActiveFilterAndSortBar.tsx create mode 100644 frontend/components/CielabColorView.tsx create mode 100644 frontend/components/CodeView.tsx create mode 100644 frontend/components/CopyButton.tsx create mode 100644 frontend/components/Copyable.tsx create mode 100644 frontend/components/CopyableBlock.tsx create mode 100644 frontend/components/DateTimeX.tsx create mode 100644 frontend/components/DeleteButton.tsx create mode 100644 frontend/components/EditButton.tsx create mode 100644 frontend/components/EnumSelect.tsx create mode 100644 frontend/components/EnumTag.tsx create mode 100644 frontend/components/Float.tsx create mode 100644 frontend/components/GnuPgKeyLink.tsx delete mode 100644 frontend/components/Highlight.tsx create mode 100644 frontend/components/Iconize.tsx create mode 100644 frontend/components/Id.tsx create mode 100644 frontend/components/IdentifierItem.tsx create mode 100644 frontend/components/InlineList.tsx create mode 100644 frontend/components/JsonView.tsx create mode 100644 frontend/components/JumpToId.tsx create mode 100644 frontend/components/PaginatedIdSelect.tsx create mode 100644 frontend/components/Pagination.tsx create mode 100644 frontend/components/QueryToolbar.tsx create mode 100644 frontend/components/Reference.tsx create mode 100644 frontend/components/SlideDown.tsx create mode 100644 frontend/components/UuidFormItem.tsx delete mode 100644 frontend/components/data/DataPageHeader.tsx create mode 100644 frontend/components/data/DataSummary.tsx create mode 100644 frontend/components/data/calorimetric/CalorimetricDataList.tsx create mode 100644 frontend/components/data/calorimetric/CalorimetricDataSummary.tsx create mode 100644 frontend/components/data/calorimetric/PaginatedCalorimetricData.tsx create mode 100644 frontend/components/data/geometric/GeometricDataList.tsx create mode 100644 frontend/components/data/geometric/GeometricDataSummary.tsx create mode 100644 frontend/components/data/geometric/PaginatedGeometricData.tsx create mode 100644 frontend/components/data/hygrothermal/HygrothermalDataList.tsx create mode 100644 frontend/components/data/hygrothermal/HygrothermalDataSummary.tsx create mode 100644 frontend/components/data/hygrothermal/PaginatedHygrothermalData.tsx create mode 100644 frontend/components/data/lifeCycle/LifeCycleDataList.tsx create mode 100644 frontend/components/data/lifeCycle/LifeCycleDataSummary.tsx create mode 100644 frontend/components/data/lifeCycle/PaginatedLifeCycleData.tsx create mode 100644 frontend/components/data/optical/OpticalDataList.tsx create mode 100644 frontend/components/data/optical/OpticalDataRibbon.tsx create mode 100644 frontend/components/data/optical/OpticalDataSummary.tsx create mode 100644 frontend/components/data/optical/PaginatedOpticalData.tsx create mode 100644 frontend/components/data/photovoltaic/PaginatedPhotovoltaicData.tsx create mode 100644 frontend/components/data/photovoltaic/PhotovoltaicDataList.tsx create mode 100644 frontend/components/data/photovoltaic/PhotovoltaicDataSummary.tsx create mode 100644 frontend/components/databases/DatabaseSummary.tsx create mode 100644 frontend/components/databases/VerifyDatabase.tsx create mode 100644 frontend/components/entities/EntityItem.tsx create mode 100644 frontend/components/entities/EntityLink.tsx create mode 100644 frontend/components/entities/EntityList.tsx create mode 100644 frontend/components/entities/EntitySummary.tsx create mode 100644 frontend/components/entities/PaginatedEntities.tsx create mode 100644 frontend/components/filtering/BaseFilterSubform.tsx create mode 100644 frontend/components/filtering/EnumFilterSubform.tsx create mode 100644 frontend/components/filtering/FilterSubform.tsx create mode 100644 frontend/components/filtering/FloatFilterSubform.tsx create mode 100644 frontend/components/filtering/IntFilterSubform.tsx create mode 100644 frontend/components/filtering/ListFilterSubform.tsx create mode 100644 frontend/components/filtering/ObjectFilterSubform.tsx create mode 100644 frontend/components/filtering/StringFilterSubform.tsx create mode 100644 frontend/components/filtering/UrlFilterSubform.tsx create mode 100644 frontend/components/filtering/UuidFilterSubform.tsx create mode 100644 frontend/components/methods/AppliedMethodView.tsx create mode 100644 frontend/components/sorting/SortSubform.tsx delete mode 100644 frontend/jest.config.js create mode 100644 frontend/lib/array.ts create mode 100644 frontend/lib/assert.ts create mode 100644 frontend/lib/connection.ts create mode 100644 frontend/lib/filter.ts delete mode 100644 frontend/lib/freeTextFilter.tsx create mode 100644 frontend/lib/hooks/useDebounce.ts create mode 100644 frontend/lib/hooks/useMutationHandler.ts create mode 100644 frontend/lib/hooks/usePaginatedQuery.ts create mode 100644 frontend/lib/hooks/usePreviousNonNull.ts create mode 100644 frontend/lib/hooks/useQueryHandler.ts create mode 100644 frontend/lib/sort.ts create mode 100644 frontend/lib/string.ts delete mode 100644 frontend/lib/table.tsx create mode 100644 frontend/queries/common.graphql delete mode 100644 frontend/test/__snapshots__/index.test.tsx.snap delete mode 100644 frontend/test/index.test.tsx diff --git a/backend/dotnet-tools.json b/backend/dotnet-tools.json index 972eb2a4..d67bced7 100644 --- a/backend/dotnet-tools.json +++ b/backend/dotnet-tools.json @@ -31,7 +31,7 @@ "rollForward": false }, "dotnet-script": { - "version": "2.0.0", + "version": "2.0.1", "commands": [ "dotnet-script" ], @@ -66,7 +66,7 @@ "rollForward": false }, "jetbrains.resharper.globaltools": { - "version": "2026.1.1", + "version": "2026.1.2", "commands": [ "jb" ], diff --git a/backend/src/ApiRequests/ComponentDataLoader.cs b/backend/src/ApiRequests/ComponentDataLoader.cs index edc0ecfe..c585a9b7 100644 --- a/backend/src/ApiRequests/ComponentDataLoader.cs +++ b/backend/src/ApiRequests/ComponentDataLoader.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Database.Services; using GreenDonut; +using HotChocolate; using static Database.ApiRequests.QueryByIdDataLoader; namespace Database.ApiRequests; @@ -16,8 +17,12 @@ public static Uri GetGraphQlEndpoint(AppSettings appSettings) => QueryByIdDataLoader.GetGraphQlEndpoint(appSettings); public sealed record Component( - Guid Id - ) : IIdNode; + [property: GraphQLIgnore] Guid Id, + string Name + ) : IIdNode + { + public Guid Uuid => Id; + } private sealed record ComponentsData( Connection? Connection diff --git a/backend/src/ApiRequests/DataFormatDataLoader.cs b/backend/src/ApiRequests/DataFormatDataLoader.cs index 09350273..762d630b 100644 --- a/backend/src/ApiRequests/DataFormatDataLoader.cs +++ b/backend/src/ApiRequests/DataFormatDataLoader.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Database.Services; using GreenDonut; +using HotChocolate; using static Database.ApiRequests.QueryByIdDataLoader; namespace Database.ApiRequests; @@ -16,12 +17,15 @@ public static Uri GetGraphQlEndpoint(AppSettings appSettings) => QueryByIdDataLoader.GetGraphQlEndpoint(appSettings); public sealed record DataFormat( - Guid Id, + [property: GraphQLIgnore] Guid Id, string Name, string? Extension, string MediaType, Uri? SchemaLocator - ) : IIdNode; + ) : IIdNode + { + public Guid Uuid => Id; + } private sealed record DataFormatsData( Connection? Connection diff --git a/backend/src/ApiRequests/DatabaseDataLoader.cs b/backend/src/ApiRequests/DatabaseDataLoader.cs new file mode 100644 index 00000000..35374435 --- /dev/null +++ b/backend/src/ApiRequests/DatabaseDataLoader.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Database.Services; +using GreenDonut; +using HotChocolate; +using static Database.ApiRequests.QueryByIdDataLoader; + +namespace Database.ApiRequests; + +public sealed class DatabaseDataLoader +{ + private const string QueryFileName = "Databases.graphql"; + + public static Uri GetGraphQlEndpoint(AppSettings appSettings) => + QueryByIdDataLoader.GetGraphQlEndpoint(appSettings); + + public sealed record Database( + [property: GraphQLIgnore] Guid Id, + string Name, + string Description, + Uri Locator, + DatabaseVerificationState VerificationState, + string VerificationCode, + DatabaseOperatorEdge Operator, + bool IsAuthorizedToUpdateNode, + bool IsAuthorizedToVerifyNode + ) : IIdNode + { + public Guid Uuid => Id; + } + + public enum DatabaseVerificationState + { + PENDING, + VERIFIED + } + + public sealed record DatabaseOperatorEdge( + InstitutionDataLoader.Institution Node + ); + + private sealed record DatabasesData( + Connection? Connection + ) : IConnectionData; + + [DataLoader] + public static Task> GetDatabaseByIdAsync( + IReadOnlyList componentIds, + ApiRequestService apiRequestService, + AppSettings appSettings, + CancellationToken cancellationToken + ) + { + return QueryByIdDataLoader.GetByIdAsync( + componentIds, + [QueryFileName], + apiRequestService, + appSettings, + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/ApiRequests/InstitutionDataLoader.cs b/backend/src/ApiRequests/InstitutionDataLoader.cs index 9d431466..37aca9ae 100644 --- a/backend/src/ApiRequests/InstitutionDataLoader.cs +++ b/backend/src/ApiRequests/InstitutionDataLoader.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Database.Services; using GreenDonut; +using HotChocolate; using static Database.ApiRequests.QueryByIdDataLoader; namespace Database.ApiRequests; @@ -16,8 +17,12 @@ public static Uri GetGraphQlEndpoint(AppSettings appSettings) => QueryByIdDataLoader.GetGraphQlEndpoint(appSettings); public sealed record Institution( - Guid Id - ) : IIdNode; + [property: GraphQLIgnore] Guid Id, + string Name + ) : IIdNode + { + public Guid Uuid => Id; + } private sealed record InstitutionsData( Connection? Connection diff --git a/backend/src/ApiRequests/MethodDataLoader.cs b/backend/src/ApiRequests/MethodDataLoader.cs index 20808157..10d1f9ed 100644 --- a/backend/src/ApiRequests/MethodDataLoader.cs +++ b/backend/src/ApiRequests/MethodDataLoader.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Database.Services; using GreenDonut; +using HotChocolate; using static Database.ApiRequests.QueryByIdDataLoader; namespace Database.ApiRequests; @@ -16,8 +17,12 @@ public static Uri GetGraphQlEndpoint(AppSettings appSettings) => QueryByIdDataLoader.GetGraphQlEndpoint(appSettings); public sealed record Method( - Guid Id - ) : IIdNode; + [property: GraphQLIgnore] Guid Id, + string Name + ) : IIdNode + { + public Guid Uuid => Id; + } private sealed record MethodsData( Connection? Connection diff --git a/backend/src/ApiRequests/OpenIdConnectApplicationDataLoader.cs b/backend/src/ApiRequests/OpenIdConnectApplicationDataLoader.cs index 7d1b4144..fc13548c 100644 --- a/backend/src/ApiRequests/OpenIdConnectApplicationDataLoader.cs +++ b/backend/src/ApiRequests/OpenIdConnectApplicationDataLoader.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Database.Services; using GreenDonut; +using HotChocolate; using static Database.ApiRequests.QueryByIdDataLoader; namespace Database.ApiRequests; @@ -16,8 +17,12 @@ public static Uri GetGraphQlEndpoint(AppSettings appSettings) => QueryByIdDataLoader.GetGraphQlEndpoint(appSettings); public sealed record OpenIdConnectApplication( - string Id - ) : IIdNode; + [property: GraphQLIgnore] string Id, + string Name + ) : IIdNode + { + string ClientId => Id; + } private sealed record OpenIdConnectApplicationsData( Connection? Connection diff --git a/backend/src/ApiRequests/Queries/Components.graphql b/backend/src/ApiRequests/Queries/Components.graphql index 610926ea..4c680835 100644 --- a/backend/src/ApiRequests/Queries/Components.graphql +++ b/backend/src/ApiRequests/Queries/Components.graphql @@ -3,8 +3,8 @@ query Components($ids: [Uuid!]!) { edges { node { id: uuid + name } } } } - diff --git a/backend/src/ApiRequests/Queries/Database.graphql b/backend/src/ApiRequests/Queries/Database.graphql deleted file mode 100644 index 5ba53c63..00000000 --- a/backend/src/ApiRequests/Queries/Database.graphql +++ /dev/null @@ -1,19 +0,0 @@ -query Database( - $id: Uuid! -) { - database(id: $id) { - uuid - name - description - locator - verificationState - verificationCode - operator { - node { - uuid - } - } - isAuthorizedToUpdateNode - isAuthorizedToVerifyNode - } -} \ No newline at end of file diff --git a/backend/src/ApiRequests/Queries/Databases.graphql b/backend/src/ApiRequests/Queries/Databases.graphql new file mode 100644 index 00000000..5585f6ed --- /dev/null +++ b/backend/src/ApiRequests/Queries/Databases.graphql @@ -0,0 +1,22 @@ +query Databases($ids: [Uuid!]!) { + connection: databases(where: { id: { in: $ids } }) { + edges { + node { + id: uuid + name + description + locator + verificationState + verificationCode + operator { + node { + id: uuid + name + } + } + isAuthorizedToUpdateNode + isAuthorizedToVerifyNode + } + } + } +} diff --git a/backend/src/ApiRequests/Queries/Institutions.graphql b/backend/src/ApiRequests/Queries/Institutions.graphql index d81a0594..59dcc7ce 100644 --- a/backend/src/ApiRequests/Queries/Institutions.graphql +++ b/backend/src/ApiRequests/Queries/Institutions.graphql @@ -3,8 +3,8 @@ query Institutions($ids: [Uuid!]!) { edges { node { id: uuid + name } } } } - diff --git a/backend/src/ApiRequests/Queries/Methods.graphql b/backend/src/ApiRequests/Queries/Methods.graphql index 05b068cd..d7ea1fd2 100644 --- a/backend/src/ApiRequests/Queries/Methods.graphql +++ b/backend/src/ApiRequests/Queries/Methods.graphql @@ -3,8 +3,8 @@ query Methods($ids: [Uuid!]!) { edges { node { id: uuid + name } } } } - diff --git a/backend/src/ApiRequests/Queries/OpenIdConnectApplications.graphql b/backend/src/ApiRequests/Queries/OpenIdConnectApplications.graphql index 932ffec3..8eb9c946 100644 --- a/backend/src/ApiRequests/Queries/OpenIdConnectApplications.graphql +++ b/backend/src/ApiRequests/Queries/OpenIdConnectApplications.graphql @@ -3,6 +3,7 @@ query OpenIdConnectApplications($ids: [String!]!) { edges { node { id: clientId + displayName } } } diff --git a/backend/src/ApiRequests/Queries/UpdateDatabase.graphql b/backend/src/ApiRequests/Queries/UpdateDatabase.graphql index 05c7eba1..fddff65e 100644 --- a/backend/src/ApiRequests/Queries/UpdateDatabase.graphql +++ b/backend/src/ApiRequests/Queries/UpdateDatabase.graphql @@ -1,26 +1,27 @@ -mutation UpdateDatabase( - $input: UpdateDatabaseInput! -) { - updateDatabase(input: $input) { - database { - uuid - name - description - locator - verificationState - verificationCode - operator { - node { - uuid - } - } - isAuthorizedToUpdateNode - isAuthorizedToVerifyNode - } - errors { - code - message - path +mutation UpdateDatabase($input: UpdateDatabaseInput!) { + updateDatabase(input: $input) { + database { + id + uuid + name + description + locator + verificationState + verificationCode + operator { + node { + id + uuid + name } + } + isAuthorizedToUpdateNode + isAuthorizedToVerifyNode + } + errors { + code + message + path } -} \ No newline at end of file + } +} diff --git a/backend/src/ApiRequests/Queries/Users.graphql b/backend/src/ApiRequests/Queries/Users.graphql index 09a0a69b..ebdea272 100644 --- a/backend/src/ApiRequests/Queries/Users.graphql +++ b/backend/src/ApiRequests/Queries/Users.graphql @@ -3,6 +3,7 @@ query Users($ids: [Uuid!]!) { edges { node { id: uuid + name } } } diff --git a/backend/src/ApiRequests/Queries/VerifyDatabase.graphql b/backend/src/ApiRequests/Queries/VerifyDatabase.graphql new file mode 100644 index 00000000..dd97f77f --- /dev/null +++ b/backend/src/ApiRequests/Queries/VerifyDatabase.graphql @@ -0,0 +1,27 @@ +mutation VerifyDatabase($input: VerifyDatabaseInput!) { + verifyDatabase(input: $input) { + database { + id + uuid + name + description + locator + verificationState + verificationCode + operator { + node { + id + uuid + name + } + } + isAuthorizedToUpdateNode + isAuthorizedToVerifyNode + } + errors { + code + message + path + } + } +} diff --git a/backend/src/ApiRequests/QueryDatabase.cs b/backend/src/ApiRequests/QueryDatabase.cs deleted file mode 100644 index 0a4953d8..00000000 --- a/backend/src/ApiRequests/QueryDatabase.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Database.Logging; -using Database.Services; -using GraphQL; -using Microsoft.Extensions.Logging; - -namespace Database.ApiRequests; - -public static partial class Log -{ - [LoggerMessage( - Level = LogLevel.Error, - Message = "Response contains errors.") - ] - internal static partial void ResponseErrors( - this ILogger logger, - [TagProvider(typeof(GraphQlErrorsTagProvider), nameof(GraphQlErrorsTagProvider.RecordTags))] GraphQLError[] errors - ); -} - -public sealed class QueryDatabase( - AppSettings appSettings, - ApiRequestService apiRequestService, - ILogger logger -) -{ - private const string QueryFileName = "Database.graphql"; - - public Uri GetGraphQlEndpoint => - appSettings.MetabaseGraphQlEndpoint; - - public sealed record Database( - Guid Uuid, - string Name, - string Description, - Uri Locator, - DatabaseVerificationState VerificationState, - string VerificationCode, - DatabaseOperatorEdge Operator, - bool IsAuthorizedToUpdateNode, - bool IsAuthorizedToVerifyNode - ); - - public enum DatabaseVerificationState - { - PENDING, - VERIFIED - } - - public sealed record DatabaseOperatorEdge( - Institution Node - ); - - public sealed record Institution( - Guid Uuid - ); - - private sealed record DatabaseData(Database? Database); - - public async Task Do( - Guid databaseId, - CancellationToken cancellationToken - ) - { - var response = (await apiRequestService.QueryGraphQl( - GetGraphQlEndpoint, - new GraphQLRequest( - await GraphQlQueryHelpers.Construct(QueryFileName), - new - { - id = databaseId - }, - "Database" - ), - cancellationToken - )); - if (response.Errors is not null) - { - logger.ResponseErrors(response.Errors); - } - return response.Data.Database; ; - } -} \ No newline at end of file diff --git a/backend/src/ApiRequests/UpdateDatabase.cs b/backend/src/ApiRequests/UpdateDatabase.cs index e876b2a2..4e842b34 100644 --- a/backend/src/ApiRequests/UpdateDatabase.cs +++ b/backend/src/ApiRequests/UpdateDatabase.cs @@ -41,7 +41,7 @@ Uri Locator ); public sealed record UpdateDatabasePayload( - QueryDatabase.Database? Database, + DatabaseDataLoader.Database? Database, IReadOnlyList? Errors ); diff --git a/backend/src/ApiRequests/UserDataLoader.cs b/backend/src/ApiRequests/UserDataLoader.cs index be2ff55a..bffa2e9d 100644 --- a/backend/src/ApiRequests/UserDataLoader.cs +++ b/backend/src/ApiRequests/UserDataLoader.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Database.Services; using GreenDonut; +using HotChocolate; using static Database.ApiRequests.QueryByIdDataLoader; namespace Database.ApiRequests; @@ -16,8 +17,12 @@ public static Uri GetGraphQlEndpoint(AppSettings appSettings) => QueryByIdDataLoader.GetGraphQlEndpoint(appSettings); public sealed record User( - Guid Id - ) : IIdNode; + [property: GraphQLIgnore] Guid Id, + string Name + ) : IIdNode + { + public Guid Uuid => Id; + } private sealed record UsersData( Connection? Connection diff --git a/backend/src/ApiRequests/VerifyDatabase.cs b/backend/src/ApiRequests/VerifyDatabase.cs new file mode 100644 index 00000000..e85efa6b --- /dev/null +++ b/backend/src/ApiRequests/VerifyDatabase.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Database.Logging; +using Database.Services; +using GraphQL; +using Microsoft.Extensions.Logging; + +namespace Database.ApiRequests; + +public static partial class Log +{ + [LoggerMessage( + Level = LogLevel.Error, + Message = "Response contains errors.") + ] + internal static partial void ResponseErrors( + this ILogger logger, + [TagProvider(typeof(GraphQlErrorsTagProvider), nameof(GraphQlErrorsTagProvider.RecordTags))] GraphQLError[] errors + ); +} + +public sealed class VerifyDatabase( + AppSettings appSettings, + ApiRequestService apiRequestService, + ILogger logger +) +{ + private const string VerifyDatabaseFileName = "VerifyDatabase.graphql"; + + public Uri GetGraphQlEndpoint => + appSettings.MetabaseGraphQlEndpoint; + + public sealed record VerifyDatabaseInput( + Guid DatabaseId + ); + + public sealed record VerifyDatabasePayload( + DatabaseDataLoader.Database? Database, + IReadOnlyList? Errors + ); + + [SuppressMessage("Naming", "CA1707")] + public enum VerifyDatabaseErrorCode + { + UNKNOWN, + UNAUTHORIZED, + UNKNOWN_DATABASE + } + + public sealed record VerifyDatabaseError( + VerifyDatabaseErrorCode Code, + string Message, + IReadOnlyList Path + ); + + private sealed record VerifyDatabaseData(VerifyDatabasePayload? VerifyDatabase); + + public async Task Do( + VerifyDatabaseInput verifyDatabaseInput, + CancellationToken cancellationToken + ) + { + var response = (await apiRequestService.QueryGraphQl( + GetGraphQlEndpoint, + new GraphQLRequest( + await GraphQlQueryHelpers.Construct( + VerifyDatabaseFileName + ), + new + { + input = verifyDatabaseInput + }, + "VerifyDatabase" + ), + cancellationToken + )); + if (response.Errors is not null) + { + logger.ResponseErrors(response.Errors); + } + return response.Data.VerifyDatabase; + } +} \ No newline at end of file diff --git a/backend/src/Configuration/GraphQlConfiguration.cs b/backend/src/Configuration/GraphQlConfiguration.cs index f485fdbd..c2d1d9e8 100644 --- a/backend/src/Configuration/GraphQlConfiguration.cs +++ b/backend/src/Configuration/GraphQlConfiguration.cs @@ -129,8 +129,8 @@ IWebHostEnvironment environment ) .ModifyCostOptions(_ => { - _.MaxFieldCost = 10000; - _.MaxTypeCost = 10000; + _.MaxFieldCost = 12000; + _.MaxTypeCost = 12000; } ) // Configure diff --git a/backend/src/Data/GetHttpsResource.cs b/backend/src/Data/GetHttpsResource.cs index e7537619..f14fe843 100644 --- a/backend/src/Data/GetHttpsResource.cs +++ b/backend/src/Data/GetHttpsResource.cs @@ -146,7 +146,7 @@ private void AssertThatExactlyOneDataIdIsNonNull() public ICollection ArchivedFilesMetaInformation { get; private set; } = []; // Note that at least one data ID is always present. So `Guid.Empty` will never be used. - [NotMapped] + [Projectable] public Guid DataId => CalorimetricDataId ?? GeometricDataId ?? HygrothermalDataId ?? LifeCycleDataId ?? OpticalDataId ?? PhotovoltaicDataId ?? Guid.Empty; [NotMapped] diff --git a/backend/src/Database.csproj b/backend/src/Database.csproj index baa7653e..601ea1f0 100644 --- a/backend/src/Database.csproj +++ b/backend/src/Database.csproj @@ -16,17 +16,17 @@ - - - - - - - - - - - + + + + + + + + + + + @@ -41,8 +41,8 @@ - - + + diff --git a/backend/src/GraphQl/AppliedMethodType.cs b/backend/src/GraphQl/AppliedMethodType.cs new file mode 100644 index 00000000..7b623ade --- /dev/null +++ b/backend/src/GraphQl/AppliedMethodType.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using Database.ApiRequests; +using Database.Data; +using Database.Extensions; +using HotChocolate; +using HotChocolate.Types; + +namespace Database.GraphQl; + +public sealed class AppliedMethodType + : ObjectType +{ + protected override void Configure( + IObjectTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor + .Field(nameof(AppliedMethod.MethodId)[..^2].FirstCharToLower()) + .Type>() + .Cost(3) + .ResolveWith(_ => Resolvers.GetMethodAsync(default!, default!)); + } + + private sealed class Resolvers + { + public static Task GetMethodAsync( + [Parent] AppliedMethod parent, + IMethodByIdDataLoader byId + ) + { + return byId.LoadAsync(parent.MethodId); + } + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataFilterType.cs b/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataFilterType.cs index 90a22c50..14a711c6 100644 --- a/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataFilterType.cs +++ b/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataFilterType.cs @@ -15,5 +15,22 @@ IFilterInputTypeDescriptor descriptor descriptor.Name(nameof(CalorimetricDataFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); descriptor.Field(x => x.GValues); descriptor.Field(x => x.UValues); + + // TODO Why are the fields below not included by `base.Configure` above? + // AuditableEntityFilterType.Configure + descriptor.Field(x => x.Id); + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); + // DataFilterTypeBase.Configure + descriptor.Field(x => x.UserId); + descriptor.Field(x => x.Locale); + descriptor.Field(x => x.Name); + descriptor.Field(x => x.Description); + descriptor.Field(x => x.ComponentId); + descriptor.Field(x => x.CreatorId); + descriptor.Field(x => x.AppliedMethod); + descriptor.Field(x => x.Approvals); + descriptor.Field(x => x.Resources); + descriptor.Field(x => x.Warnings); } } \ No newline at end of file diff --git a/backend/src/GraphQl/Common/OpenEndedDateTimeRangeType.cs b/backend/src/GraphQl/Common/OpenEndedDateTimeRangeType.cs index c0989874..20108396 100644 --- a/backend/src/GraphQl/Common/OpenEndedDateTimeRangeType.cs +++ b/backend/src/GraphQl/Common/OpenEndedDateTimeRangeType.cs @@ -1,6 +1,7 @@ using HotChocolate.Types; using NodaTime; using NpgsqlTypes; +using DateTimeType = HotChocolate.Types.NodaTime.DateTimeType; namespace Database.GraphQl.Common; @@ -12,10 +13,7 @@ IObjectTypeDescriptor> descriptor ) { descriptor.BindFieldsExplicitly(); - - var suffixedName = nameof(OpenEndedDateTimeRangeType); - descriptor.Name(suffixedName[..^"Type".Length]); - + descriptor.Name(nameof(OpenEndedDateTimeRangeType)[..^"Type".Length]); descriptor .Field("from") .Type() @@ -27,7 +25,6 @@ IObjectTypeDescriptor> descriptor : range.LowerBound; } ); - descriptor .Field("until") .Type() diff --git a/backend/src/GraphQl/CrossDatabaseDataReferenceType.cs b/backend/src/GraphQl/CrossDatabaseDataReferenceType.cs new file mode 100644 index 00000000..32c929b9 --- /dev/null +++ b/backend/src/GraphQl/CrossDatabaseDataReferenceType.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using Database.ApiRequests; +using Database.Data; +using Database.Extensions; +using HotChocolate; +using HotChocolate.Types; + +namespace Database.GraphQl; + +public sealed class CrossDatabaseDataReferenceType + : ObjectType +{ + protected override void Configure( + IObjectTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor + .Field(nameof(CrossDatabaseDataReference.DatabaseId)[..^2].FirstCharToLower()) + .Type>() + .Cost(3) + .ResolveWith(_ => Resolvers.GetDatabaseAsync(default!, default!)); + } + + private sealed class Resolvers + { + public static Task GetDatabaseAsync( + [Parent] CrossDatabaseDataReference parent, + IDatabaseByIdDataLoader byId + ) + { + return byId.LoadAsync(parent.DatabaseId); + } + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/DataApprovals/DataApprovalType.cs b/backend/src/GraphQl/DataApprovals/DataApprovalType.cs index 9d306bbf..ff19a04b 100644 --- a/backend/src/GraphQl/DataApprovals/DataApprovalType.cs +++ b/backend/src/GraphQl/DataApprovals/DataApprovalType.cs @@ -1,5 +1,9 @@ +using System.Threading.Tasks; +using Database.ApiRequests; using Database.Data; +using Database.Extensions; using Database.GraphQl.References; +using HotChocolate; using HotChocolate.Types; namespace Database.GraphQl.DataApprovals; @@ -11,11 +15,27 @@ protected override void Configure(IObjectTypeDescriptor descriptor { descriptor .Field(t => t.Statement) - .Type() + .Type>() .Resolve(context => context .Parent() .Statement .TheReference ); + descriptor + .Field(nameof(DataApproval.ApproverId)[..^2].FirstCharToLower()) + .Type>() + .Cost(3) + .ResolveWith(_ => Resolvers.GetApproverAsync(default!, default!)); + } + + private sealed class Resolvers + { + public static Task GetApproverAsync( + [Parent] DataApproval parent, + IInstitutionByIdDataLoader byId + ) + { + return byId.LoadAsync(parent.ApproverId); + } } } \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/DataDataLoaders.cs b/backend/src/GraphQl/DataX/DataDataLoaders.cs deleted file mode 100644 index 26ea47c6..00000000 --- a/backend/src/GraphQl/DataX/DataDataLoaders.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Linq; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using GreenDonut; -using GreenDonut.Data; -using Database.Data; -using Microsoft.EntityFrameworkCore; - -namespace Database.GraphQl.DataX; - -public sealed class DataDataLoaders -: DataLoaders -{ - [DataLoader] - public static ValueTask> GetHttpsResourcesByDataIdAsync( - IReadOnlyList ids, - QueryContext queryContext, - IDbContextFactory databaseContextFactory, - CancellationToken cancellationToken - ) - { - return GetManyByOneIdAsync( - ids, - (databaseContext) => databaseContext.GetHttpsResources, - _ => _.DataId, - queryContext, - databaseContextFactory, - cancellationToken - ); - } - - [DataLoader] - public static ValueTask> GetHttpsResourceTreeNonRootVerticesByDataIdAsync( - IReadOnlyList ids, - QueryContext queryContext, - IDbContextFactory databaseContextFactory, - CancellationToken cancellationToken - ) - { - return GetManyByOneIdAsync( - ids, - (databaseContext) => databaseContext.GetHttpsResources.Where(_ => _.ParentId != null), - _ => _.DataId, - queryContext, - databaseContextFactory, - cancellationToken - ); - } - - [DataLoader] - public static ValueTask> GetHttpsResourceTreeRootByDataIdAsync( - IReadOnlyList ids, - QueryContext queryContext, - IDbContextFactory databaseContextFactory, - CancellationToken cancellationToken - ) - { - return GetManyByOneIdAsync( - ids, - (databaseContext) => databaseContext.GetHttpsResources.Where(_ => _.ParentId == null), - _ => _.DataId, - queryContext, - databaseContextFactory, - cancellationToken - ); - } -} \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/DataFilterType.cs b/backend/src/GraphQl/DataX/DataFilterType.cs index fcf30ee9..93cc8e2d 100644 --- a/backend/src/GraphQl/DataX/DataFilterType.cs +++ b/backend/src/GraphQl/DataX/DataFilterType.cs @@ -12,5 +12,22 @@ IFilterInputTypeDescriptor descriptor { base.Configure(descriptor); descriptor.Name(nameof(DataFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); + + // TODO Why are the fields below not included by `base.Configure` above? + // AuditableEntityFilterType.Configure + descriptor.Field(x => x.Id); + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); + // DataFilterTypeBase.Configure + descriptor.Field(x => x.UserId); + descriptor.Field(x => x.Locale); + descriptor.Field(x => x.Name); + descriptor.Field(x => x.Description); + descriptor.Field(x => x.ComponentId); + descriptor.Field(x => x.CreatorId); + descriptor.Field(x => x.AppliedMethod); + descriptor.Field(x => x.Approvals); + descriptor.Field(x => x.Resources); + descriptor.Field(x => x.Warnings); } } \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/DataFilterTypeBase.cs b/backend/src/GraphQl/DataX/DataFilterTypeBase.cs index 001f734f..7ef5a8ca 100644 --- a/backend/src/GraphQl/DataX/DataFilterTypeBase.cs +++ b/backend/src/GraphQl/DataX/DataFilterTypeBase.cs @@ -6,7 +6,7 @@ namespace Database.GraphQl.DataX; public abstract class DataFilterTypeBase : AuditableEntityFilterType - where TData : IData, IAuditable + where TData : IData { protected override void Configure( IFilterInputTypeDescriptor descriptor diff --git a/backend/src/GraphQl/DataX/DataResolvers.cs b/backend/src/GraphQl/DataX/DataResolvers.cs index a605d4fe..1e90eb81 100644 --- a/backend/src/GraphQl/DataX/DataResolvers.cs +++ b/backend/src/GraphQl/DataX/DataResolvers.cs @@ -1,5 +1,6 @@ using System.Threading; using System.Threading.Tasks; +using Database.ApiRequests; using Database.Data; using Database.GraphQl.Extensions; using Database.GraphQl.GetHttpsResources; @@ -33,4 +34,29 @@ [Parent] IData data { return new GetHttpsResourceTree(data); } + + public Task GetDatabaseAsync( + [Parent] IData data, + IDatabaseByIdDataLoader byId, + AppSettings appSettings + ) + { + return byId.LoadAsync(appSettings.DatabaseId); + } + + public Task GetComponentAsync( + [Parent] IData data, + IComponentByIdDataLoader byId + ) + { + return byId.LoadAsync(data.ComponentId); + } + + public Task GetInstitutionAsync( + [Parent] IData data, + IInstitutionByIdDataLoader byId + ) + { + return byId.LoadAsync(data.CreatorId); + } } \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/DataType.cs b/backend/src/GraphQl/DataX/DataType.cs index 101b46ba..344c48e6 100644 --- a/backend/src/GraphQl/DataX/DataType.cs +++ b/backend/src/GraphQl/DataX/DataType.cs @@ -1,4 +1,6 @@ +using Database.ApiRequests; using Database.Data; +using Database.Extensions; using Database.GraphQl.Scalars; using HotChocolate.Types; @@ -20,12 +22,17 @@ IInterfaceTypeDescriptor descriptor descriptor .Field(_ => _.PublishingState) .Ignore(); + descriptor + .Field(GraphQlConstants.IdFieldName) + .Type>(); descriptor .Field(GraphQlConstants.UuidFieldName) .Type>(); descriptor .Field(_ => _.UpdatedAt) .Name(TimestampFieldName); + descriptor + .Field(_ => _.CreatedAt); descriptor .Field(x => x.Locale) .Type>(); @@ -39,5 +46,14 @@ IInterfaceTypeDescriptor descriptor descriptor .Field(x => x.Approval) .Type>>(); + descriptor + .Field(DataType.DatabaseIdFieldName[..^2].FirstCharToLower()) + .Type>(); + descriptor + .Field(nameof(IData.ComponentId)[..^2].FirstCharToLower()) + .Type>(); + descriptor + .Field(nameof(IData.CreatorId)[..^2].FirstCharToLower()) + .Type>(); } } \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/DataTypeBase.cs b/backend/src/GraphQl/DataX/DataTypeBase.cs index bcfcda17..cad3965c 100644 --- a/backend/src/GraphQl/DataX/DataTypeBase.cs +++ b/backend/src/GraphQl/DataX/DataTypeBase.cs @@ -1,7 +1,9 @@ using System; +using Database.ApiRequests; using Database.Data; using Database.GraphQl.Entities; using Database.GraphQl.Scalars; +using Database.Extensions; using GreenDonut; using HotChocolate.Types; @@ -24,16 +26,34 @@ IObjectTypeDescriptor descriptor .Field(_ => _.UpdatedAt) .Name(DataType.TimestampFieldName); descriptor - .Field(x => x.Locale) + .Field(_ => _.Locale) .Type>(); descriptor - .Field(x => x.Resources) - .ResolveWith(t => t.GetHttpsResources(default!, default!, default!, default!)); + .Field(_ => _.Resources) + .Cost(1) + .ResolveWith(_ => _.GetHttpsResources(default!, default!, default!, default!)); descriptor .Field(DataType.ResourceTreeFieldName) - .ResolveWith(t => t.GetHttpsResourceTree(default!)); + .Cost(1) + .ResolveWith(_ => _.GetHttpsResourceTree(default!)); descriptor - .Field(x => x.Approval) + .Field(_ => _.Approval) + .Cost(3) .Type>>(); + descriptor + .Field(DataType.DatabaseIdFieldName[..^2].FirstCharToLower()) + .Type>() + .Cost(3) + .ResolveWith(_ => _.GetDatabaseAsync(default!, default!, default!)); + descriptor + .Field(nameof(IData.ComponentId)[..^2].FirstCharToLower()) + .Type>() + .Cost(3) + .ResolveWith(_ => _.GetComponentAsync(default!, default!)); + descriptor + .Field(nameof(IData.CreatorId)[..^2].FirstCharToLower()) + .Type>() + .Cost(3) + .ResolveWith(_ => _.GetInstitutionAsync(default!, default!)); } } \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/GetHttpsResourceTree.cs b/backend/src/GraphQl/DataX/GetHttpsResourceTree.cs index 76c9c24a..bee24b31 100644 --- a/backend/src/GraphQl/DataX/GetHttpsResourceTree.cs +++ b/backend/src/GraphQl/DataX/GetHttpsResourceTree.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Database.Data; using Database.GraphQl.Extensions; +using Database.GraphQl.GetHttpsResources; using GreenDonut; using GreenDonut.Data; using HotChocolate.Data; @@ -39,8 +40,8 @@ await byId .With(resolverContext.GetQueryContext()) .LoadRequiredAsync(data.Id, cancellationToken) ) - .Select(v => - new GetHttpsResourceTreeNonRootVertex(v) + .Select(_ => + new GetHttpsResourceTreeNonRootVertex(_) ) .ToList() .AsReadOnly(); diff --git a/backend/src/GraphQl/Databases/DatabaseQueries.cs b/backend/src/GraphQl/Databases/DatabaseQueries.cs index 75142f1b..a769526f 100644 --- a/backend/src/GraphQl/Databases/DatabaseQueries.cs +++ b/backend/src/GraphQl/Databases/DatabaseQueries.cs @@ -10,20 +10,20 @@ namespace Database.GraphQl.Databases; [ExtendObjectType(nameof(Query))] public sealed class DatabaseQueries { - public async Task GetDatabaseAsync( + public async Task GetDatabaseAsync( AppSettings appSettings, - QueryDatabase queryDatabase, + IDatabaseByIdDataLoader byId, IResolverContext resolverContext, CancellationToken cancellationToken ) { var database = await GraphQlRequestHelper.TransformExceptionsAsync( - () => queryDatabase.Do( + () => byId.LoadAsync( appSettings.DatabaseId, cancellationToken ), resolverContext, - queryDatabase.GetGraphQlEndpoint + DatabaseDataLoader.GetGraphQlEndpoint(appSettings) ); if (database is null) { diff --git a/backend/src/GraphQl/Databases/VerifyDatabaseMutation.cs b/backend/src/GraphQl/Databases/VerifyDatabaseMutation.cs new file mode 100644 index 00000000..663a72d6 --- /dev/null +++ b/backend/src/GraphQl/Databases/VerifyDatabaseMutation.cs @@ -0,0 +1,42 @@ +using System.Threading; +using System.Threading.Tasks; +using Database.ApiRequests; +using HotChocolate.Resolvers; +using static Database.ApiRequests.VerifyDatabase; + +namespace Database.GraphQl.Databases; + +// TODO [ExtendObjectType(nameof(Mutation))] +public sealed class VerifyDatabaseMutation +{ + public async Task VerifyDatabaseAsync( + VerifyDatabaseInput input, + VerifyDatabase verifyDatabase, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + var databasePayload = await GraphQlRequestHelper.TransformExceptionsAsync( + () => verifyDatabase.Do( + input, + cancellationToken + ), + resolverContext, + verifyDatabase.GetGraphQlEndpoint + ); + if (databasePayload is null) + { + return new VerifyDatabasePayload( + null, + [ + new VerifyDatabaseError( + VerifyDatabaseErrorCode.UNKNOWN, + "Unknown error.", + [] + ) + ] + ); + } + return databasePayload; + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/FileMetaInformationType.cs b/backend/src/GraphQl/FileMetaInformationType.cs new file mode 100644 index 00000000..bbabc84e --- /dev/null +++ b/backend/src/GraphQl/FileMetaInformationType.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using Database.ApiRequests; +using Database.Data; +using Database.Extensions; +using HotChocolate; +using HotChocolate.Types; + +namespace Database.GraphQl; + +public sealed class FileMetaInformationType + : ObjectType +{ + protected override void Configure( + IObjectTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor + .Field(nameof(FileMetaInformation.DataFormatId)[..^2].FirstCharToLower()) + .Type>() + .Cost(3) + .ResolveWith(_ => Resolvers.GetDataFormatAsync(default!, default!)); + } + + private sealed class Resolvers + { + public static Task GetDataFormatAsync( + [Parent] FileMetaInformation parent, + IDataFormatByIdDataLoader byId + ) + { + return byId.LoadAsync(parent.DataFormatId); + } + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/GeometricDataX/GeometricDataFilterType.cs b/backend/src/GraphQl/GeometricDataX/GeometricDataFilterType.cs index 8f168aab..a1d68e0d 100644 --- a/backend/src/GraphQl/GeometricDataX/GeometricDataFilterType.cs +++ b/backend/src/GraphQl/GeometricDataX/GeometricDataFilterType.cs @@ -14,5 +14,22 @@ IFilterInputTypeDescriptor descriptor base.Configure(descriptor); descriptor.Name(nameof(GeometricDataFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); descriptor.Field(x => x.Thicknesses); + + // TODO Why are the fields below not included by `base.Configure` above? + // AuditableEntityFilterType.Configure + descriptor.Field(x => x.Id); + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); + // DataFilterTypeBase.Configure + descriptor.Field(x => x.UserId); + descriptor.Field(x => x.Locale); + descriptor.Field(x => x.Name); + descriptor.Field(x => x.Description); + descriptor.Field(x => x.ComponentId); + descriptor.Field(x => x.CreatorId); + descriptor.Field(x => x.AppliedMethod); + descriptor.Field(x => x.Approvals); + descriptor.Field(x => x.Resources); + descriptor.Field(x => x.Warnings); } } \ No newline at end of file diff --git a/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceDataLoaders.cs b/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceDataLoaders.cs index a04f9b0a..77155783 100644 --- a/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceDataLoaders.cs +++ b/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceDataLoaders.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -46,4 +47,58 @@ CancellationToken cancellationToken cancellationToken ); } + + [DataLoader] + public static ValueTask> GetHttpsResourcesByDataIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetManyByOneIdAsync( + ids, + (databaseContext) => databaseContext.GetHttpsResources, + _ => _.DataId, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask> GetHttpsResourceTreeNonRootVerticesByDataIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetManyByOneIdAsync( + ids, + (databaseContext) => databaseContext.GetHttpsResources.Where(_ => _.ParentId != null), + _ => _.DataId, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask> GetHttpsResourceTreeRootByDataIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetManyByOneIdAsync( + ids, + (databaseContext) => databaseContext.GetHttpsResources.Where(_ => _.ParentId == null), + _ => _.DataId, + queryContext, + databaseContextFactory, + cancellationToken + ); + } } \ No newline at end of file diff --git a/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceResolvers.cs b/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceResolvers.cs index aca20aa5..56b6a57a 100644 --- a/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceResolvers.cs +++ b/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceResolvers.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Database.ApiRequests; using Database.Data; using Database.GraphQl.CalorimetricDataX; using Database.GraphQl.GeometricDataX; @@ -100,4 +101,12 @@ CancellationToken cancellationToken { return byId.LoadAsync(getHttpsResource.Id, cancellationToken)!; } + + public Task GetDataFormatAsync( + [Parent] GetHttpsResource getHttpsResource, + IDataFormatByIdDataLoader byId + ) + { + return byId.LoadAsync(getHttpsResource.DataFormatId); + } } \ No newline at end of file diff --git a/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceType.cs b/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceType.cs index 01bc6cab..9b7fdf6a 100644 --- a/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceType.cs +++ b/backend/src/GraphQl/GetHttpsResources/GetHttpsResourceType.cs @@ -1,5 +1,7 @@ +using Database.ApiRequests; using Database.Data; using Database.GraphQl.Entities; +using Database.Extensions; using HotChocolate.Types; namespace Database.GraphQl.GetHttpsResources; @@ -14,6 +16,7 @@ IObjectTypeDescriptor descriptor base.Configure(descriptor); descriptor .Field("locator") + .Cost(0) .ResolveWith(t => t.GetLocator(default!, default!)); descriptor .Field(_ => _.FileName) @@ -29,9 +32,11 @@ IObjectTypeDescriptor descriptor .Ignore(); descriptor .Field(x => x.Parent) + .Cost(0) .ResolveWith(t => t.GetParent(default!, default!, default!)); descriptor .Field(x => x.Children) + .Cost(0) .ResolveWith(t => t.GetChildren(default!, default!, default!)); descriptor .Field(x => x.DataId) @@ -74,7 +79,13 @@ IObjectTypeDescriptor descriptor .Ignore(); descriptor .Field(x => x.Data) + .Cost(0) .ResolveWith(t => t.GetData(default!, default!, default!, default!, default!, default!, default!, default!)); + descriptor + .Field(nameof(GetHttpsResource.DataFormatId)[..^2].FirstCharToLower()) + .Type>() + .Cost(3) + .ResolveWith(_ => _.GetDataFormatAsync(default!, default!)); } } \ No newline at end of file diff --git a/backend/src/GraphQl/GraphQlConstants.cs b/backend/src/GraphQl/GraphQlConstants.cs index b8ce2ece..30a0470a 100644 --- a/backend/src/GraphQl/GraphQlConstants.cs +++ b/backend/src/GraphQl/GraphQlConstants.cs @@ -9,6 +9,7 @@ internal static class GraphQlConstants internal const string FilterInputSuffix = "PropositionInput"; internal const string SortInputSuffix = "SortInput"; internal const string PendingPrefix = "pending"; + internal const string IdFieldName = "id"; internal const string UuidFieldName = "uuid"; internal const string VersionFieldName = "version"; } \ No newline at end of file diff --git a/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataFilterType.cs b/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataFilterType.cs index 016404ee..fcadf11e 100644 --- a/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataFilterType.cs +++ b/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataFilterType.cs @@ -13,5 +13,22 @@ IFilterInputTypeDescriptor descriptor { base.Configure(descriptor); descriptor.Name(nameof(HygrothermalDataFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); + + // TODO Why are the fields below not included by `base.Configure` above? + // AuditableEntityFilterType.Configure + descriptor.Field(x => x.Id); + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); + // DataFilterTypeBase.Configure + descriptor.Field(x => x.UserId); + descriptor.Field(x => x.Locale); + descriptor.Field(x => x.Name); + descriptor.Field(x => x.Description); + descriptor.Field(x => x.ComponentId); + descriptor.Field(x => x.CreatorId); + descriptor.Field(x => x.AppliedMethod); + descriptor.Field(x => x.Approvals); + descriptor.Field(x => x.Resources); + descriptor.Field(x => x.Warnings); } } \ No newline at end of file diff --git a/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataFilterType.cs b/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataFilterType.cs index a61b41eb..34d92e62 100644 --- a/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataFilterType.cs +++ b/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataFilterType.cs @@ -13,5 +13,22 @@ IFilterInputTypeDescriptor descriptor { base.Configure(descriptor); descriptor.Name(nameof(LifeCycleDataFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); + + // TODO Why are the fields below not included by `base.Configure` above? + // AuditableEntityFilterType.Configure + descriptor.Field(x => x.Id); + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); + // DataFilterTypeBase.Configure + descriptor.Field(x => x.UserId); + descriptor.Field(x => x.Locale); + descriptor.Field(x => x.Name); + descriptor.Field(x => x.Description); + descriptor.Field(x => x.ComponentId); + descriptor.Field(x => x.CreatorId); + descriptor.Field(x => x.AppliedMethod); + descriptor.Field(x => x.Approvals); + descriptor.Field(x => x.Resources); + descriptor.Field(x => x.Warnings); } } \ No newline at end of file diff --git a/backend/src/GraphQl/Methods/MethodQueries.cs b/backend/src/GraphQl/Methods/MethodQueries.cs index 764fc95a..07deb0f0 100644 --- a/backend/src/GraphQl/Methods/MethodQueries.cs +++ b/backend/src/GraphQl/Methods/MethodQueries.cs @@ -96,7 +96,7 @@ public async Task CalculateMethodAsync( AppSettings appSettings, MethodFactory methodFactory, ApiRequestService apiRequestService, - QueryDatabase queryDatabase, + IDatabaseByIdDataLoader databaseById, QueryData queryData, IResolverContext resolverContext, CancellationToken cancellationToken @@ -115,12 +115,12 @@ [new CalculateMethodError( ); } var database = await GraphQlRequestHelper.TransformExceptionsAsync( - () => queryDatabase.Do( + () => databaseById.LoadAsync( dataReference.DatabaseId, cancellationToken ), resolverContext, - queryDatabase.GetGraphQlEndpoint + DatabaseDataLoader.GetGraphQlEndpoint(appSettings) ); if (database is null) { diff --git a/backend/src/GraphQl/OpticalDataX/OpticalDataFilterType.cs b/backend/src/GraphQl/OpticalDataX/OpticalDataFilterType.cs index 580ec31e..94e9e859 100644 --- a/backend/src/GraphQl/OpticalDataX/OpticalDataFilterType.cs +++ b/backend/src/GraphQl/OpticalDataX/OpticalDataFilterType.cs @@ -23,5 +23,22 @@ IFilterInputTypeDescriptor descriptor descriptor.Field(x => x.InfraredEmittances); descriptor.Field(x => x.ColorRenderingIndices); descriptor.Field(x => x.CielabColors); + + // TODO Why are the fields below not included by `base.Configure` above? + // AuditableEntityFilterType.Configure + descriptor.Field(x => x.Id); + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); + // DataFilterTypeBase.Configure + descriptor.Field(x => x.UserId); + descriptor.Field(x => x.Locale); + descriptor.Field(x => x.Name); + descriptor.Field(x => x.Description); + descriptor.Field(x => x.ComponentId); + descriptor.Field(x => x.CreatorId); + descriptor.Field(x => x.AppliedMethod); + descriptor.Field(x => x.Approvals); + descriptor.Field(x => x.Resources); + descriptor.Field(x => x.Warnings); } } \ No newline at end of file diff --git a/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataFilterType.cs b/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataFilterType.cs index abd8c2d9..fb479dc8 100644 --- a/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataFilterType.cs +++ b/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataFilterType.cs @@ -13,5 +13,22 @@ IFilterInputTypeDescriptor descriptor { base.Configure(descriptor); descriptor.Name(nameof(PhotovoltaicDataFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); + + // TODO Why are the fields below not included by `base.Configure` above? + // AuditableEntityFilterType.Configure + descriptor.Field(x => x.Id); + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); + // DataFilterTypeBase.Configure + descriptor.Field(x => x.UserId); + descriptor.Field(x => x.Locale); + descriptor.Field(x => x.Name); + descriptor.Field(x => x.Description); + descriptor.Field(x => x.ComponentId); + descriptor.Field(x => x.CreatorId); + descriptor.Field(x => x.AppliedMethod); + descriptor.Field(x => x.Approvals); + descriptor.Field(x => x.Resources); + descriptor.Field(x => x.Warnings); } } \ No newline at end of file diff --git a/backend/src/GraphQl/References/ReferenceType.cs b/backend/src/GraphQl/References/ReferenceType.cs index abad0cdf..047dd47e 100644 --- a/backend/src/GraphQl/References/ReferenceType.cs +++ b/backend/src/GraphQl/References/ReferenceType.cs @@ -1,7 +1,4 @@ -using System; using Database.Data; -using Database.GraphQl.Publications; -using Database.GraphQl.Standards; using HotChocolate.Types; namespace Database.GraphQl.References; diff --git a/backend/src/GraphQl/ToTreeVertexAppliedConversionMethodType.cs b/backend/src/GraphQl/ToTreeVertexAppliedConversionMethodType.cs new file mode 100644 index 00000000..bd0857ab --- /dev/null +++ b/backend/src/GraphQl/ToTreeVertexAppliedConversionMethodType.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using Database.ApiRequests; +using Database.Data; +using Database.Extensions; +using HotChocolate; +using HotChocolate.Types; + +namespace Database.GraphQl; + +public sealed class ToTreeVertexAppliedConversionMethodType + : ObjectType +{ + protected override void Configure( + IObjectTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor + .Field(nameof(ToTreeVertexAppliedConversionMethod.MethodId)[..^2].FirstCharToLower()) + .Type>() + .Cost(3) + .ResolveWith(_ => Resolvers.GetMethodAsync(default!, default!)); + } + + private sealed class Resolvers + { + public static Task GetMethodAsync( + [Parent] ToTreeVertexAppliedConversionMethod parent, + IMethodByIdDataLoader byId + ) + { + return byId.LoadAsync(parent.MethodId); + } + } +} \ No newline at end of file diff --git a/backend/src/Migrations/20260527161543_MakeEntitiesAuditable.Designer.cs b/backend/src/Migrations/20260527161543_MakeEntitiesAuditable.Designer.cs new file mode 100644 index 00000000..690595ea --- /dev/null +++ b/backend/src/Migrations/20260527161543_MakeEntitiesAuditable.Designer.cs @@ -0,0 +1,3226 @@ +// +using System; +using System.Collections.Generic; +using System.Text.Json; +using Database.Data; +using Database.Enumerations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Database.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260527161543_MakeEntitiesAuditable")] + partial class MakeEntitiesAuditable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("database") + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "database", "calorimetric_observer", new[] { "ten_degrees", "two_degrees" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "database", "coated_side", new[] { "both", "neither", "non_prime", "not_applicable", "prime", "unknown" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "database", "data_kind", new[] { "calorimetric_data", "geometric_data", "hygrothermal_data", "life_cycle_data", "optical_data", "photovoltaic_data" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "database", "illuminant", new[] { "a", "d65" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "database", "optical_component_subtype", new[] { "acid_etched_glass", "applied_film", "cellular_shade", "chromogenic", "coated", "coating", "diffusing_shade", "embedded_coating", "film", "fritted_glass", "interlayer", "laminate", "monolithic", "perforated_screen", "pleated_shade", "roller_shade", "roman_shade", "sandblasted_glass", "shade_material", "venetian_blind", "vertical_louver", "woven_shade" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "database", "optical_component_type", new[] { "glazing", "shading" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "database", "publishing_state", new[] { "pending", "published", "retracted" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "database", "standardizer", new[] { "aerc", "agi", "ashrae", "breeam", "bs", "bsi", "cen", "cie", "dgnb", "din", "dvwg", "iec", "ies", "ift", "iso", "jis", "leed", "nfrc", "riba", "ul", "unece", "vdi", "vff", "well" }); + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "pgcrypto"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Database.Data.CalorimetricData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ComponentId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatorId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.PrimitiveCollection("GValues") + .IsRequired() + .HasColumnType("double precision[]"); + + b.Property("Locale") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("PublishingState") + .HasColumnType("database.publishing_state"); + + b.PrimitiveCollection("UValues") + .IsRequired() + .HasColumnType("double precision[]"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.PrimitiveCollection("Warnings") + .IsRequired() + .HasColumnType("text[]"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.ToTable("calorimetric_data", "database"); + }); + + modelBuilder.Entity("Database.Data.GeometricData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ComponentId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatorId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.PrimitiveCollection("Heights") + .IsRequired() + .HasColumnType("double precision[]"); + + b.Property("Locale") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("PublishingState") + .HasColumnType("database.publishing_state"); + + b.PrimitiveCollection("Thicknesses") + .IsRequired() + .HasColumnType("double precision[]"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.PrimitiveCollection("Warnings") + .IsRequired() + .HasColumnType("text[]"); + + b.PrimitiveCollection("Widths") + .IsRequired() + .HasColumnType("double precision[]"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.ToTable("geometric_data", "database"); + }); + + modelBuilder.Entity("Database.Data.GetHttpsResource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CalorimetricDataId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("DataFormatId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("FileExtension") + .HasColumnType("text"); + + b.Property("GeometricDataId") + .HasColumnType("uuid"); + + b.Property("HashValue") + .IsRequired() + .HasColumnType("text"); + + b.Property("HygrothermalDataId") + .HasColumnType("uuid"); + + b.Property("LifeCycleDataId") + .HasColumnType("uuid"); + + b.Property("OpticalDataId") + .HasColumnType("uuid"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("PhotovoltaicDataId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("CalorimetricDataId") + .IsUnique() + .HasFilter("\"CalorimetricDataId\" IS NOT NULL AND \"ParentId\" IS NULL"); + + b.HasIndex("GeometricDataId") + .IsUnique() + .HasFilter("\"GeometricDataId\" IS NOT NULL AND \"ParentId\" IS NULL"); + + b.HasIndex("HygrothermalDataId") + .IsUnique() + .HasFilter("\"HygrothermalDataId\" IS NOT NULL AND \"ParentId\" IS NULL"); + + b.HasIndex("LifeCycleDataId") + .IsUnique() + .HasFilter("\"LifeCycleDataId\" IS NOT NULL AND \"ParentId\" IS NULL"); + + b.HasIndex("OpticalDataId") + .IsUnique() + .HasFilter("\"OpticalDataId\" IS NOT NULL AND \"ParentId\" IS NULL"); + + b.HasIndex("ParentId"); + + b.HasIndex("PhotovoltaicDataId") + .IsUnique() + .HasFilter("\"PhotovoltaicDataId\" IS NOT NULL AND \"ParentId\" IS NULL"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.ToTable("get_https_resource", "database", t => + { + t.HasTrigger("LC_TRIGGER_data_id_cannot_change"); + + t.HasTrigger("LC_TRIGGER_data_ids_must_match"); + + t.HasTrigger("data_id_cannot_change"); + + t.HasTrigger("data_ids_must_match"); + + t.HasCheckConstraint("CK_GetHttpsResource_Exactly_One_Data_Set", "NUM_NONNULLS(\"CalorimetricDataId\", \"GeometricDataId\", \"HygrothermalDataId\", \"LifeCycleDataId\", \"OpticalDataId\", \"PhotovoltaicDataId\") = 1"); + + t.HasCheckConstraint("CK_GetHttpsResource_Root_Or_Child", "(\"ParentId\" IS NULL AND \"AppliedConversionMethod_MethodId\" IS NULL)\nOR (\"ParentId\" IS NOT NULL AND \"AppliedConversionMethod_MethodId\" IS NOT NULL)"); + }); + + b + .HasAnnotation("LC_TRIGGER_data_id_cannot_change", "CREATE FUNCTION \"database\".\"LC_TRIGGER_data_id_cannot_change\"() RETURNS trigger as $LC_TRIGGER_data_id_cannot_change$\r\nBEGIN\r\n IF COALESCE(OLD.\"CalorimetricDataId\", OLD.\"GeometricDataId\", OLD.\"HygrothermalDataId\", OLD.\"LifeCycleDataId\", OLD.\"OpticalDataId\", OLD.\"PhotovoltaicDataId\") <> COALESCE(NEW.\"CalorimetricDataId\", NEW.\"GeometricDataId\", NEW.\"HygrothermalDataId\", NEW.\"LifeCycleDataId\", NEW.\"OpticalDataId\", NEW.\"PhotovoltaicDataId\")\nTHEN\n RAISE EXCEPTION 'You cannot change the data ID of a resource.';\nEND IF;\r\nRETURN NEW;\r\nEND;\r\n$LC_TRIGGER_data_id_cannot_change$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_data_id_cannot_change BEFORE UPDATE\r\nON \"database\".\"get_https_resource\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"database\".\"LC_TRIGGER_data_id_cannot_change\"();") + .HasAnnotation("LC_TRIGGER_data_ids_must_match", "CREATE FUNCTION \"database\".\"LC_TRIGGER_data_ids_must_match\"() RETURNS trigger as $LC_TRIGGER_data_ids_must_match$\r\nBEGIN\r\n IF NEW.\"ParentId\" IS NOT NULL\n AND (\n SELECT COUNT(\"Id\")\n FROM database.\"get_https_resource\"\n WHERE \"Id\" = NEW.\"ParentId\" AND COALESCE(\"CalorimetricDataId\", \"GeometricDataId\", \"HygrothermalDataId\", \"LifeCycleDataId\", \"OpticalDataId\", \"PhotovoltaicDataId\") = COALESCE(NEW.\"CalorimetricDataId\", NEW.\"GeometricDataId\", NEW.\"HygrothermalDataId\", NEW.\"LifeCycleDataId\", NEW.\"OpticalDataId\", NEW.\"PhotovoltaicDataId\")\n )\n <> 1\nTHEN\n RAISE EXCEPTION 'The new resource must have the same data ID as its parent.';\nEND IF;\r\nRETURN NEW;\r\nEND;\r\n$LC_TRIGGER_data_ids_must_match$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_data_ids_must_match BEFORE INSERT\r\nON \"database\".\"get_https_resource\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"database\".\"LC_TRIGGER_data_ids_must_match\"();"); + }); + + modelBuilder.Entity("Database.Data.HygrothermalData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ComponentId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatorId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Locale") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("PublishingState") + .HasColumnType("database.publishing_state"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.PrimitiveCollection("Warnings") + .IsRequired() + .HasColumnType("text[]"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.ToTable("hygrothermal_data", "database"); + }); + + modelBuilder.Entity("Database.Data.InstitutionAccessRights", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AllowedDatasetsPerTime") + .HasColumnType("bigint"); + + b.Property("AllowedUserCount") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("InstitutionId") + .HasColumnType("uuid"); + + b.Property("Period") + .HasColumnType("interval"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.PrimitiveCollection>("UserAlreadyAccessed") + .IsRequired() + .HasColumnType("uuid[]"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("InstitutionId") + .IsUnique(); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.ToTable("institution_access_rights", "database"); + }); + + modelBuilder.Entity("Database.Data.LifeCycleData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ComponentId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatorId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Locale") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("PublishingState") + .HasColumnType("database.publishing_state"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.PrimitiveCollection("Warnings") + .IsRequired() + .HasColumnType("text[]"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.ToTable("lifeCycle_data", "database"); + }); + + modelBuilder.Entity("Database.Data.OpticalData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CoatedSide") + .HasColumnType("database.coated_side"); + + b.PrimitiveCollection("ColorRenderingIndices") + .IsRequired() + .HasColumnType("double precision[]"); + + b.Property("ComponentId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatorId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.PrimitiveCollection("InfraredEmittances") + .IsRequired() + .HasColumnType("double precision[]"); + + b.Property("Locale") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.PrimitiveCollection("NearnormalHemisphericalSolarReflectances") + .IsRequired() + .HasColumnType("double precision[]"); + + b.PrimitiveCollection("NearnormalHemisphericalSolarTransmittances") + .IsRequired() + .HasColumnType("double precision[]"); + + b.PrimitiveCollection("NearnormalHemisphericalVisibleReflectances") + .IsRequired() + .HasColumnType("double precision[]"); + + b.PrimitiveCollection("NearnormalHemisphericalVisibleTransmittances") + .IsRequired() + .HasColumnType("double precision[]"); + + b.Property("PublishingState") + .HasColumnType("database.publishing_state"); + + b.Property("Subtype") + .HasColumnType("database.optical_component_subtype"); + + b.Property("Type") + .HasColumnType("database.optical_component_type"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.PrimitiveCollection("Warnings") + .IsRequired() + .HasColumnType("text[]"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.ToTable("optical_data", "database"); + }); + + modelBuilder.Entity("Database.Data.PhotovoltaicData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ComponentId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("CreatorId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Locale") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("PublishingState") + .HasColumnType("database.publishing_state"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.PrimitiveCollection("Warnings") + .IsRequired() + .HasColumnType("text[]"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.ToTable("photovoltaic_data", "database"); + }); + + modelBuilder.Entity("Database.Data.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.ToTable("user", "database"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("text"); + + b.Property("Xml") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("DataProtectionKeys", "database"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ClientId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ClientSecret") + .HasColumnType("text"); + + b.Property("ClientType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ConsentType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("JsonWebKeySet") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("PostLogoutRedirectUris") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedirectUris") + .HasColumnType("text"); + + b.Property("Requirements") + .HasColumnType("text"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.ToTable("OpenIddictApplications", "database"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Scopes") + .HasColumnType("text"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", "database"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Descriptions") + .HasColumnType("text"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("Resources") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes", "database"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ApplicationId") + .HasColumnType("text"); + + b.Property("AuthorizationId") + .HasColumnType("text"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RedemptionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Status") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Subject") + .HasMaxLength(400) + .HasColumnType("character varying(400)"); + + b.Property("Type") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", "database"); + }); + + modelBuilder.Entity("Database.Data.CalorimetricData", b => + { + b.OwnsOne("Database.Data.AppliedMethod", "AppliedMethod", b1 => + { + b1.Property("CalorimetricDataId") + .HasColumnType("uuid"); + + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.HasKey("CalorimetricDataId"); + + b1.ToTable("calorimetric_data", "database"); + + b1.WithOwner() + .HasForeignKey("CalorimetricDataId"); + + b1.OwnsMany("Database.Data.NamedMethodArgument", "Arguments", b2 => + { + b2.Property("AppliedMethodCalorimetricDataId") + .HasColumnType("uuid"); + + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b2.Property("Id")); + + b2.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b2.Property("Value") + .HasColumnType("jsonb"); + + b2.HasKey("AppliedMethodCalorimetricDataId", "Id"); + + b2.ToTable("calorimetric_data_Arguments", "database"); + + b2.WithOwner() + .HasForeignKey("AppliedMethodCalorimetricDataId"); + }); + + b1.OwnsMany("Database.Data.NamedMethodSource", "Sources", b2 => + { + b2.Property("AppliedMethodCalorimetricDataId") + .HasColumnType("uuid"); + + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b2.Property("Id")); + + b2.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b2.HasKey("AppliedMethodCalorimetricDataId", "Id"); + + b2.ToTable("calorimetric_data_Sources", "database"); + + b2.WithOwner() + .HasForeignKey("AppliedMethodCalorimetricDataId"); + + b2.OwnsOne("Database.Data.CrossDatabaseDataReference", "Value", b3 => + { + b3.Property("NamedMethodSourceAppliedMethodCalorimetricDataId") + .HasColumnType("uuid"); + + b3.Property("NamedMethodSourceId") + .HasColumnType("integer"); + + b3.Property("DataId") + .HasColumnType("uuid"); + + b3.Property("DataKind") + .HasColumnType("database.data_kind"); + + b3.Property("DataTimestamp") + .HasColumnType("timestamp with time zone"); + + b3.Property("DatabaseId") + .HasColumnType("uuid"); + + b3.HasKey("NamedMethodSourceAppliedMethodCalorimetricDataId", "NamedMethodSourceId"); + + b3.ToTable("calorimetric_data_Sources", "database"); + + b3.WithOwner() + .HasForeignKey("NamedMethodSourceAppliedMethodCalorimetricDataId", "NamedMethodSourceId"); + }); + + b2.Navigation("Value") + .IsRequired(); + }); + + b1.Navigation("Arguments"); + + b1.Navigation("Sources"); + }); + + b.OwnsOne("Database.Data.ResponseApproval", "Approval", b1 => + { + b1.Property("CalorimetricDataId") + .HasColumnType("uuid"); + + b1.Property("ApproverId") + .HasColumnType("uuid"); + + b1.Property("KeyFingerprint") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Query") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Signature") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b1.Property("Variables") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValueSql("'{}'"); + + b1.HasKey("CalorimetricDataId"); + + b1.ToTable("calorimetric_data", "database"); + + b1.WithOwner() + .HasForeignKey("CalorimetricDataId"); + }); + + b.OwnsMany("Database.Data.DataApproval", "Approvals", b1 => + { + b1.Property("CalorimetricDataId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("ApproverId") + .HasColumnType("uuid"); + + b1.Property("KeyFingerprint") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Query") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Signature") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b1.Property("Variables") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValueSql("'{}'"); + + b1.HasKey("CalorimetricDataId", "Id"); + + b1.ToTable("calorimetric_data_Approvals", "database"); + + b1.WithOwner() + .HasForeignKey("CalorimetricDataId"); + + b1.OwnsOne("Database.Data.Reference", "Statement", b2 => + { + b2.Property("DataApprovalCalorimetricDataId") + .HasColumnType("uuid"); + + b2.Property("DataApprovalId") + .HasColumnType("integer"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DataApprovalCalorimetricDataId", "DataApprovalId"); + + b2.ToTable("calorimetric_data_Approvals", "database"); + + b2.WithOwner() + .HasForeignKey("DataApprovalCalorimetricDataId", "DataApprovalId"); + + b2.OwnsOne("Database.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDataApprovalCalorimetricDataId") + .HasColumnType("uuid"); + + b3.Property("ReferenceDataApprovalId") + .HasColumnType("integer"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("ArXiv") + .HasColumnType("text"); + + b3.PrimitiveCollection("Authors") + .HasColumnType("text[]"); + + b3.Property("Doi") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Urn") + .HasColumnType("text"); + + b3.Property("WebAddress") + .HasColumnType("text"); + + b3.HasKey("ReferenceDataApprovalCalorimetricDataId", "ReferenceDataApprovalId"); + + b3.ToTable("calorimetric_data_Approvals", "database"); + + b3.WithOwner() + .HasForeignKey("ReferenceDataApprovalCalorimetricDataId", "ReferenceDataApprovalId"); + }); + + b2.OwnsOne("Database.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDataApprovalCalorimetricDataId") + .HasColumnType("uuid"); + + b3.Property("ReferenceDataApprovalId") + .HasColumnType("integer"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("database.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDataApprovalCalorimetricDataId", "ReferenceDataApprovalId"); + + b3.ToTable("calorimetric_data_Approvals", "database"); + + b3.WithOwner() + .HasForeignKey("ReferenceDataApprovalCalorimetricDataId", "ReferenceDataApprovalId"); + + b3.OwnsOne("Database.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDataApprovalCalorimetricDataId") + .HasColumnType("uuid"); + + b4.Property("StandardReferenceDataApprovalId") + .HasColumnType("integer"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDataApprovalCalorimetricDataId", "StandardReferenceDataApprovalId"); + + b4.ToTable("calorimetric_data_Approvals", "database"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDataApprovalCalorimetricDataId", "StandardReferenceDataApprovalId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Statement") + .IsRequired(); + }); + + b.OwnsOne("Database.Data.DataAccessRights", "DataAccessRights", b1 => + { + b1.Property("CalorimetricDataId") + .HasColumnType("uuid"); + + b1.PrimitiveCollection("AllowedApplications") + .HasColumnType("text[]"); + + b1.PrimitiveCollection("AllowedInstitutions") + .HasColumnType("uuid[]"); + + b1.Property("AllowedUserAndQuantity") + .HasColumnType("jsonb"); + + b1.HasKey("CalorimetricDataId"); + + b1.ToTable("calorimetric_data", "database"); + + b1.WithOwner() + .HasForeignKey("CalorimetricDataId"); + }); + + b.Navigation("AppliedMethod") + .IsRequired(); + + b.Navigation("Approval"); + + b.Navigation("Approvals"); + + b.Navigation("DataAccessRights") + .IsRequired(); + }); + + modelBuilder.Entity("Database.Data.GeometricData", b => + { + b.OwnsOne("Database.Data.AppliedMethod", "AppliedMethod", b1 => + { + b1.Property("GeometricDataId") + .HasColumnType("uuid"); + + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.HasKey("GeometricDataId"); + + b1.ToTable("geometric_data", "database"); + + b1.WithOwner() + .HasForeignKey("GeometricDataId"); + + b1.OwnsMany("Database.Data.NamedMethodArgument", "Arguments", b2 => + { + b2.Property("AppliedMethodGeometricDataId") + .HasColumnType("uuid"); + + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b2.Property("Id")); + + b2.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b2.Property("Value") + .HasColumnType("jsonb"); + + b2.HasKey("AppliedMethodGeometricDataId", "Id"); + + b2.ToTable("geometric_data_Arguments", "database"); + + b2.WithOwner() + .HasForeignKey("AppliedMethodGeometricDataId"); + }); + + b1.OwnsMany("Database.Data.NamedMethodSource", "Sources", b2 => + { + b2.Property("AppliedMethodGeometricDataId") + .HasColumnType("uuid"); + + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b2.Property("Id")); + + b2.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b2.HasKey("AppliedMethodGeometricDataId", "Id"); + + b2.ToTable("geometric_data_Sources", "database"); + + b2.WithOwner() + .HasForeignKey("AppliedMethodGeometricDataId"); + + b2.OwnsOne("Database.Data.CrossDatabaseDataReference", "Value", b3 => + { + b3.Property("NamedMethodSourceAppliedMethodGeometricDataId") + .HasColumnType("uuid"); + + b3.Property("NamedMethodSourceId") + .HasColumnType("integer"); + + b3.Property("DataId") + .HasColumnType("uuid"); + + b3.Property("DataKind") + .HasColumnType("database.data_kind"); + + b3.Property("DataTimestamp") + .HasColumnType("timestamp with time zone"); + + b3.Property("DatabaseId") + .HasColumnType("uuid"); + + b3.HasKey("NamedMethodSourceAppliedMethodGeometricDataId", "NamedMethodSourceId"); + + b3.ToTable("geometric_data_Sources", "database"); + + b3.WithOwner() + .HasForeignKey("NamedMethodSourceAppliedMethodGeometricDataId", "NamedMethodSourceId"); + }); + + b2.Navigation("Value") + .IsRequired(); + }); + + b1.Navigation("Arguments"); + + b1.Navigation("Sources"); + }); + + b.OwnsOne("Database.Data.ResponseApproval", "Approval", b1 => + { + b1.Property("GeometricDataId") + .HasColumnType("uuid"); + + b1.Property("ApproverId") + .HasColumnType("uuid"); + + b1.Property("KeyFingerprint") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Query") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Signature") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b1.Property("Variables") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValueSql("'{}'"); + + b1.HasKey("GeometricDataId"); + + b1.ToTable("geometric_data", "database"); + + b1.WithOwner() + .HasForeignKey("GeometricDataId"); + }); + + b.OwnsMany("Database.Data.DataApproval", "Approvals", b1 => + { + b1.Property("GeometricDataId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("ApproverId") + .HasColumnType("uuid"); + + b1.Property("KeyFingerprint") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Query") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Signature") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b1.Property("Variables") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValueSql("'{}'"); + + b1.HasKey("GeometricDataId", "Id"); + + b1.ToTable("geometric_data_Approvals", "database"); + + b1.WithOwner() + .HasForeignKey("GeometricDataId"); + + b1.OwnsOne("Database.Data.Reference", "Statement", b2 => + { + b2.Property("DataApprovalGeometricDataId") + .HasColumnType("uuid"); + + b2.Property("DataApprovalId") + .HasColumnType("integer"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DataApprovalGeometricDataId", "DataApprovalId"); + + b2.ToTable("geometric_data_Approvals", "database"); + + b2.WithOwner() + .HasForeignKey("DataApprovalGeometricDataId", "DataApprovalId"); + + b2.OwnsOne("Database.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDataApprovalGeometricDataId") + .HasColumnType("uuid"); + + b3.Property("ReferenceDataApprovalId") + .HasColumnType("integer"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("ArXiv") + .HasColumnType("text"); + + b3.PrimitiveCollection("Authors") + .HasColumnType("text[]"); + + b3.Property("Doi") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Urn") + .HasColumnType("text"); + + b3.Property("WebAddress") + .HasColumnType("text"); + + b3.HasKey("ReferenceDataApprovalGeometricDataId", "ReferenceDataApprovalId"); + + b3.ToTable("geometric_data_Approvals", "database"); + + b3.WithOwner() + .HasForeignKey("ReferenceDataApprovalGeometricDataId", "ReferenceDataApprovalId"); + }); + + b2.OwnsOne("Database.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDataApprovalGeometricDataId") + .HasColumnType("uuid"); + + b3.Property("ReferenceDataApprovalId") + .HasColumnType("integer"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("database.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDataApprovalGeometricDataId", "ReferenceDataApprovalId"); + + b3.ToTable("geometric_data_Approvals", "database"); + + b3.WithOwner() + .HasForeignKey("ReferenceDataApprovalGeometricDataId", "ReferenceDataApprovalId"); + + b3.OwnsOne("Database.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDataApprovalGeometricDataId") + .HasColumnType("uuid"); + + b4.Property("StandardReferenceDataApprovalId") + .HasColumnType("integer"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDataApprovalGeometricDataId", "StandardReferenceDataApprovalId"); + + b4.ToTable("geometric_data_Approvals", "database"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDataApprovalGeometricDataId", "StandardReferenceDataApprovalId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Statement") + .IsRequired(); + }); + + b.OwnsOne("Database.Data.DataAccessRights", "DataAccessRights", b1 => + { + b1.Property("GeometricDataId") + .HasColumnType("uuid"); + + b1.PrimitiveCollection("AllowedApplications") + .HasColumnType("text[]"); + + b1.PrimitiveCollection("AllowedInstitutions") + .HasColumnType("uuid[]"); + + b1.Property("AllowedUserAndQuantity") + .HasColumnType("jsonb"); + + b1.HasKey("GeometricDataId"); + + b1.ToTable("geometric_data", "database"); + + b1.WithOwner() + .HasForeignKey("GeometricDataId"); + }); + + b.Navigation("AppliedMethod") + .IsRequired(); + + b.Navigation("Approval"); + + b.Navigation("Approvals"); + + b.Navigation("DataAccessRights") + .IsRequired(); + }); + + modelBuilder.Entity("Database.Data.GetHttpsResource", b => + { + b.HasOne("Database.Data.CalorimetricData", "CalorimetricData") + .WithMany("Resources") + .HasForeignKey("CalorimetricDataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Database.Data.GeometricData", "GeometricData") + .WithMany("Resources") + .HasForeignKey("GeometricDataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Database.Data.HygrothermalData", "HygrothermalData") + .WithMany("Resources") + .HasForeignKey("HygrothermalDataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Database.Data.LifeCycleData", "LifeCycleData") + .WithMany("Resources") + .HasForeignKey("LifeCycleDataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Database.Data.OpticalData", "OpticalData") + .WithMany("Resources") + .HasForeignKey("OpticalDataId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Database.Data.GetHttpsResource", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId"); + + b.HasOne("Database.Data.PhotovoltaicData", "PhotovoltaicData") + .WithMany("Resources") + .HasForeignKey("PhotovoltaicDataId") + .OnDelete(DeleteBehavior.Cascade); + + b.OwnsMany("Database.Data.FileMetaInformation", "ArchivedFilesMetaInformation", b1 => + { + b1.Property("GetHttpsResourceId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("DataFormatId") + .HasColumnType("uuid"); + + b1.PrimitiveCollection("Path") + .IsRequired() + .HasColumnType("text[]"); + + b1.HasKey("GetHttpsResourceId", "Id"); + + b1.ToTable("FileMetaInformation", "database"); + + b1.WithOwner() + .HasForeignKey("GetHttpsResourceId"); + }); + + b.OwnsOne("Database.Data.ToTreeVertexAppliedConversionMethod", "AppliedConversionMethod", b1 => + { + b1.Property("GetHttpsResourceId") + .HasColumnType("uuid"); + + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.Property("SourceName") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("GetHttpsResourceId"); + + b1.ToTable("get_https_resource", "database"); + + b1.WithOwner() + .HasForeignKey("GetHttpsResourceId"); + + b1.OwnsMany("Database.Data.NamedMethodArgument", "Arguments", b2 => + { + b2.Property("ToTreeVertexAppliedConversionMethodGetHttpsResourceId") + .HasColumnType("uuid"); + + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b2.Property("Id")); + + b2.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b2.Property("Value") + .HasColumnType("jsonb"); + + b2.HasKey("ToTreeVertexAppliedConversionMethodGetHttpsResourceId", "Id"); + + b2.ToTable("get_https_resource_Arguments", "database"); + + b2.WithOwner() + .HasForeignKey("ToTreeVertexAppliedConversionMethodGetHttpsResourceId"); + }); + + b1.Navigation("Arguments"); + }); + + b.Navigation("AppliedConversionMethod"); + + b.Navigation("ArchivedFilesMetaInformation"); + + b.Navigation("CalorimetricData"); + + b.Navigation("GeometricData"); + + b.Navigation("HygrothermalData"); + + b.Navigation("LifeCycleData"); + + b.Navigation("OpticalData"); + + b.Navigation("Parent"); + + b.Navigation("PhotovoltaicData"); + }); + + modelBuilder.Entity("Database.Data.HygrothermalData", b => + { + b.OwnsOne("Database.Data.AppliedMethod", "AppliedMethod", b1 => + { + b1.Property("HygrothermalDataId") + .HasColumnType("uuid"); + + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.HasKey("HygrothermalDataId"); + + b1.ToTable("hygrothermal_data", "database"); + + b1.WithOwner() + .HasForeignKey("HygrothermalDataId"); + + b1.OwnsMany("Database.Data.NamedMethodArgument", "Arguments", b2 => + { + b2.Property("AppliedMethodHygrothermalDataId") + .HasColumnType("uuid"); + + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b2.Property("Id")); + + b2.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b2.Property("Value") + .HasColumnType("jsonb"); + + b2.HasKey("AppliedMethodHygrothermalDataId", "Id"); + + b2.ToTable("hygrothermal_data_Arguments", "database"); + + b2.WithOwner() + .HasForeignKey("AppliedMethodHygrothermalDataId"); + }); + + b1.OwnsMany("Database.Data.NamedMethodSource", "Sources", b2 => + { + b2.Property("AppliedMethodHygrothermalDataId") + .HasColumnType("uuid"); + + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b2.Property("Id")); + + b2.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b2.HasKey("AppliedMethodHygrothermalDataId", "Id"); + + b2.ToTable("hygrothermal_data_Sources", "database"); + + b2.WithOwner() + .HasForeignKey("AppliedMethodHygrothermalDataId"); + + b2.OwnsOne("Database.Data.CrossDatabaseDataReference", "Value", b3 => + { + b3.Property("NamedMethodSourceAppliedMethodHygrothermalDataId") + .HasColumnType("uuid"); + + b3.Property("NamedMethodSourceId") + .HasColumnType("integer"); + + b3.Property("DataId") + .HasColumnType("uuid"); + + b3.Property("DataKind") + .HasColumnType("database.data_kind"); + + b3.Property("DataTimestamp") + .HasColumnType("timestamp with time zone"); + + b3.Property("DatabaseId") + .HasColumnType("uuid"); + + b3.HasKey("NamedMethodSourceAppliedMethodHygrothermalDataId", "NamedMethodSourceId"); + + b3.ToTable("hygrothermal_data_Sources", "database"); + + b3.WithOwner() + .HasForeignKey("NamedMethodSourceAppliedMethodHygrothermalDataId", "NamedMethodSourceId"); + }); + + b2.Navigation("Value") + .IsRequired(); + }); + + b1.Navigation("Arguments"); + + b1.Navigation("Sources"); + }); + + b.OwnsOne("Database.Data.ResponseApproval", "Approval", b1 => + { + b1.Property("HygrothermalDataId") + .HasColumnType("uuid"); + + b1.Property("ApproverId") + .HasColumnType("uuid"); + + b1.Property("KeyFingerprint") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Query") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Signature") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b1.Property("Variables") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValueSql("'{}'"); + + b1.HasKey("HygrothermalDataId"); + + b1.ToTable("hygrothermal_data", "database"); + + b1.WithOwner() + .HasForeignKey("HygrothermalDataId"); + }); + + b.OwnsMany("Database.Data.DataApproval", "Approvals", b1 => + { + b1.Property("HygrothermalDataId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("ApproverId") + .HasColumnType("uuid"); + + b1.Property("KeyFingerprint") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Query") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Signature") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b1.Property("Variables") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValueSql("'{}'"); + + b1.HasKey("HygrothermalDataId", "Id"); + + b1.ToTable("hygrothermal_data_Approvals", "database"); + + b1.WithOwner() + .HasForeignKey("HygrothermalDataId"); + + b1.OwnsOne("Database.Data.Reference", "Statement", b2 => + { + b2.Property("DataApprovalHygrothermalDataId") + .HasColumnType("uuid"); + + b2.Property("DataApprovalId") + .HasColumnType("integer"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DataApprovalHygrothermalDataId", "DataApprovalId"); + + b2.ToTable("hygrothermal_data_Approvals", "database"); + + b2.WithOwner() + .HasForeignKey("DataApprovalHygrothermalDataId", "DataApprovalId"); + + b2.OwnsOne("Database.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDataApprovalHygrothermalDataId") + .HasColumnType("uuid"); + + b3.Property("ReferenceDataApprovalId") + .HasColumnType("integer"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("ArXiv") + .HasColumnType("text"); + + b3.PrimitiveCollection("Authors") + .HasColumnType("text[]"); + + b3.Property("Doi") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Urn") + .HasColumnType("text"); + + b3.Property("WebAddress") + .HasColumnType("text"); + + b3.HasKey("ReferenceDataApprovalHygrothermalDataId", "ReferenceDataApprovalId"); + + b3.ToTable("hygrothermal_data_Approvals", "database"); + + b3.WithOwner() + .HasForeignKey("ReferenceDataApprovalHygrothermalDataId", "ReferenceDataApprovalId"); + }); + + b2.OwnsOne("Database.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDataApprovalHygrothermalDataId") + .HasColumnType("uuid"); + + b3.Property("ReferenceDataApprovalId") + .HasColumnType("integer"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("database.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDataApprovalHygrothermalDataId", "ReferenceDataApprovalId"); + + b3.ToTable("hygrothermal_data_Approvals", "database"); + + b3.WithOwner() + .HasForeignKey("ReferenceDataApprovalHygrothermalDataId", "ReferenceDataApprovalId"); + + b3.OwnsOne("Database.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDataApprovalHygrothermalDataId") + .HasColumnType("uuid"); + + b4.Property("StandardReferenceDataApprovalId") + .HasColumnType("integer"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDataApprovalHygrothermalDataId", "StandardReferenceDataApprovalId"); + + b4.ToTable("hygrothermal_data_Approvals", "database"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDataApprovalHygrothermalDataId", "StandardReferenceDataApprovalId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Statement") + .IsRequired(); + }); + + b.OwnsOne("Database.Data.DataAccessRights", "DataAccessRights", b1 => + { + b1.Property("HygrothermalDataId") + .HasColumnType("uuid"); + + b1.PrimitiveCollection("AllowedApplications") + .HasColumnType("text[]"); + + b1.PrimitiveCollection("AllowedInstitutions") + .HasColumnType("uuid[]"); + + b1.Property("AllowedUserAndQuantity") + .HasColumnType("jsonb"); + + b1.HasKey("HygrothermalDataId"); + + b1.ToTable("hygrothermal_data", "database"); + + b1.WithOwner() + .HasForeignKey("HygrothermalDataId"); + }); + + b.Navigation("AppliedMethod") + .IsRequired(); + + b.Navigation("Approval"); + + b.Navigation("Approvals"); + + b.Navigation("DataAccessRights") + .IsRequired(); + }); + + modelBuilder.Entity("Database.Data.LifeCycleData", b => + { + b.OwnsOne("Database.Data.AppliedMethod", "AppliedMethod", b1 => + { + b1.Property("LifeCycleDataId") + .HasColumnType("uuid"); + + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.HasKey("LifeCycleDataId"); + + b1.ToTable("lifeCycle_data", "database"); + + b1.WithOwner() + .HasForeignKey("LifeCycleDataId"); + + b1.OwnsMany("Database.Data.NamedMethodArgument", "Arguments", b2 => + { + b2.Property("AppliedMethodLifeCycleDataId") + .HasColumnType("uuid"); + + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b2.Property("Id")); + + b2.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b2.Property("Value") + .HasColumnType("jsonb"); + + b2.HasKey("AppliedMethodLifeCycleDataId", "Id"); + + b2.ToTable("lifeCycle_data_Arguments", "database"); + + b2.WithOwner() + .HasForeignKey("AppliedMethodLifeCycleDataId"); + }); + + b1.OwnsMany("Database.Data.NamedMethodSource", "Sources", b2 => + { + b2.Property("AppliedMethodLifeCycleDataId") + .HasColumnType("uuid"); + + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b2.Property("Id")); + + b2.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b2.HasKey("AppliedMethodLifeCycleDataId", "Id"); + + b2.ToTable("lifeCycle_data_Sources", "database"); + + b2.WithOwner() + .HasForeignKey("AppliedMethodLifeCycleDataId"); + + b2.OwnsOne("Database.Data.CrossDatabaseDataReference", "Value", b3 => + { + b3.Property("NamedMethodSourceAppliedMethodLifeCycleDataId") + .HasColumnType("uuid"); + + b3.Property("NamedMethodSourceId") + .HasColumnType("integer"); + + b3.Property("DataId") + .HasColumnType("uuid"); + + b3.Property("DataKind") + .HasColumnType("database.data_kind"); + + b3.Property("DataTimestamp") + .HasColumnType("timestamp with time zone"); + + b3.Property("DatabaseId") + .HasColumnType("uuid"); + + b3.HasKey("NamedMethodSourceAppliedMethodLifeCycleDataId", "NamedMethodSourceId"); + + b3.ToTable("lifeCycle_data_Sources", "database"); + + b3.WithOwner() + .HasForeignKey("NamedMethodSourceAppliedMethodLifeCycleDataId", "NamedMethodSourceId"); + }); + + b2.Navigation("Value") + .IsRequired(); + }); + + b1.Navigation("Arguments"); + + b1.Navigation("Sources"); + }); + + b.OwnsOne("Database.Data.ResponseApproval", "Approval", b1 => + { + b1.Property("LifeCycleDataId") + .HasColumnType("uuid"); + + b1.Property("ApproverId") + .HasColumnType("uuid"); + + b1.Property("KeyFingerprint") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Query") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Signature") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b1.Property("Variables") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValueSql("'{}'"); + + b1.HasKey("LifeCycleDataId"); + + b1.ToTable("lifeCycle_data", "database"); + + b1.WithOwner() + .HasForeignKey("LifeCycleDataId"); + }); + + b.OwnsMany("Database.Data.DataApproval", "Approvals", b1 => + { + b1.Property("LifeCycleDataId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("ApproverId") + .HasColumnType("uuid"); + + b1.Property("KeyFingerprint") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Query") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Signature") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b1.Property("Variables") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValueSql("'{}'"); + + b1.HasKey("LifeCycleDataId", "Id"); + + b1.ToTable("lifeCycle_data_Approvals", "database"); + + b1.WithOwner() + .HasForeignKey("LifeCycleDataId"); + + b1.OwnsOne("Database.Data.Reference", "Statement", b2 => + { + b2.Property("DataApprovalLifeCycleDataId") + .HasColumnType("uuid"); + + b2.Property("DataApprovalId") + .HasColumnType("integer"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DataApprovalLifeCycleDataId", "DataApprovalId"); + + b2.ToTable("lifeCycle_data_Approvals", "database"); + + b2.WithOwner() + .HasForeignKey("DataApprovalLifeCycleDataId", "DataApprovalId"); + + b2.OwnsOne("Database.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDataApprovalLifeCycleDataId") + .HasColumnType("uuid"); + + b3.Property("ReferenceDataApprovalId") + .HasColumnType("integer"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("ArXiv") + .HasColumnType("text"); + + b3.PrimitiveCollection("Authors") + .HasColumnType("text[]"); + + b3.Property("Doi") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Urn") + .HasColumnType("text"); + + b3.Property("WebAddress") + .HasColumnType("text"); + + b3.HasKey("ReferenceDataApprovalLifeCycleDataId", "ReferenceDataApprovalId"); + + b3.ToTable("lifeCycle_data_Approvals", "database"); + + b3.WithOwner() + .HasForeignKey("ReferenceDataApprovalLifeCycleDataId", "ReferenceDataApprovalId"); + }); + + b2.OwnsOne("Database.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDataApprovalLifeCycleDataId") + .HasColumnType("uuid"); + + b3.Property("ReferenceDataApprovalId") + .HasColumnType("integer"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("database.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDataApprovalLifeCycleDataId", "ReferenceDataApprovalId"); + + b3.ToTable("lifeCycle_data_Approvals", "database"); + + b3.WithOwner() + .HasForeignKey("ReferenceDataApprovalLifeCycleDataId", "ReferenceDataApprovalId"); + + b3.OwnsOne("Database.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDataApprovalLifeCycleDataId") + .HasColumnType("uuid"); + + b4.Property("StandardReferenceDataApprovalId") + .HasColumnType("integer"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDataApprovalLifeCycleDataId", "StandardReferenceDataApprovalId"); + + b4.ToTable("lifeCycle_data_Approvals", "database"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDataApprovalLifeCycleDataId", "StandardReferenceDataApprovalId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Statement") + .IsRequired(); + }); + + b.OwnsOne("Database.Data.DataAccessRights", "DataAccessRights", b1 => + { + b1.Property("LifeCycleDataId") + .HasColumnType("uuid"); + + b1.PrimitiveCollection("AllowedApplications") + .HasColumnType("text[]"); + + b1.PrimitiveCollection("AllowedInstitutions") + .HasColumnType("uuid[]"); + + b1.Property("AllowedUserAndQuantity") + .HasColumnType("jsonb"); + + b1.HasKey("LifeCycleDataId"); + + b1.ToTable("lifeCycle_data", "database"); + + b1.WithOwner() + .HasForeignKey("LifeCycleDataId"); + }); + + b.Navigation("AppliedMethod") + .IsRequired(); + + b.Navigation("Approval"); + + b.Navigation("Approvals"); + + b.Navigation("DataAccessRights") + .IsRequired(); + }); + + modelBuilder.Entity("Database.Data.OpticalData", b => + { + b.OwnsMany("Database.Data.CielabColor", "CielabColors", b1 => + { + b1.Property("OpticalDataId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("AStar") + .HasColumnType("double precision"); + + b1.Property("BStar") + .HasColumnType("double precision"); + + b1.Property("Illuminant") + .HasColumnType("database.illuminant"); + + b1.Property("LStar") + .HasColumnType("double precision"); + + b1.Property("Observer") + .HasColumnType("database.calorimetric_observer"); + + b1.HasKey("OpticalDataId", "Id"); + + b1.ToTable("CielabColor", "database", t => + { + t.HasCheckConstraint("CK_OpticalData_CielabColors_LStar", "\"LStar\" >= 0.0 AND \"LStar\" <= 100.0"); + }); + + b1.WithOwner() + .HasForeignKey("OpticalDataId"); + }); + + b.OwnsOne("Database.Data.AppliedMethod", "AppliedMethod", b1 => + { + b1.Property("OpticalDataId") + .HasColumnType("uuid"); + + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.HasKey("OpticalDataId"); + + b1.ToTable("optical_data", "database"); + + b1.WithOwner() + .HasForeignKey("OpticalDataId"); + + b1.OwnsMany("Database.Data.NamedMethodArgument", "Arguments", b2 => + { + b2.Property("AppliedMethodOpticalDataId") + .HasColumnType("uuid"); + + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b2.Property("Id")); + + b2.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b2.Property("Value") + .HasColumnType("jsonb"); + + b2.HasKey("AppliedMethodOpticalDataId", "Id"); + + b2.ToTable("optical_data_Arguments", "database"); + + b2.WithOwner() + .HasForeignKey("AppliedMethodOpticalDataId"); + }); + + b1.OwnsMany("Database.Data.NamedMethodSource", "Sources", b2 => + { + b2.Property("AppliedMethodOpticalDataId") + .HasColumnType("uuid"); + + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b2.Property("Id")); + + b2.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b2.HasKey("AppliedMethodOpticalDataId", "Id"); + + b2.ToTable("optical_data_Sources", "database"); + + b2.WithOwner() + .HasForeignKey("AppliedMethodOpticalDataId"); + + b2.OwnsOne("Database.Data.CrossDatabaseDataReference", "Value", b3 => + { + b3.Property("NamedMethodSourceAppliedMethodOpticalDataId") + .HasColumnType("uuid"); + + b3.Property("NamedMethodSourceId") + .HasColumnType("integer"); + + b3.Property("DataId") + .HasColumnType("uuid"); + + b3.Property("DataKind") + .HasColumnType("database.data_kind"); + + b3.Property("DataTimestamp") + .HasColumnType("timestamp with time zone"); + + b3.Property("DatabaseId") + .HasColumnType("uuid"); + + b3.HasKey("NamedMethodSourceAppliedMethodOpticalDataId", "NamedMethodSourceId"); + + b3.ToTable("optical_data_Sources", "database"); + + b3.WithOwner() + .HasForeignKey("NamedMethodSourceAppliedMethodOpticalDataId", "NamedMethodSourceId"); + }); + + b2.Navigation("Value") + .IsRequired(); + }); + + b1.Navigation("Arguments"); + + b1.Navigation("Sources"); + }); + + b.OwnsOne("Database.Data.ResponseApproval", "Approval", b1 => + { + b1.Property("OpticalDataId") + .HasColumnType("uuid"); + + b1.Property("ApproverId") + .HasColumnType("uuid"); + + b1.Property("KeyFingerprint") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Query") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Signature") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b1.Property("Variables") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValueSql("'{}'"); + + b1.HasKey("OpticalDataId"); + + b1.ToTable("optical_data", "database"); + + b1.WithOwner() + .HasForeignKey("OpticalDataId"); + }); + + b.OwnsMany("Database.Data.DataApproval", "Approvals", b1 => + { + b1.Property("OpticalDataId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("ApproverId") + .HasColumnType("uuid"); + + b1.Property("KeyFingerprint") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Query") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Signature") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b1.Property("Variables") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValueSql("'{}'"); + + b1.HasKey("OpticalDataId", "Id"); + + b1.ToTable("optical_data_Approvals", "database"); + + b1.WithOwner() + .HasForeignKey("OpticalDataId"); + + b1.OwnsOne("Database.Data.Reference", "Statement", b2 => + { + b2.Property("DataApprovalOpticalDataId") + .HasColumnType("uuid"); + + b2.Property("DataApprovalId") + .HasColumnType("integer"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DataApprovalOpticalDataId", "DataApprovalId"); + + b2.ToTable("optical_data_Approvals", "database"); + + b2.WithOwner() + .HasForeignKey("DataApprovalOpticalDataId", "DataApprovalId"); + + b2.OwnsOne("Database.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDataApprovalOpticalDataId") + .HasColumnType("uuid"); + + b3.Property("ReferenceDataApprovalId") + .HasColumnType("integer"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("ArXiv") + .HasColumnType("text"); + + b3.PrimitiveCollection("Authors") + .HasColumnType("text[]"); + + b3.Property("Doi") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Urn") + .HasColumnType("text"); + + b3.Property("WebAddress") + .HasColumnType("text"); + + b3.HasKey("ReferenceDataApprovalOpticalDataId", "ReferenceDataApprovalId"); + + b3.ToTable("optical_data_Approvals", "database"); + + b3.WithOwner() + .HasForeignKey("ReferenceDataApprovalOpticalDataId", "ReferenceDataApprovalId"); + }); + + b2.OwnsOne("Database.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDataApprovalOpticalDataId") + .HasColumnType("uuid"); + + b3.Property("ReferenceDataApprovalId") + .HasColumnType("integer"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("database.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDataApprovalOpticalDataId", "ReferenceDataApprovalId"); + + b3.ToTable("optical_data_Approvals", "database"); + + b3.WithOwner() + .HasForeignKey("ReferenceDataApprovalOpticalDataId", "ReferenceDataApprovalId"); + + b3.OwnsOne("Database.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDataApprovalOpticalDataId") + .HasColumnType("uuid"); + + b4.Property("StandardReferenceDataApprovalId") + .HasColumnType("integer"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDataApprovalOpticalDataId", "StandardReferenceDataApprovalId"); + + b4.ToTable("optical_data_Approvals", "database"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDataApprovalOpticalDataId", "StandardReferenceDataApprovalId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Statement") + .IsRequired(); + }); + + b.OwnsOne("Database.Data.DataAccessRights", "DataAccessRights", b1 => + { + b1.Property("OpticalDataId") + .HasColumnType("uuid"); + + b1.PrimitiveCollection("AllowedApplications") + .HasColumnType("text[]"); + + b1.PrimitiveCollection("AllowedInstitutions") + .HasColumnType("uuid[]"); + + b1.Property("AllowedUserAndQuantity") + .HasColumnType("jsonb"); + + b1.HasKey("OpticalDataId"); + + b1.ToTable("optical_data", "database"); + + b1.WithOwner() + .HasForeignKey("OpticalDataId"); + }); + + b.Navigation("AppliedMethod") + .IsRequired(); + + b.Navigation("Approval"); + + b.Navigation("Approvals"); + + b.Navigation("CielabColors"); + + b.Navigation("DataAccessRights") + .IsRequired(); + }); + + modelBuilder.Entity("Database.Data.PhotovoltaicData", b => + { + b.OwnsOne("Database.Data.AppliedMethod", "AppliedMethod", b1 => + { + b1.Property("PhotovoltaicDataId") + .HasColumnType("uuid"); + + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.HasKey("PhotovoltaicDataId"); + + b1.ToTable("photovoltaic_data", "database"); + + b1.WithOwner() + .HasForeignKey("PhotovoltaicDataId"); + + b1.OwnsMany("Database.Data.NamedMethodArgument", "Arguments", b2 => + { + b2.Property("AppliedMethodPhotovoltaicDataId") + .HasColumnType("uuid"); + + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b2.Property("Id")); + + b2.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b2.Property("Value") + .HasColumnType("jsonb"); + + b2.HasKey("AppliedMethodPhotovoltaicDataId", "Id"); + + b2.ToTable("photovoltaic_data_Arguments", "database"); + + b2.WithOwner() + .HasForeignKey("AppliedMethodPhotovoltaicDataId"); + }); + + b1.OwnsMany("Database.Data.NamedMethodSource", "Sources", b2 => + { + b2.Property("AppliedMethodPhotovoltaicDataId") + .HasColumnType("uuid"); + + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b2.Property("Id")); + + b2.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b2.HasKey("AppliedMethodPhotovoltaicDataId", "Id"); + + b2.ToTable("photovoltaic_data_Sources", "database"); + + b2.WithOwner() + .HasForeignKey("AppliedMethodPhotovoltaicDataId"); + + b2.OwnsOne("Database.Data.CrossDatabaseDataReference", "Value", b3 => + { + b3.Property("NamedMethodSourceAppliedMethodPhotovoltaicDataId") + .HasColumnType("uuid"); + + b3.Property("NamedMethodSourceId") + .HasColumnType("integer"); + + b3.Property("DataId") + .HasColumnType("uuid"); + + b3.Property("DataKind") + .HasColumnType("database.data_kind"); + + b3.Property("DataTimestamp") + .HasColumnType("timestamp with time zone"); + + b3.Property("DatabaseId") + .HasColumnType("uuid"); + + b3.HasKey("NamedMethodSourceAppliedMethodPhotovoltaicDataId", "NamedMethodSourceId"); + + b3.ToTable("photovoltaic_data_Sources", "database"); + + b3.WithOwner() + .HasForeignKey("NamedMethodSourceAppliedMethodPhotovoltaicDataId", "NamedMethodSourceId"); + }); + + b2.Navigation("Value") + .IsRequired(); + }); + + b1.Navigation("Arguments"); + + b1.Navigation("Sources"); + }); + + b.OwnsOne("Database.Data.ResponseApproval", "Approval", b1 => + { + b1.Property("PhotovoltaicDataId") + .HasColumnType("uuid"); + + b1.Property("ApproverId") + .HasColumnType("uuid"); + + b1.Property("KeyFingerprint") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Query") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Signature") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b1.Property("Variables") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValueSql("'{}'"); + + b1.HasKey("PhotovoltaicDataId"); + + b1.ToTable("photovoltaic_data", "database"); + + b1.WithOwner() + .HasForeignKey("PhotovoltaicDataId"); + }); + + b.OwnsMany("Database.Data.DataApproval", "Approvals", b1 => + { + b1.Property("PhotovoltaicDataId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("ApproverId") + .HasColumnType("uuid"); + + b1.Property("KeyFingerprint") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Query") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Signature") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b1.Property("Variables") + .ValueGeneratedOnAdd() + .HasColumnType("jsonb") + .HasDefaultValueSql("'{}'"); + + b1.HasKey("PhotovoltaicDataId", "Id"); + + b1.ToTable("photovoltaic_data_Approvals", "database"); + + b1.WithOwner() + .HasForeignKey("PhotovoltaicDataId"); + + b1.OwnsOne("Database.Data.Reference", "Statement", b2 => + { + b2.Property("DataApprovalPhotovoltaicDataId") + .HasColumnType("uuid"); + + b2.Property("DataApprovalId") + .HasColumnType("integer"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DataApprovalPhotovoltaicDataId", "DataApprovalId"); + + b2.ToTable("photovoltaic_data_Approvals", "database"); + + b2.WithOwner() + .HasForeignKey("DataApprovalPhotovoltaicDataId", "DataApprovalId"); + + b2.OwnsOne("Database.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDataApprovalPhotovoltaicDataId") + .HasColumnType("uuid"); + + b3.Property("ReferenceDataApprovalId") + .HasColumnType("integer"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("ArXiv") + .HasColumnType("text"); + + b3.PrimitiveCollection("Authors") + .HasColumnType("text[]"); + + b3.Property("Doi") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Urn") + .HasColumnType("text"); + + b3.Property("WebAddress") + .HasColumnType("text"); + + b3.HasKey("ReferenceDataApprovalPhotovoltaicDataId", "ReferenceDataApprovalId"); + + b3.ToTable("photovoltaic_data_Approvals", "database"); + + b3.WithOwner() + .HasForeignKey("ReferenceDataApprovalPhotovoltaicDataId", "ReferenceDataApprovalId"); + }); + + b2.OwnsOne("Database.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDataApprovalPhotovoltaicDataId") + .HasColumnType("uuid"); + + b3.Property("ReferenceDataApprovalId") + .HasColumnType("integer"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("database.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDataApprovalPhotovoltaicDataId", "ReferenceDataApprovalId"); + + b3.ToTable("photovoltaic_data_Approvals", "database"); + + b3.WithOwner() + .HasForeignKey("ReferenceDataApprovalPhotovoltaicDataId", "ReferenceDataApprovalId"); + + b3.OwnsOne("Database.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDataApprovalPhotovoltaicDataId") + .HasColumnType("uuid"); + + b4.Property("StandardReferenceDataApprovalId") + .HasColumnType("integer"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDataApprovalPhotovoltaicDataId", "StandardReferenceDataApprovalId"); + + b4.ToTable("photovoltaic_data_Approvals", "database"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDataApprovalPhotovoltaicDataId", "StandardReferenceDataApprovalId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Statement") + .IsRequired(); + }); + + b.OwnsOne("Database.Data.DataAccessRights", "DataAccessRights", b1 => + { + b1.Property("PhotovoltaicDataId") + .HasColumnType("uuid"); + + b1.PrimitiveCollection("AllowedApplications") + .HasColumnType("text[]"); + + b1.PrimitiveCollection("AllowedInstitutions") + .HasColumnType("uuid[]"); + + b1.Property("AllowedUserAndQuantity") + .HasColumnType("jsonb"); + + b1.HasKey("PhotovoltaicDataId"); + + b1.ToTable("photovoltaic_data", "database"); + + b1.WithOwner() + .HasForeignKey("PhotovoltaicDataId"); + }); + + b.Navigation("AppliedMethod") + .IsRequired(); + + b.Navigation("Approval"); + + b.Navigation("Approvals"); + + b.Navigation("DataAccessRights") + .IsRequired(); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => + { + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("Database.Data.CalorimetricData", b => + { + b.Navigation("Resources"); + }); + + modelBuilder.Entity("Database.Data.GeometricData", b => + { + b.Navigation("Resources"); + }); + + modelBuilder.Entity("Database.Data.GetHttpsResource", b => + { + b.Navigation("Children"); + }); + + modelBuilder.Entity("Database.Data.HygrothermalData", b => + { + b.Navigation("Resources"); + }); + + modelBuilder.Entity("Database.Data.LifeCycleData", b => + { + b.Navigation("Resources"); + }); + + modelBuilder.Entity("Database.Data.OpticalData", b => + { + b.Navigation("Resources"); + }); + + modelBuilder.Entity("Database.Data.PhotovoltaicData", b => + { + b.Navigation("Resources"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => + { + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Migrations/20260527161543_MakeEntitiesAuditable.cs b/backend/src/Migrations/20260527161543_MakeEntitiesAuditable.cs new file mode 100644 index 00000000..69c81dca --- /dev/null +++ b/backend/src/Migrations/20260527161543_MakeEntitiesAuditable.cs @@ -0,0 +1,407 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Database.Migrations +{ + /// + public partial class MakeEntitiesAuditable : Migration + { + /// + [SuppressMessage("Performance", "CA1861")] + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CreatedAt", + schema: "database", + table: "user", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "database", + table: "user", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + schema: "database", + table: "photovoltaic_data", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()", + oldClrType: typeof(OffsetDateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "database", + table: "photovoltaic_data", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + schema: "database", + table: "optical_data", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()", + oldClrType: typeof(OffsetDateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "database", + table: "optical_data", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + schema: "database", + table: "lifeCycle_data", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()", + oldClrType: typeof(OffsetDateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "database", + table: "lifeCycle_data", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "CreatedAt", + schema: "database", + table: "institution_access_rights", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "database", + table: "institution_access_rights", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + schema: "database", + table: "hygrothermal_data", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()", + oldClrType: typeof(OffsetDateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "database", + table: "hygrothermal_data", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "CreatedAt", + schema: "database", + table: "get_https_resource", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "database", + table: "get_https_resource", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + schema: "database", + table: "geometric_data", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()", + oldClrType: typeof(OffsetDateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "database", + table: "geometric_data", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + schema: "database", + table: "calorimetric_data", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()", + oldClrType: typeof(OffsetDateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "database", + table: "calorimetric_data", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.CreateIndex( + name: "IX_user_CreatedAt_Id", + schema: "database", + table: "user", + columns: new[] { "CreatedAt", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_photovoltaic_data_CreatedAt_Id", + schema: "database", + table: "photovoltaic_data", + columns: new[] { "CreatedAt", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_optical_data_CreatedAt_Id", + schema: "database", + table: "optical_data", + columns: new[] { "CreatedAt", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_lifeCycle_data_CreatedAt_Id", + schema: "database", + table: "lifeCycle_data", + columns: new[] { "CreatedAt", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_institution_access_rights_CreatedAt_Id", + schema: "database", + table: "institution_access_rights", + columns: new[] { "CreatedAt", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_hygrothermal_data_CreatedAt_Id", + schema: "database", + table: "hygrothermal_data", + columns: new[] { "CreatedAt", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_get_https_resource_CreatedAt_Id", + schema: "database", + table: "get_https_resource", + columns: new[] { "CreatedAt", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_geometric_data_CreatedAt_Id", + schema: "database", + table: "geometric_data", + columns: new[] { "CreatedAt", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_calorimetric_data_CreatedAt_Id", + schema: "database", + table: "calorimetric_data", + columns: new[] { "CreatedAt", "Id" }, + unique: true); + } + + /// + [SuppressMessage("Performance", "CA1861")] + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_user_CreatedAt_Id", + schema: "database", + table: "user"); + + migrationBuilder.DropIndex( + name: "IX_photovoltaic_data_CreatedAt_Id", + schema: "database", + table: "photovoltaic_data"); + + migrationBuilder.DropIndex( + name: "IX_optical_data_CreatedAt_Id", + schema: "database", + table: "optical_data"); + + migrationBuilder.DropIndex( + name: "IX_lifeCycle_data_CreatedAt_Id", + schema: "database", + table: "lifeCycle_data"); + + migrationBuilder.DropIndex( + name: "IX_institution_access_rights_CreatedAt_Id", + schema: "database", + table: "institution_access_rights"); + + migrationBuilder.DropIndex( + name: "IX_hygrothermal_data_CreatedAt_Id", + schema: "database", + table: "hygrothermal_data"); + + migrationBuilder.DropIndex( + name: "IX_get_https_resource_CreatedAt_Id", + schema: "database", + table: "get_https_resource"); + + migrationBuilder.DropIndex( + name: "IX_geometric_data_CreatedAt_Id", + schema: "database", + table: "geometric_data"); + + migrationBuilder.DropIndex( + name: "IX_calorimetric_data_CreatedAt_Id", + schema: "database", + table: "calorimetric_data"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + schema: "database", + table: "user"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "database", + table: "user"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "database", + table: "photovoltaic_data"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "database", + table: "optical_data"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "database", + table: "lifeCycle_data"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + schema: "database", + table: "institution_access_rights"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "database", + table: "institution_access_rights"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "database", + table: "hygrothermal_data"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + schema: "database", + table: "get_https_resource"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "database", + table: "get_https_resource"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "database", + table: "geometric_data"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "database", + table: "calorimetric_data"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + schema: "database", + table: "photovoltaic_data", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTimeOffset), + oldType: "timestamp with time zone", + oldDefaultValueSql: "now()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + schema: "database", + table: "optical_data", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTimeOffset), + oldType: "timestamp with time zone", + oldDefaultValueSql: "now()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + schema: "database", + table: "lifeCycle_data", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTimeOffset), + oldType: "timestamp with time zone", + oldDefaultValueSql: "now()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + schema: "database", + table: "hygrothermal_data", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTimeOffset), + oldType: "timestamp with time zone", + oldDefaultValueSql: "now()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + schema: "database", + table: "geometric_data", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTimeOffset), + oldType: "timestamp with time zone", + oldDefaultValueSql: "now()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + schema: "database", + table: "calorimetric_data", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTimeOffset), + oldType: "timestamp with time zone", + oldDefaultValueSql: "now()"); + } + } +} \ No newline at end of file diff --git a/backend/src/Migrations/ApplicationDbContextModelSnapshot.cs b/backend/src/Migrations/ApplicationDbContextModelSnapshot.cs index ef9d6da4..1514594a 100644 --- a/backend/src/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/backend/src/Migrations/ApplicationDbContextModelSnapshot.cs @@ -22,7 +22,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("database") - .HasAnnotation("ProductVersion", "10.0.2") + .HasAnnotation("ProductVersion", "10.0.8") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "database", "calorimetric_observer", new[] { "ten_degrees", "two_degrees" }); @@ -46,8 +46,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ComponentId") .HasColumnType("uuid"); - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("CreatorId") .HasColumnType("uuid"); @@ -73,6 +75,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("double precision[]"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("UserId") .HasColumnType("uuid"); @@ -88,6 +95,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + b.ToTable("calorimetric_data", "database"); }); @@ -101,8 +111,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ComponentId") .HasColumnType("uuid"); - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("CreatorId") .HasColumnType("uuid"); @@ -128,6 +140,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("double precision[]"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("UserId") .HasColumnType("uuid"); @@ -147,6 +164,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + b.ToTable("geometric_data", "database"); }); @@ -160,6 +180,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CalorimetricDataId") .HasColumnType("uuid"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("DataFormatId") .HasColumnType("uuid"); @@ -191,6 +216,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("PhotovoltaicDataId") .HasColumnType("uuid"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("Version") .IsConcurrencyToken() .ValueGeneratedOnAddOrUpdate() @@ -225,6 +255,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsUnique() .HasFilter("\"PhotovoltaicDataId\" IS NOT NULL AND \"ParentId\" IS NULL"); + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + b.ToTable("get_https_resource", "database", t => { t.HasTrigger("LC_TRIGGER_data_id_cannot_change"); @@ -255,8 +288,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ComponentId") .HasColumnType("uuid"); - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("CreatorId") .HasColumnType("uuid"); @@ -274,6 +309,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("PublishingState") .HasColumnType("database.publishing_state"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("UserId") .HasColumnType("uuid"); @@ -289,6 +329,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + b.ToTable("hygrothermal_data", "database"); }); @@ -305,12 +348,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("AllowedUserCount") .HasColumnType("bigint"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("InstitutionId") .HasColumnType("uuid"); b.Property("Period") .HasColumnType("interval"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.PrimitiveCollection>("UserAlreadyAccessed") .IsRequired() .HasColumnType("uuid[]"); @@ -326,6 +379,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("InstitutionId") .IsUnique(); + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + b.ToTable("institution_access_rights", "database"); }); @@ -339,8 +395,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ComponentId") .HasColumnType("uuid"); - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("CreatorId") .HasColumnType("uuid"); @@ -358,6 +416,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("PublishingState") .HasColumnType("database.publishing_state"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("UserId") .HasColumnType("uuid"); @@ -373,6 +436,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + b.ToTable("lifeCycle_data", "database"); }); @@ -393,8 +459,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ComponentId") .HasColumnType("uuid"); - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("CreatorId") .HasColumnType("uuid"); @@ -438,6 +506,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Type") .HasColumnType("database.optical_component_type"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("UserId") .HasColumnType("uuid"); @@ -453,6 +526,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + b.ToTable("optical_data", "database"); }); @@ -466,8 +542,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ComponentId") .HasColumnType("uuid"); - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("CreatorId") .HasColumnType("uuid"); @@ -485,6 +563,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("PublishingState") .HasColumnType("database.publishing_state"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("UserId") .HasColumnType("uuid"); @@ -500,6 +583,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + b.ToTable("photovoltaic_data", "database"); }); @@ -510,6 +596,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid") .HasDefaultValueSql("gen_random_uuid()"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("Name") .IsRequired() .HasColumnType("text"); @@ -518,6 +609,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("Version") .IsConcurrencyToken() .ValueGeneratedOnAddOrUpdate() @@ -526,6 +622,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + b.ToTable("user", "database"); }); @@ -1088,7 +1187,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.PrimitiveCollection("AllowedInstitutions") .HasColumnType("uuid[]"); - b1.Property>("AllowedUserAndQuantity") + b1.Property("AllowedUserAndQuantity") .HasColumnType("jsonb"); b1.HasKey("CalorimetricDataId"); @@ -1442,7 +1541,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.PrimitiveCollection("AllowedInstitutions") .HasColumnType("uuid[]"); - b1.Property>("AllowedUserAndQuantity") + b1.Property("AllowedUserAndQuantity") .HasColumnType("jsonb"); b1.HasKey("GeometricDataId"); @@ -1925,7 +2024,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.PrimitiveCollection("AllowedInstitutions") .HasColumnType("uuid[]"); - b1.Property>("AllowedUserAndQuantity") + b1.Property("AllowedUserAndQuantity") .HasColumnType("jsonb"); b1.HasKey("HygrothermalDataId"); @@ -2279,7 +2378,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.PrimitiveCollection("AllowedInstitutions") .HasColumnType("uuid[]"); - b1.Property>("AllowedUserAndQuantity") + b1.Property("AllowedUserAndQuantity") .HasColumnType("jsonb"); b1.HasKey("LifeCycleDataId"); @@ -2670,7 +2769,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.PrimitiveCollection("AllowedInstitutions") .HasColumnType("uuid[]"); - b1.Property>("AllowedUserAndQuantity") + b1.Property("AllowedUserAndQuantity") .HasColumnType("jsonb"); b1.HasKey("OpticalDataId"); @@ -3026,7 +3125,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.PrimitiveCollection("AllowedInstitutions") .HasColumnType("uuid[]"); - b1.Property>("AllowedUserAndQuantity") + b1.Property("AllowedUserAndQuantity") .HasColumnType("jsonb"); b1.HasKey("PhotovoltaicDataId"); diff --git a/backend/src/Migrations/migrate.sql b/backend/src/Migrations/migrate.sql index afc66795..dc11d21f 100644 --- a/backend/src/Migrations/migrate.sql +++ b/backend/src/Migrations/migrate.sql @@ -2047,3 +2047,203 @@ BEGIN END $EF$; COMMIT; +START TRANSACTION; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database."user" ADD "CreatedAt" timestamp with time zone NOT NULL DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database."user" ADD "UpdatedAt" timestamp with time zone NOT NULL DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.photovoltaic_data ALTER COLUMN "CreatedAt" SET DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.photovoltaic_data ADD "UpdatedAt" timestamp with time zone NOT NULL DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.optical_data ALTER COLUMN "CreatedAt" SET DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.optical_data ADD "UpdatedAt" timestamp with time zone NOT NULL DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database."lifeCycle_data" ALTER COLUMN "CreatedAt" SET DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database."lifeCycle_data" ADD "UpdatedAt" timestamp with time zone NOT NULL DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.institution_access_rights ADD "CreatedAt" timestamp with time zone NOT NULL DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.institution_access_rights ADD "UpdatedAt" timestamp with time zone NOT NULL DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.hygrothermal_data ALTER COLUMN "CreatedAt" SET DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.hygrothermal_data ADD "UpdatedAt" timestamp with time zone NOT NULL DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.get_https_resource ADD "CreatedAt" timestamp with time zone NOT NULL DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.get_https_resource ADD "UpdatedAt" timestamp with time zone NOT NULL DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.geometric_data ALTER COLUMN "CreatedAt" SET DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.geometric_data ADD "UpdatedAt" timestamp with time zone NOT NULL DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.calorimetric_data ALTER COLUMN "CreatedAt" SET DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.calorimetric_data ADD "UpdatedAt" timestamp with time zone NOT NULL DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + CREATE UNIQUE INDEX "IX_user_CreatedAt_Id" ON database."user" ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + CREATE UNIQUE INDEX "IX_photovoltaic_data_CreatedAt_Id" ON database.photovoltaic_data ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + CREATE UNIQUE INDEX "IX_optical_data_CreatedAt_Id" ON database.optical_data ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + CREATE UNIQUE INDEX "IX_lifeCycle_data_CreatedAt_Id" ON database."lifeCycle_data" ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + CREATE UNIQUE INDEX "IX_institution_access_rights_CreatedAt_Id" ON database.institution_access_rights ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + CREATE UNIQUE INDEX "IX_hygrothermal_data_CreatedAt_Id" ON database.hygrothermal_data ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + CREATE UNIQUE INDEX "IX_get_https_resource_CreatedAt_Id" ON database.get_https_resource ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + CREATE UNIQUE INDEX "IX_geometric_data_CreatedAt_Id" ON database.geometric_data ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + CREATE UNIQUE INDEX "IX_calorimetric_data_CreatedAt_Id" ON database.calorimetric_data ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") + VALUES ('20260527161543_MakeEntitiesAuditable', '10.0.8'); + END IF; +END $EF$; +COMMIT; + diff --git a/backend/src/Migrations/migrate_from_20260316165622_AddLifeCycleData_to_20260527161543_MakeEntitiesAuditable.sql b/backend/src/Migrations/migrate_from_20260316165622_AddLifeCycleData_to_20260527161543_MakeEntitiesAuditable.sql new file mode 100644 index 00000000..0145846a --- /dev/null +++ b/backend/src/Migrations/migrate_from_20260316165622_AddLifeCycleData_to_20260527161543_MakeEntitiesAuditable.sql @@ -0,0 +1,200 @@ +START TRANSACTION; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database."user" ADD "CreatedAt" timestamp with time zone NOT NULL DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database."user" ADD "UpdatedAt" timestamp with time zone NOT NULL DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.photovoltaic_data ALTER COLUMN "CreatedAt" SET DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.photovoltaic_data ADD "UpdatedAt" timestamp with time zone NOT NULL DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.optical_data ALTER COLUMN "CreatedAt" SET DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.optical_data ADD "UpdatedAt" timestamp with time zone NOT NULL DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database."lifeCycle_data" ALTER COLUMN "CreatedAt" SET DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database."lifeCycle_data" ADD "UpdatedAt" timestamp with time zone NOT NULL DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.institution_access_rights ADD "CreatedAt" timestamp with time zone NOT NULL DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.institution_access_rights ADD "UpdatedAt" timestamp with time zone NOT NULL DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.hygrothermal_data ALTER COLUMN "CreatedAt" SET DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.hygrothermal_data ADD "UpdatedAt" timestamp with time zone NOT NULL DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.get_https_resource ADD "CreatedAt" timestamp with time zone NOT NULL DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.get_https_resource ADD "UpdatedAt" timestamp with time zone NOT NULL DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.geometric_data ALTER COLUMN "CreatedAt" SET DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.geometric_data ADD "UpdatedAt" timestamp with time zone NOT NULL DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.calorimetric_data ALTER COLUMN "CreatedAt" SET DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.calorimetric_data ADD "UpdatedAt" timestamp with time zone NOT NULL DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + CREATE UNIQUE INDEX "IX_user_CreatedAt_Id" ON database."user" ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + CREATE UNIQUE INDEX "IX_photovoltaic_data_CreatedAt_Id" ON database.photovoltaic_data ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + CREATE UNIQUE INDEX "IX_optical_data_CreatedAt_Id" ON database.optical_data ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + CREATE UNIQUE INDEX "IX_lifeCycle_data_CreatedAt_Id" ON database."lifeCycle_data" ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + CREATE UNIQUE INDEX "IX_institution_access_rights_CreatedAt_Id" ON database.institution_access_rights ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + CREATE UNIQUE INDEX "IX_hygrothermal_data_CreatedAt_Id" ON database.hygrothermal_data ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + CREATE UNIQUE INDEX "IX_get_https_resource_CreatedAt_Id" ON database.get_https_resource ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + CREATE UNIQUE INDEX "IX_geometric_data_CreatedAt_Id" ON database.geometric_data ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + CREATE UNIQUE INDEX "IX_calorimetric_data_CreatedAt_Id" ON database.calorimetric_data ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") + VALUES ('20260527161543_MakeEntitiesAuditable', '10.0.8'); + END IF; +END $EF$; +COMMIT; + diff --git a/backend/src/Migrations/rollback_from_20260527161543_MakeEntitiesAuditable_to_20260316165622_AddLifeCycleData.sql b/backend/src/Migrations/rollback_from_20260527161543_MakeEntitiesAuditable_to_20260316165622_AddLifeCycleData.sql new file mode 100644 index 00000000..8fbaad92 --- /dev/null +++ b/backend/src/Migrations/rollback_from_20260527161543_MakeEntitiesAuditable_to_20260316165622_AddLifeCycleData.sql @@ -0,0 +1,172 @@ +START TRANSACTION; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + DROP INDEX database."IX_user_CreatedAt_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + DROP INDEX database."IX_photovoltaic_data_CreatedAt_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + DROP INDEX database."IX_optical_data_CreatedAt_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + DROP INDEX database."IX_lifeCycle_data_CreatedAt_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + DROP INDEX database."IX_institution_access_rights_CreatedAt_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + DROP INDEX database."IX_hygrothermal_data_CreatedAt_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + DROP INDEX database."IX_get_https_resource_CreatedAt_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + DROP INDEX database."IX_geometric_data_CreatedAt_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + DROP INDEX database."IX_calorimetric_data_CreatedAt_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database."user" DROP COLUMN "CreatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database."user" DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.photovoltaic_data DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.optical_data DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database."lifeCycle_data" DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.institution_access_rights DROP COLUMN "CreatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.institution_access_rights DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.hygrothermal_data DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.get_https_resource DROP COLUMN "CreatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.get_https_resource DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.geometric_data DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.calorimetric_data DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.photovoltaic_data ALTER COLUMN "CreatedAt" DROP DEFAULT; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.optical_data ALTER COLUMN "CreatedAt" DROP DEFAULT; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database."lifeCycle_data" ALTER COLUMN "CreatedAt" DROP DEFAULT; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.hygrothermal_data ALTER COLUMN "CreatedAt" DROP DEFAULT; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.geometric_data ALTER COLUMN "CreatedAt" DROP DEFAULT; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + ALTER TABLE database.calorimetric_data ALTER COLUMN "CreatedAt" DROP DEFAULT; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable') THEN + DELETE FROM "__EFMigrationsHistory" + WHERE "MigrationId" = '20260527161543_MakeEntitiesAuditable'; + END IF; +END $EF$; +COMMIT; + diff --git a/backend/src/Startup.cs b/backend/src/Startup.cs index ab086e01..37859ca7 100644 --- a/backend/src/Startup.cs +++ b/backend/src/Startup.cs @@ -218,7 +218,7 @@ DbContextOptionsBuilder options connectionStringBuilder.ConnectionString, _ => _ // Keep version in sync with the one in ./docker-compose.*.yaml - .SetPostgresVersion(13, 23) + .SetPostgresVersion(18, 4) .UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery) // https://learn.microsoft.com/en-us/ef/core/querying/single-split-queries#enabling-split-queries-globally .UseNodaTime() // https://www.npgsql.org/efcore/mapping/enum.html @@ -301,7 +301,6 @@ public static void ConfigureApiRequests(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); } diff --git a/backend/test/Database.Tests.csproj b/backend/test/Database.Tests.csproj index e052358d..f8bf7d19 100644 --- a/backend/test/Database.Tests.csproj +++ b/backend/test/Database.Tests.csproj @@ -14,7 +14,7 @@ - + diff --git a/backend/test/Integration/GraphQl/__snapshots__/GraphQlSchemaTests.IsUnchanged.snap b/backend/test/Integration/GraphQl/__snapshots__/GraphQlSchemaTests.IsUnchanged.snap index 37a3c68f..e4b69ac0 100644 --- a/backend/test/Integration/GraphQl/__snapshots__/GraphQlSchemaTests.IsUnchanged.snap +++ b/backend/test/Integration/GraphQl/__snapshots__/GraphQlSchemaTests.IsUnchanged.snap @@ -3,52 +3,399 @@ schema { mutation: Mutation } -interface Approval { - approverId: Uuid! - keyFingerprint: String! - message: String! - query: String! - signature: String! - timestamp: DateTime! - variables: Any! -} - -interface Data { - appliedMethod: AppliedMethod! - approval: ResponseApproval! - approvals: [DataApproval!]! - componentId: Uuid! - createdAt: DateTime! - creatorId: Uuid! - dataAccessRights: DataAccessRights! - databaseId: Uuid! - description: String - isRestrictedByApplication(applicationId: String!): Boolean! - isRestrictedByInstitutions(institutions: [Uuid!]!): Boolean! - isRestrictedByUser(alreadyAccesedCount: NonNegativeInt! uuid: Uuid!): Boolean! - locale: Locale! - name: String - resources: [GetHttpsResource!]! - resourceTree: GetHttpsResourceTree! - timestamp: DateTime! - userId: Uuid - uuid: Uuid! - warnings: [String!]! -} - -interface GetHttpsResourceTreeVertex { - value: GetHttpsResource! - vertexId: ID -} - -"The node interface is implemented by entities that have a global unique identifier." -interface Node { - id: ID! +type Query { + allCalorimetricData( + "Returns the elements in the list that come after the specified cursor." + after: String + "Returns the elements in the list that come before the specified cursor." + before: String + "Returns the first _n_ elements from the list." + first: Int + "Returns the last _n_ elements from the list." + last: Int + locale: Locale + order: [CalorimetricDataSortInput!] @cost(weight: "10") + where: CalorimetricDataPropositionInput @cost(weight: "10") + ): CalorimetricDataConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allGeometricData( + "Returns the elements in the list that come after the specified cursor." + after: String + "Returns the elements in the list that come before the specified cursor." + before: String + "Returns the first _n_ elements from the list." + first: Int + "Returns the last _n_ elements from the list." + last: Int + locale: Locale + order: [GeometricDataSortInput!] @cost(weight: "10") + where: GeometricDataPropositionInput @cost(weight: "10") + ): GeometricDataConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allHygrothermalData( + "Returns the elements in the list that come after the specified cursor." + after: String + "Returns the elements in the list that come before the specified cursor." + before: String + "Returns the first _n_ elements from the list." + first: Int + "Returns the last _n_ elements from the list." + last: Int + locale: Locale + order: [HygrothermalDataSortInput!] @cost(weight: "10") + where: HygrothermalDataPropositionInput @cost(weight: "10") + ): HygrothermalDataConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allLifeCycleData( + "Returns the elements in the list that come after the specified cursor." + after: String + "Returns the elements in the list that come before the specified cursor." + before: String + "Returns the first _n_ elements from the list." + first: Int + "Returns the last _n_ elements from the list." + last: Int + locale: Locale + order: [LifeCycleDataSortInput!] @cost(weight: "10") + where: LifeCycleDataPropositionInput @cost(weight: "10") + ): LifeCycleDataConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allOpticalData( + "Returns the elements in the list that come after the specified cursor." + after: String + "Returns the elements in the list that come before the specified cursor." + before: String + "Returns the first _n_ elements from the list." + first: Int + "Returns the last _n_ elements from the list." + last: Int + locale: Locale + order: [OpticalDataSortInput!] @cost(weight: "10") + where: OpticalDataPropositionInput @cost(weight: "10") + ): OpticalDataConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allPendingCalorimetricData( + "Returns the elements in the list that come after the specified cursor." + after: String + "Returns the elements in the list that come before the specified cursor." + before: String + "Returns the first _n_ elements from the list." + first: Int + "Returns the last _n_ elements from the list." + last: Int + locale: Locale + order: [CalorimetricDataSortInput!] @cost(weight: "10") + where: CalorimetricDataPropositionInput @cost(weight: "10") + ): CalorimetricDataConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allPendingGeometricData( + "Returns the elements in the list that come after the specified cursor." + after: String + "Returns the elements in the list that come before the specified cursor." + before: String + "Returns the first _n_ elements from the list." + first: Int + "Returns the last _n_ elements from the list." + last: Int + locale: Locale + order: [GeometricDataSortInput!] @cost(weight: "10") + where: GeometricDataPropositionInput @cost(weight: "10") + ): GeometricDataConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allPendingHygrothermalData( + "Returns the elements in the list that come after the specified cursor." + after: String + "Returns the elements in the list that come before the specified cursor." + before: String + "Returns the first _n_ elements from the list." + first: Int + "Returns the last _n_ elements from the list." + last: Int + locale: Locale + order: [HygrothermalDataSortInput!] @cost(weight: "10") + where: HygrothermalDataPropositionInput @cost(weight: "10") + ): HygrothermalDataConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allPendingLifeCycleData( + "Returns the elements in the list that come after the specified cursor." + after: String + "Returns the elements in the list that come before the specified cursor." + before: String + "Returns the first _n_ elements from the list." + first: Int + "Returns the last _n_ elements from the list." + last: Int + locale: Locale + order: [LifeCycleDataSortInput!] @cost(weight: "10") + where: LifeCycleDataPropositionInput @cost(weight: "10") + ): LifeCycleDataConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allPendingOpticalData( + "Returns the elements in the list that come after the specified cursor." + after: String + "Returns the elements in the list that come before the specified cursor." + before: String + "Returns the first _n_ elements from the list." + first: Int + "Returns the last _n_ elements from the list." + last: Int + locale: Locale + order: [OpticalDataSortInput!] @cost(weight: "10") + where: OpticalDataPropositionInput @cost(weight: "10") + ): OpticalDataConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allPendingPhotovoltaicData( + "Returns the elements in the list that come after the specified cursor." + after: String + "Returns the elements in the list that come before the specified cursor." + before: String + "Returns the first _n_ elements from the list." + first: Int + "Returns the last _n_ elements from the list." + last: Int + locale: Locale + order: [PhotovoltaicDataSortInput!] @cost(weight: "10") + where: PhotovoltaicDataPropositionInput @cost(weight: "10") + ): PhotovoltaicDataConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allPhotovoltaicData( + "Returns the elements in the list that come after the specified cursor." + after: String + "Returns the elements in the list that come before the specified cursor." + before: String + "Returns the first _n_ elements from the list." + first: Int + "Returns the last _n_ elements from the list." + last: Int + locale: Locale + order: [PhotovoltaicDataSortInput!] @cost(weight: "10") + where: PhotovoltaicDataPropositionInput @cost(weight: "10") + ): PhotovoltaicDataConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + calculateMethod( + dataReference: CrossDatabaseDataReferenceInput! + methodId: Uuid! + ): CalculateMethodPayload! @cost(weight: "10") + calculateMethodWithDataUpload(data: Upload!, methodId: Uuid!): CalculateMethodPayload! + @cost(weight: "10") + calorimetricData(id: Uuid!, locale: Locale): CalorimetricData + @cost(weight: "10") + currentInstitution: CurrentInstitution @cost(weight: "10") + currentUser: User @cost(weight: "10") + currentUserInfo: UserInfo! @cost(weight: "10") + data(dataKind: DataKind!, id: Uuid!, locale: Locale): Data @cost(weight: "10") + database: Database! @cost(weight: "10") + geometricData(id: Uuid!, locale: Locale): GeometricData @cost(weight: "10") + getHttpsResource(id: Uuid!): GetHttpsResource @cost(weight: "10") + getHttpsResources( + "Returns the elements in the list that come after the specified cursor." + after: String + "Returns the elements in the list that come before the specified cursor." + before: String + "Returns the first _n_ elements from the list." + first: Int + "Returns the last _n_ elements from the list." + last: Int + order: [GetHttpsResourceSortInput!] @cost(weight: "10") + where: GetHttpsResourcePropositionInput @cost(weight: "10") + ): GetHttpsResourceConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + hasCalorimetricData( + locale: Locale + where: CalorimetricDataPropositionInput @cost(weight: "10") + ): Boolean! @cost(weight: "10") + hasGeometricData( + locale: Locale + where: GeometricDataPropositionInput @cost(weight: "10") + ): Boolean! @cost(weight: "10") + hasHygrothermalData( + locale: Locale + where: HygrothermalDataPropositionInput @cost(weight: "10") + ): Boolean! @cost(weight: "10") + hasLifeCycleData( + locale: Locale + where: LifeCycleDataPropositionInput @cost(weight: "10") + ): Boolean! @cost(weight: "10") + hasOpticalData( + locale: Locale + where: OpticalDataPropositionInput @cost(weight: "10") + ): Boolean! @cost(weight: "10") + hasPhotovoltaicData( + locale: Locale + where: PhotovoltaicDataPropositionInput @cost(weight: "10") + ): Boolean! @cost(weight: "10") + hygrothermalData(id: Uuid!, locale: Locale): HygrothermalData + @cost(weight: "10") + lifeCycleData(id: Uuid!, locale: Locale): LifeCycleData @cost(weight: "10") + "Fetches an object given its ID." + node("ID of the object." id: ID!): Node @cost(weight: "10") + "Lookup nodes by a list of IDs." + nodes("The list of node IDs." ids: [ID!]!): [Node]! @cost(weight: "10") + opticalData(id: Uuid!, locale: Locale): OpticalData @cost(weight: "10") + pendingGetHttpsResources( + "Returns the elements in the list that come after the specified cursor." + after: String + "Returns the elements in the list that come before the specified cursor." + before: String + "Returns the first _n_ elements from the list." + first: Int + "Returns the last _n_ elements from the list." + last: Int + order: [GetHttpsResourceSortInput!] @cost(weight: "10") + where: GetHttpsResourcePropositionInput @cost(weight: "10") + ): GetHttpsResourceConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + photovoltaicData(id: Uuid!, locale: Locale): PhotovoltaicData + @cost(weight: "10") + verificationCode: String! } -interface UserError { - message: String! - path: [String!]! +type Mutation { + addDataApproval(input: AddDataApprovalInput!): AddDataApprovalPayload! + @cost(weight: "10") + addInstitutionAccessRights(input: AddInstitutionAccessRightsInput!): AddInstitutionAccessRightsPayload! + @cost(weight: "10") + createCalorimetricData(input: CreateCalorimetricDataInput!): CreateCalorimetricDataPayload! + @cost(weight: "10") + createGeometricData(input: CreateGeometricDataInput!): CreateGeometricDataPayload! + @cost(weight: "10") + createGetHttpsResource(input: CreateGetHttpsResourceInput!): CreateGetHttpsResourcePayload! + @cost(weight: "10") + createHygrothermalData(input: CreateHygrothermalDataInput!): CreateHygrothermalDataPayload! + @cost(weight: "10") + createLifeCycleData(input: CreateLifeCycleDataInput!): CreateLifeCycleDataPayload! + @cost(weight: "10") + createOpticalData(input: CreateOpticalDataInput!): CreateOpticalDataPayload! + @cost(weight: "10") + createPhotovoltaicData(input: CreatePhotovoltaicDataInput!): CreatePhotovoltaicDataPayload! + @cost(weight: "10") + createResponseApprovals( + where: CreateResponseApprovalsPropositionInput @cost(weight: "10") + ): CreateResponseApprovalsPayload! @cost(weight: "10") + deleteData(input: DeleteDataInput!): DeleteDataPayload! @cost(weight: "10") + deleteGetHttpsResource(input: DeleteGetHttpsResourceInput!): DeleteGetHttpsResourcePayload! + @cost(weight: "10") + publishData(input: PublishDataInput!): PublishDataPayload! @cost(weight: "10") + recomputeGetHttpsResourceHashValues( + where: RecomputeGetHttpsResourceHashValuesPropositionInput @cost(weight: "10") + ): RecomputeGetHttpsResourceHashValuesPayload! @cost(weight: "10") + removeDataApproval(input: RemoveDataApprovalInput!): RemoveDataApprovalPayload! + @cost(weight: "10") + retractData(input: RetractDataInput!): RetractDataPayload! @cost(weight: "10") + setGetHttpsResourceParent(input: SetGetHttpsResourceParentInput!): SetGetHttpsResourceParentPayload! + @cost(weight: "10") + updateChildGetHttpsResource(input: UpdateChildGetHttpsResourceInput!): UpdateChildGetHttpsResourcePayload! + @cost(weight: "10") + updateData(input: UpdateDataInput!): UpdateDataPayload! @cost(weight: "10") + updateDataAccessRights(input: UpdateDataAccessRightsInput!): UpdateDataAccessRightsPayload! + @cost(weight: "10") + updateDatabase(input: UpdateDatabaseInput!): UpdateDatabasePayload! + @cost(weight: "10") + updateInstitutionAccessRights(input: UpdateInstitutionAccessRightsInput!): UpdateInstitutionAccessRightsPayload! + @cost(weight: "10") + updateResponseApprovals( + where: UpdateResponseApprovalsPropositionInput @cost(weight: "10") + ): UpdateResponseApprovalsPayload! @cost(weight: "10") + updateRootGetHttpsResource(input: UpdateRootGetHttpsResourceInput!): UpdateRootGetHttpsResourcePayload! + @cost(weight: "10") } type AddDataApprovalError implements UserError { @@ -81,6 +428,7 @@ type Address { type AppliedMethod { arguments: [NamedMethodArgument!]! + method: Method @cost(weight: "3") methodId: Uuid! sources: [NamedMethodSource!]! } @@ -98,26 +446,32 @@ type CalculateMethodPayload { type CalorimetricData implements Node & Data { appliedMethod: AppliedMethod! - approval: ResponseApproval! + approval: ResponseApproval! @cost(weight: "3") approvals: [DataApproval!]! + component: Component @cost(weight: "3") componentId: Uuid! createdAt: DateTime! + creator: Institution @cost(weight: "3") creatorId: Uuid! dataAccessRights: DataAccessRights! + database: Database @cost(weight: "3") databaseId: Uuid! description: String gValues: [Float!]! id: ID! isRestrictedByApplication(applicationId: String!): Boolean! isRestrictedByInstitutions(institutions: [Uuid!]!): Boolean! - isRestrictedByUser(alreadyAccesedCount: NonNegativeInt! uuid: Uuid!): Boolean! + isRestrictedByUser(alreadyAccesedCount: NonNegativeInt!, uuid: Uuid!): Boolean! locale: Locale! name: String - resources(order: [GetHttpsResourceSortInput!] @cost(weight: "10") where: GetHttpsResourcePropositionInput @cost(weight: "10")): [GetHttpsResource!]! @cost(weight: "10") + resources( + order: [GetHttpsResourceSortInput!] + where: GetHttpsResourcePropositionInput + ): [GetHttpsResource!]! resourceTree: GetHttpsResourceTree! timestamp: DateTime! userId: Uuid - uuid: Uuid! @cost(weight: "10") + uuid: Uuid! uValues: [Float!]! warnings: [String!]! } @@ -148,6 +502,11 @@ type CielabColor { observer: CalorimetricObserver } +type Component { + name: String! + uuid: Uuid! +} + type CreateCalorimetricDataError implements UserError { code: CreateCalorimetricDataErrorCode! message: String! @@ -245,16 +604,25 @@ type CreateResponseApprovalsPayload { } type CrossDatabaseDataReference { + database: Database @cost(weight: "3") databaseId: Uuid! dataId: Uuid! dataKind: DataKind! dataTimestamp: DateTime! } +type CurrentInstitution { + databaseOperatingDatabases: DatabaseOperatingDatabaseConnection! + databaseOperatingManagedInstitutions: DatabaseOperatingManagedInstitutionConnection! + isDatabaseOperator: Boolean! + name: String! + uuid: Uuid! +} + type DataAccessRights { allowedApplications: [String!] allowedInstitutions: [Uuid!] - allowedUserAndQuantity: [KeyValuePairOfGuidAndNullableOfUInt32!] + allowedUserAndQuantity: [KeyValuePairOfGuidAndNullableUInt32!] hasRestrictions: Boolean! hasRestrictionsByApplication: Boolean! hasRestrictionsByInstitution: Boolean! @@ -262,27 +630,17 @@ type DataAccessRights { } type DataApproval implements Approval { + approver: Institution @cost(weight: "3") approverId: Uuid! keyFingerprint: String! message: String! query: String! signature: String! - statement: Reference @cost(weight: "10") + statement: Reference! @cost(weight: "10") timestamp: DateTime! variables: Any! } -type DataConnection { - edges: [DataEdge!]! - pageInfo: PageInfo! - totalCount: NonNegativeInt -} - -type DataEdge { - cursor: String! - node: Data! -} - type Database { description: String! isAuthorizedToUpdateNode: Boolean! @@ -295,10 +653,37 @@ type Database { verificationState: DatabaseVerificationState! } +type DatabaseOperatingDatabaseConnection { + totalCount: NonNegativeInt! +} + +type DatabaseOperatingManagedInstitutionConnection { + totalCount: NonNegativeInt! +} + type DatabaseOperatorEdge { node: Institution! } +type DataConnection { + edges: [DataEdge!]! + pageInfo: PageInfo! + totalCount: NonNegativeInt +} + +type DataEdge { + cursor: String! + node: Data! +} + +type DataFormat { + extension: String + mediaType: String! + name: String! + schemaLocator: Url + uuid: Uuid! +} + type DeleteDataError implements UserError { code: DeleteDataErrorCode! message: String! @@ -323,33 +708,40 @@ type DeleteGetHttpsResourcePayload { } type FileMetaInformation { + dataFormat: DataFormat @cost(weight: "3") dataFormatId: Uuid! path: [String!]! } type GeometricData implements Node & Data { appliedMethod: AppliedMethod! - approval: ResponseApproval! + approval: ResponseApproval! @cost(weight: "3") approvals: [DataApproval!]! + component: Component @cost(weight: "3") componentId: Uuid! createdAt: DateTime! + creator: Institution @cost(weight: "3") creatorId: Uuid! dataAccessRights: DataAccessRights! + database: Database @cost(weight: "3") databaseId: Uuid! description: String heights: [Float!]! id: ID! isRestrictedByApplication(applicationId: String!): Boolean! isRestrictedByInstitutions(institutions: [Uuid!]!): Boolean! - isRestrictedByUser(alreadyAccesedCount: NonNegativeInt! uuid: Uuid!): Boolean! + isRestrictedByUser(alreadyAccesedCount: NonNegativeInt!, uuid: Uuid!): Boolean! locale: Locale! name: String - resources(order: [GetHttpsResourceSortInput!] @cost(weight: "10") where: GetHttpsResourcePropositionInput @cost(weight: "10")): [GetHttpsResource!]! @cost(weight: "10") + resources( + order: [GetHttpsResourceSortInput!] + where: GetHttpsResourcePropositionInput + ): [GetHttpsResource!]! resourceTree: GetHttpsResourceTree! thicknesses: [Float!]! timestamp: DateTime! userId: Uuid - uuid: Uuid! @cost(weight: "10") + uuid: Uuid! warnings: [String!]! widths: [Float!]! } @@ -375,8 +767,10 @@ type GeometricDataEdge { type GetHttpsResource implements Node { appliedConversionMethod: ToTreeVertexAppliedConversionMethod archivedFilesMetaInformation: [FileMetaInformation!]! - children: [GetHttpsResource!]! @cost(weight: "10") - data: Data @cost(weight: "10") + children: [GetHttpsResource!]! @cost(weight: "0") + createdAt: DateTime! + data: Data @cost(weight: "0") + dataFormat: DataFormat @cost(weight: "3") dataFormatId: Uuid! description: String doesFileExist: Boolean! @@ -385,8 +779,9 @@ type GetHttpsResource implements Node { isChild: Boolean! isRoot: Boolean! locator: Url! - parent: GetHttpsResource @cost(weight: "10") - uuid: Uuid! @cost(weight: "10") + parent: GetHttpsResource @cost(weight: "0") + updatedAt: DateTime! + uuid: Uuid! } "A connection to a list of items." @@ -408,7 +803,10 @@ type GetHttpsResourceEdge { } type GetHttpsResourceTree { - nonRootVertices(order: [GetHttpsResourceTreeNonRootVertSortInput!] @cost(weight: "10") where: GetHttpsResourceTreeNonRootVertexPropositionInput @cost(weight: "10")): [GetHttpsResourceTreeNonRootVertex!]! @cost(weight: "10") + nonRootVertices( + order: [GetHttpsResourceTreeNonRootVertSortInput!] @cost(weight: "10") + where: GetHttpsResourceTreeNonRootVertexPropositionInput @cost(weight: "10") + ): [GetHttpsResourceTreeNonRootVertex!]! @cost(weight: "10") root: GetHttpsResourceTreeRoot! @cost(weight: "10") } @@ -426,25 +824,31 @@ type GetHttpsResourceTreeRoot implements GetHttpsResourceTreeVertex { type HygrothermalData implements Node & Data { appliedMethod: AppliedMethod! - approval: ResponseApproval! + approval: ResponseApproval! @cost(weight: "3") approvals: [DataApproval!]! + component: Component @cost(weight: "3") componentId: Uuid! createdAt: DateTime! + creator: Institution @cost(weight: "3") creatorId: Uuid! dataAccessRights: DataAccessRights! + database: Database @cost(weight: "3") databaseId: Uuid! description: String id: ID! isRestrictedByApplication(applicationId: String!): Boolean! isRestrictedByInstitutions(institutions: [Uuid!]!): Boolean! - isRestrictedByUser(alreadyAccesedCount: NonNegativeInt! uuid: Uuid!): Boolean! + isRestrictedByUser(alreadyAccesedCount: NonNegativeInt!, uuid: Uuid!): Boolean! locale: Locale! name: String - resources(order: [GetHttpsResourceSortInput!] @cost(weight: "10") where: GetHttpsResourcePropositionInput @cost(weight: "10")): [GetHttpsResource!]! @cost(weight: "10") + resources( + order: [GetHttpsResourceSortInput!] + where: GetHttpsResourcePropositionInput + ): [GetHttpsResource!]! resourceTree: GetHttpsResourceTree! timestamp: DateTime! userId: Uuid - uuid: Uuid! @cost(weight: "10") + uuid: Uuid! warnings: [String!]! } @@ -467,48 +871,57 @@ type HygrothermalDataEdge { } type Institution { + name: String! uuid: Uuid! } type InstitutionAccessRights { allowedDatasetsPerTime: NonNegativeInt allowedUserCount: NonNegativeInt + createdAt: DateTime! hasRestrictions: Boolean! hasRestrictionsByTime: Boolean! hasRestrictionsByUser: Boolean! id: Uuid! institutionId: Uuid! period: Duration! + updatedAt: DateTime! userAlreadyAccessed: [Uuid!]! version: NonNegativeInt! } -type KeyValuePairOfGuidAndNullableOfUInt32 { +type KeyValuePairOfGuidAndNullableUInt32 { key: Uuid! value: NonNegativeInt } type LifeCycleData implements Node & Data { appliedMethod: AppliedMethod! - approval: ResponseApproval! + approval: ResponseApproval! @cost(weight: "3") approvals: [DataApproval!]! + component: Component @cost(weight: "3") componentId: Uuid! createdAt: DateTime! + creator: Institution @cost(weight: "3") creatorId: Uuid! dataAccessRights: DataAccessRights! + database: Database @cost(weight: "3") databaseId: Uuid! description: String id: ID! isRestrictedByApplication(applicationId: String!): Boolean! isRestrictedByInstitutions(institutions: [Uuid!]!): Boolean! - isRestrictedByUser(alreadyAccesedCount: NonNegativeInt! uuid: Uuid!): Boolean! + isRestrictedByUser(alreadyAccesedCount: NonNegativeInt!, uuid: Uuid!): Boolean! locale: Locale! name: String - resources(order: [GetHttpsResourceSortInput!] @cost(weight: "10") where: GetHttpsResourcePropositionInput @cost(weight: "10")): [GetHttpsResource!]! @cost(weight: "10") + resources( + order: [GetHttpsResourceSortInput!] + where: GetHttpsResourcePropositionInput + ): [GetHttpsResource!]! resourceTree: GetHttpsResourceTree! timestamp: DateTime! userId: Uuid - uuid: Uuid! @cost(weight: "10") + uuid: Uuid! warnings: [String!]! } @@ -530,31 +943,9 @@ type LifeCycleDataEdge { node: LifeCycleData! } -type Mutation { - addDataApproval(input: AddDataApprovalInput!): AddDataApprovalPayload! @cost(weight: "10") - addInstitutionAccessRights(input: AddInstitutionAccessRightsInput!): AddInstitutionAccessRightsPayload! @cost(weight: "10") - createCalorimetricData(input: CreateCalorimetricDataInput!): CreateCalorimetricDataPayload! @cost(weight: "10") - createGeometricData(input: CreateGeometricDataInput!): CreateGeometricDataPayload! @cost(weight: "10") - createGetHttpsResource(input: CreateGetHttpsResourceInput!): CreateGetHttpsResourcePayload! @cost(weight: "10") - createHygrothermalData(input: CreateHygrothermalDataInput!): CreateHygrothermalDataPayload! @cost(weight: "10") - createLifeCycleData(input: CreateLifeCycleDataInput!): CreateLifeCycleDataPayload! @cost(weight: "10") - createOpticalData(input: CreateOpticalDataInput!): CreateOpticalDataPayload! @cost(weight: "10") - createPhotovoltaicData(input: CreatePhotovoltaicDataInput!): CreatePhotovoltaicDataPayload! @cost(weight: "10") - createResponseApprovals(where: CreateResponseApprovalsPropositionInput @cost(weight: "10")): CreateResponseApprovalsPayload! @cost(weight: "10") - deleteData(input: DeleteDataInput!): DeleteDataPayload! @cost(weight: "10") - deleteGetHttpsResource(input: DeleteGetHttpsResourceInput!): DeleteGetHttpsResourcePayload! @cost(weight: "10") - publishData(input: PublishDataInput!): PublishDataPayload! @cost(weight: "10") - recomputeGetHttpsResourceHashValues(where: RecomputeGetHttpsResourceHashValuesPropositionInput @cost(weight: "10")): RecomputeGetHttpsResourceHashValuesPayload! @cost(weight: "10") - removeDataApproval(input: RemoveDataApprovalInput!): RemoveDataApprovalPayload! @cost(weight: "10") - retractData(input: RetractDataInput!): RetractDataPayload! @cost(weight: "10") - setGetHttpsResourceParent(input: SetGetHttpsResourceParentInput!): SetGetHttpsResourceParentPayload! @cost(weight: "10") - updateChildGetHttpsResource(input: UpdateChildGetHttpsResourceInput!): UpdateChildGetHttpsResourcePayload! @cost(weight: "10") - updateData(input: UpdateDataInput!): UpdateDataPayload! @cost(weight: "10") - updateDataAccessRights(input: UpdateDataAccessRightsInput!): UpdateDataAccessRightsPayload! @cost(weight: "10") - updateDatabase(input: UpdateDatabaseInput!): UpdateDatabasePayload! @cost(weight: "10") - updateInstitutionAccessRights(input: UpdateInstitutionAccessRightsInput!): UpdateInstitutionAccessRightsPayload! @cost(weight: "10") - updateResponseApprovals(where: UpdateResponseApprovalsPropositionInput @cost(weight: "10")): UpdateResponseApprovalsPayload! @cost(weight: "10") - updateRootGetHttpsResource(input: UpdateRootGetHttpsResourceInput!): UpdateRootGetHttpsResourcePayload! @cost(weight: "10") +type Method { + name: String! + uuid: Uuid! } type NamedMethodArgument { @@ -580,35 +971,41 @@ type OpenEndedDateTimeRange { type OpticalData implements Node & Data { appliedMethod: AppliedMethod! - approval: ResponseApproval! + approval: ResponseApproval! @cost(weight: "3") approvals: [DataApproval!]! cielabColors: [CielabColor!]! coatedSide: CoatedSide colorRenderingIndices: [Float!]! + component: Component @cost(weight: "3") componentId: Uuid! createdAt: DateTime! + creator: Institution @cost(weight: "3") creatorId: Uuid! dataAccessRights: DataAccessRights! + database: Database @cost(weight: "3") databaseId: Uuid! description: String id: ID! infraredEmittances: [Float!]! isRestrictedByApplication(applicationId: String!): Boolean! isRestrictedByInstitutions(institutions: [Uuid!]!): Boolean! - isRestrictedByUser(alreadyAccesedCount: NonNegativeInt! uuid: Uuid!): Boolean! + isRestrictedByUser(alreadyAccesedCount: NonNegativeInt!, uuid: Uuid!): Boolean! locale: Locale! name: String nearnormalHemisphericalSolarReflectances: [Float!]! nearnormalHemisphericalSolarTransmittances: [Float!]! nearnormalHemisphericalVisibleReflectances: [Float!]! nearnormalHemisphericalVisibleTransmittances: [Float!]! - resources(order: [GetHttpsResourceSortInput!] @cost(weight: "10") where: GetHttpsResourcePropositionInput @cost(weight: "10")): [GetHttpsResource!]! @cost(weight: "10") + resources( + order: [GetHttpsResourceSortInput!] + where: GetHttpsResourcePropositionInput + ): [GetHttpsResource!]! resourceTree: GetHttpsResourceTree! subtype: OpticalComponentSubtype timestamp: DateTime! type: OpticalComponentType userId: Uuid - uuid: Uuid! @cost(weight: "10") + uuid: Uuid! warnings: [String!]! } @@ -644,25 +1041,31 @@ type PageInfo { type PhotovoltaicData implements Node & Data { appliedMethod: AppliedMethod! - approval: ResponseApproval! + approval: ResponseApproval! @cost(weight: "3") approvals: [DataApproval!]! + component: Component @cost(weight: "3") componentId: Uuid! createdAt: DateTime! + creator: Institution @cost(weight: "3") creatorId: Uuid! dataAccessRights: DataAccessRights! + database: Database @cost(weight: "3") databaseId: Uuid! description: String id: ID! isRestrictedByApplication(applicationId: String!): Boolean! isRestrictedByInstitutions(institutions: [Uuid!]!): Boolean! - isRestrictedByUser(alreadyAccesedCount: NonNegativeInt! uuid: Uuid!): Boolean! + isRestrictedByUser(alreadyAccesedCount: NonNegativeInt!, uuid: Uuid!): Boolean! locale: Locale! name: String - resources(order: [GetHttpsResourceSortInput!] @cost(weight: "10") where: GetHttpsResourcePropositionInput @cost(weight: "10")): [GetHttpsResource!]! @cost(weight: "10") + resources( + order: [GetHttpsResourceSortInput!] + where: GetHttpsResourcePropositionInput + ): [GetHttpsResource!]! resourceTree: GetHttpsResourceTree! timestamp: DateTime! userId: Uuid - uuid: Uuid! @cost(weight: "10") + uuid: Uuid! warnings: [String!]! } @@ -712,47 +1115,6 @@ type PublishDataPayload { query: Query! } -type Query { - allCalorimetricData("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int locale: Locale order: [CalorimetricDataSortInput!] @cost(weight: "10") where: CalorimetricDataPropositionInput @cost(weight: "10")): CalorimetricDataConnection @listSize(assumedSize: 100, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - allGeometricData("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int locale: Locale order: [GeometricDataSortInput!] @cost(weight: "10") where: GeometricDataPropositionInput @cost(weight: "10")): GeometricDataConnection @listSize(assumedSize: 100, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - allHygrothermalData("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int locale: Locale order: [HygrothermalDataSortInput!] @cost(weight: "10") where: HygrothermalDataPropositionInput @cost(weight: "10")): HygrothermalDataConnection @listSize(assumedSize: 100, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - allLifeCycleData("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int locale: Locale order: [LifeCycleDataSortInput!] @cost(weight: "10") where: LifeCycleDataPropositionInput @cost(weight: "10")): LifeCycleDataConnection @listSize(assumedSize: 100, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - allOpticalData("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int locale: Locale order: [OpticalDataSortInput!] @cost(weight: "10") where: OpticalDataPropositionInput @cost(weight: "10")): OpticalDataConnection @listSize(assumedSize: 100, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - allPendingCalorimetricData("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int locale: Locale order: [CalorimetricDataSortInput!] @cost(weight: "10") where: CalorimetricDataPropositionInput @cost(weight: "10")): CalorimetricDataConnection @listSize(assumedSize: 100, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - allPendingGeometricData("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int locale: Locale order: [GeometricDataSortInput!] @cost(weight: "10") where: GeometricDataPropositionInput @cost(weight: "10")): GeometricDataConnection @listSize(assumedSize: 100, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - allPendingHygrothermalData("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int locale: Locale order: [HygrothermalDataSortInput!] @cost(weight: "10") where: HygrothermalDataPropositionInput @cost(weight: "10")): HygrothermalDataConnection @listSize(assumedSize: 100, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - allPendingLifeCycleData("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int locale: Locale order: [LifeCycleDataSortInput!] @cost(weight: "10") where: LifeCycleDataPropositionInput @cost(weight: "10")): LifeCycleDataConnection @listSize(assumedSize: 100, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - allPendingOpticalData("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int locale: Locale order: [OpticalDataSortInput!] @cost(weight: "10") where: OpticalDataPropositionInput @cost(weight: "10")): OpticalDataConnection @listSize(assumedSize: 100, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - allPendingPhotovoltaicData("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int locale: Locale order: [PhotovoltaicDataSortInput!] @cost(weight: "10") where: PhotovoltaicDataPropositionInput @cost(weight: "10")): PhotovoltaicDataConnection @listSize(assumedSize: 100, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - allPhotovoltaicData("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int locale: Locale order: [PhotovoltaicDataSortInput!] @cost(weight: "10") where: PhotovoltaicDataPropositionInput @cost(weight: "10")): PhotovoltaicDataConnection @listSize(assumedSize: 100, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - calculateMethod(dataReference: CrossDatabaseDataReferenceInput! methodId: Uuid!): CalculateMethodPayload! @cost(weight: "10") - calculateMethodWithDataUpload(data: Upload! methodId: Uuid!): CalculateMethodPayload! @cost(weight: "10") - calorimetricData(id: Uuid! locale: Locale): CalorimetricData @cost(weight: "10") - currentUser: User @cost(weight: "10") - currentUserInfo: UserInfo! @cost(weight: "10") - data(dataKind: DataKind! id: Uuid! locale: Locale): Data @cost(weight: "10") - database: Database! @cost(weight: "10") - geometricData(id: Uuid! locale: Locale): GeometricData @cost(weight: "10") - getHttpsResource(id: Uuid!): GetHttpsResource @cost(weight: "10") - getHttpsResources("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int order: [GetHttpsResourceSortInput!] @cost(weight: "10") where: GetHttpsResourcePropositionInput @cost(weight: "10")): GetHttpsResourceConnection @listSize(assumedSize: 100, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - hasCalorimetricData(locale: Locale where: CalorimetricDataPropositionInput @cost(weight: "10")): Boolean! @cost(weight: "10") - hasGeometricData(locale: Locale where: GeometricDataPropositionInput @cost(weight: "10")): Boolean! @cost(weight: "10") - hasHygrothermalData(locale: Locale where: HygrothermalDataPropositionInput @cost(weight: "10")): Boolean! @cost(weight: "10") - hasLifeCycleData(locale: Locale where: LifeCycleDataPropositionInput @cost(weight: "10")): Boolean! @cost(weight: "10") - hasOpticalData(locale: Locale where: OpticalDataPropositionInput @cost(weight: "10")): Boolean! @cost(weight: "10") - hasPhotovoltaicData(locale: Locale where: PhotovoltaicDataPropositionInput @cost(weight: "10")): Boolean! @cost(weight: "10") - hygrothermalData(id: Uuid! locale: Locale): HygrothermalData @cost(weight: "10") - lifeCycleData(id: Uuid! locale: Locale): LifeCycleData @cost(weight: "10") - "Fetches an object given its ID." - node("ID of the object." id: ID!): Node @cost(weight: "10") - "Lookup nodes by a list of IDs." - nodes("The list of node IDs." ids: [ID!]!): [Node]! @cost(weight: "10") - opticalData(id: Uuid! locale: Locale): OpticalData @cost(weight: "10") - pendingGetHttpsResources("Returns the elements in the list that come after the specified cursor." after: String "Returns the elements in the list that come before the specified cursor." before: String "Returns the first _n_ elements from the list." first: Int "Returns the last _n_ elements from the list." last: Int order: [GetHttpsResourceSortInput!] @cost(weight: "10") where: GetHttpsResourcePropositionInput @cost(weight: "10")): GetHttpsResourceConnection @listSize(assumedSize: 100, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - photovoltaicData(id: Uuid! locale: Locale): PhotovoltaicData @cost(weight: "10") - verificationCode: String! -} - type RecomputeGetHttpsResourceHashValuesError implements UserError { code: RecomputeGetHttpsResourceHashValuesErrorCode! message: String! @@ -826,6 +1188,7 @@ type Standard { type ToTreeVertexAppliedConversionMethod { arguments: [NamedMethodArgument!]! + method: Method @cost(weight: "3") methodId: Uuid! sourceName: String! } @@ -854,27 +1217,27 @@ type UpdateDataAccessRightsPayload { query: Query! } -type UpdateDataError implements UserError { - code: UpdateDataErrorCode! +type UpdateDatabaseError { + code: UpdateDatabaseErrorCode! message: String! path: [String!]! } -type UpdateDataPayload { - data: Data - errors: [UpdateDataError!] +type UpdateDatabasePayload { + database: Database + errors: [UpdateDatabaseError!] query: Query! } -type UpdateDatabaseError { - code: UpdateDatabaseErrorCode! +type UpdateDataError implements UserError { + code: UpdateDataErrorCode! message: String! path: [String!]! } -type UpdateDatabasePayload { - database: Database - errors: [UpdateDatabaseError!] +type UpdateDataPayload { + data: Data + errors: [UpdateDataError!] query: Query! } @@ -915,10 +1278,12 @@ type UpdateRootGetHttpsResourcePayload { } type User implements Node { + createdAt: DateTime! id: ID! name: String! subject: String! - uuid: Uuid! @cost(weight: "10") + updatedAt: DateTime! + uuid: Uuid! } type UserInfo { @@ -933,6 +1298,58 @@ type UserInfo { website: String } +interface Approval { + approverId: Uuid! + keyFingerprint: String! + message: String! + query: String! + signature: String! + timestamp: DateTime! + variables: Any! +} + +interface Data { + appliedMethod: AppliedMethod! + approval: ResponseApproval! + approvals: [DataApproval!]! + component: Component + componentId: Uuid! + createdAt: DateTime! + creator: Institution + creatorId: Uuid! + dataAccessRights: DataAccessRights! + database: Database + databaseId: Uuid! + description: String + id: ID! + isRestrictedByApplication(applicationId: String!): Boolean! + isRestrictedByInstitutions(institutions: [Uuid!]!): Boolean! + isRestrictedByUser(alreadyAccesedCount: NonNegativeInt!, uuid: Uuid!): Boolean! + locale: Locale! + name: String + resources: [GetHttpsResource!]! + resourceTree: GetHttpsResourceTree! + timestamp: DateTime! + userId: Uuid + uuid: Uuid! + warnings: [String!]! +} + +interface GetHttpsResourceTreeVertex { + value: GetHttpsResource! + vertexId: ID +} + +"The node interface is implemented by entities that have a global unique identifier." +interface Node { + id: ID! +} + +interface UserError { + message: String! + path: [String!]! +} + union Reference = Publication | Standard input AddDataApprovalInput { @@ -963,9 +1380,9 @@ input AppliedMethodInput { input AppliedMethodPropositionInput { and: [AppliedMethodPropositionInput!] - or: [AppliedMethodPropositionInput!] - methodId: UuidPropositionInput arguments: FilterInputTypeOfNamedMethodArgumentsPropositionInput + methodId: UuidPropositionInput + or: [AppliedMethodPropositionInput!] sources: FilterInputTypeOfNamedMethodSourcesPropositionInput } @@ -980,57 +1397,49 @@ input BooleanPropositionInput { input BytePropositionInput { equalTo: Byte @cost(weight: "10") - notEqualTo: Byte @cost(weight: "10") - in: [Byte] @cost(weight: "10") - notIn: [Byte] @cost(weight: "10") greaterThan: Byte @cost(weight: "10") - notGreaterThan: Byte @cost(weight: "10") greaterThanOrEqualTo: Byte @cost(weight: "10") - notGreaterThanOrEqualTo: Byte @cost(weight: "10") + in: [Byte] @cost(weight: "10") lessThan: Byte @cost(weight: "10") - notLessThan: Byte @cost(weight: "10") lessThanOrEqualTo: Byte @cost(weight: "10") + notEqualTo: Byte @cost(weight: "10") + notGreaterThan: Byte @cost(weight: "10") + notGreaterThanOrEqualTo: Byte @cost(weight: "10") + notIn: [Byte] @cost(weight: "10") + notLessThan: Byte @cost(weight: "10") notLessThanOrEqualTo: Byte @cost(weight: "10") } -input CalendarSystemPropositionInput { - and: [CalendarSystemPropositionInput!] - or: [CalendarSystemPropositionInput!] - id: StringPropositionInput - name: StringPropositionInput - minYear: IntPropositionInput - maxYear: IntPropositionInput - eras: FilterInputTypeOfErasPropositionInput -} - input CalorimetricDataPropositionInput { and: [CalorimetricDataPropositionInput!] - or: [CalorimetricDataPropositionInput!] + appliedMethod: AppliedMethodPropositionInput + approvals: FilterInputTypeOfDataApprovalsPropositionInput + componentId: UuidPropositionInput + createdAt: DateTimePropositionInput + creatorId: UuidPropositionInput + description: StringPropositionInput + gValues: FloatsPropositionInput id: UuidPropositionInput - userId: UuidPropositionInput locale: StringPropositionInput name: StringPropositionInput - description: StringPropositionInput - componentId: UuidPropositionInput - creatorId: UuidPropositionInput - createdAt: OffsetDateTimePropositionInput - appliedMethod: AppliedMethodPropositionInput - approvals: FilterInputTypeOfDataApprovalsPropositionInput + or: [CalorimetricDataPropositionInput!] resources: FilterInputTypeOfGetHttpsResourcesPropositionInput - warnings: StringsPropositionInput - gValues: FloatsPropositionInput + updatedAt: DateTimePropositionInput + userId: UuidPropositionInput uValues: FloatsPropositionInput + warnings: StringsPropositionInput } input CalorimetricDataSortInput { + appliedMethod: AppliedMethodSortInput @cost(weight: "10") + componentId: SortEnumType @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") + creatorId: SortEnumType @cost(weight: "10") + description: SortEnumType @cost(weight: "10") id: SortEnumType @cost(weight: "10") locale: SortEnumType @cost(weight: "10") name: SortEnumType @cost(weight: "10") - description: SortEnumType @cost(weight: "10") - componentId: SortEnumType @cost(weight: "10") - creatorId: SortEnumType @cost(weight: "10") - createdAt: SortEnumType @cost(weight: "10") - appliedMethod: AppliedMethodSortInput @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input CielabColorInput { @@ -1043,12 +1452,12 @@ input CielabColorInput { input CielabColorPropositionInput { and: [CielabColorPropositionInput!] - or: [CielabColorPropositionInput!] - lStar: FloatPropositionInput aStar: FloatPropositionInput bStar: FloatPropositionInput - observer: NullableOfCalorimetricObserverPropositionInput illuminant: NullableOfIlluminantPropositionInput + lStar: FloatPropositionInput + observer: NullableOfCalorimetricObserverPropositionInput + or: [CielabColorPropositionInput!] } input ClosedIntervalInput { @@ -1155,18 +1564,16 @@ input CreatePhotovoltaicDataInput { input CreateResponseApprovalsPropositionInput { and: [CreateResponseApprovalsPropositionInput!] - or: [CreateResponseApprovalsPropositionInput!] - id: UuidPropositionInput - userId: UuidPropositionInput - locale: StringPropositionInput - name: StringPropositionInput - description: StringPropositionInput - componentId: UuidPropositionInput - creatorId: UuidPropositionInput - createdAt: OffsetDateTimePropositionInput appliedMethod: AppliedMethodPropositionInput approvals: FilterInputTypeOfDataApprovalsPropositionInput + componentId: UuidPropositionInput + creatorId: UuidPropositionInput + description: StringPropositionInput + locale: StringPropositionInput + name: StringPropositionInput + or: [CreateResponseApprovalsPropositionInput!] resources: FilterInputTypeOfGetHttpsResourcesPropositionInput + userId: UuidPropositionInput warnings: StringsPropositionInput } @@ -1179,88 +1586,90 @@ input CrossDatabaseDataReferenceInput { input CrossDatabaseDataReferencePropositionInput { and: [CrossDatabaseDataReferencePropositionInput!] - or: [CrossDatabaseDataReferencePropositionInput!] + databaseId: UuidPropositionInput dataId: UuidPropositionInput - dataTimestamp: OffsetDateTimePropositionInput dataKind: DataKindPropositionInput - databaseId: UuidPropositionInput + dataTimestamp: DateTimePropositionInput + or: [CrossDatabaseDataReferencePropositionInput!] } input DataApprovalPropositionInput { and: [DataApprovalPropositionInput!] - or: [DataApprovalPropositionInput!] approverId: UuidPropositionInput - timestamp: OffsetDateTimePropositionInput - signature: StringPropositionInput keyFingerprint: StringPropositionInput - query: StringPropositionInput - variables: JsonElementPropositionInput message: StringPropositionInput + or: [DataApprovalPropositionInput!] + query: StringPropositionInput + signature: StringPropositionInput statement: ReferencePropositionInput + timestamp: DateTimePropositionInput + variables: JsonElementPropositionInput } input DataKindPropositionInput { equalTo: DataKind @cost(weight: "10") - notEqualTo: DataKind @cost(weight: "10") in: [DataKind!] @cost(weight: "10") + notEqualTo: DataKind @cost(weight: "10") notIn: [DataKind!] @cost(weight: "10") } input DataPropositionInput { and: [DataPropositionInput!] - or: [DataPropositionInput!] + appliedMethod: AppliedMethodPropositionInput + approvals: FilterInputTypeOfDataApprovalsPropositionInput + componentId: UuidPropositionInput + createdAt: DateTimePropositionInput + creatorId: UuidPropositionInput + description: StringPropositionInput id: UuidPropositionInput - userId: UuidPropositionInput locale: StringPropositionInput name: StringPropositionInput - description: StringPropositionInput - componentId: UuidPropositionInput - creatorId: UuidPropositionInput - createdAt: OffsetDateTimePropositionInput - appliedMethod: AppliedMethodPropositionInput - approvals: FilterInputTypeOfDataApprovalsPropositionInput + or: [DataPropositionInput!] resources: FilterInputTypeOfGetHttpsResourcesPropositionInput + updatedAt: DateTimePropositionInput + userId: UuidPropositionInput warnings: StringsPropositionInput } input DataSortInput { + appliedMethod: AppliedMethodSortInput @cost(weight: "10") + componentId: SortEnumType @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") + creatorId: SortEnumType @cost(weight: "10") + description: SortEnumType @cost(weight: "10") id: SortEnumType @cost(weight: "10") locale: SortEnumType @cost(weight: "10") name: SortEnumType @cost(weight: "10") - description: SortEnumType @cost(weight: "10") - componentId: SortEnumType @cost(weight: "10") - creatorId: SortEnumType @cost(weight: "10") - createdAt: SortEnumType @cost(weight: "10") - appliedMethod: AppliedMethodSortInput @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input DateTimePropositionInput { equalTo: DateTime @cost(weight: "10") - notEqualTo: DateTime @cost(weight: "10") - in: [DateTime] @cost(weight: "10") - notIn: [DateTime] @cost(weight: "10") greaterThan: DateTime @cost(weight: "10") - notGreaterThan: DateTime @cost(weight: "10") greaterThanOrEqualTo: DateTime @cost(weight: "10") - notGreaterThanOrEqualTo: DateTime @cost(weight: "10") + in: [DateTime] @cost(weight: "10") lessThan: DateTime @cost(weight: "10") - notLessThan: DateTime @cost(weight: "10") lessThanOrEqualTo: DateTime @cost(weight: "10") + notEqualTo: DateTime @cost(weight: "10") + notGreaterThan: DateTime @cost(weight: "10") + notGreaterThanOrEqualTo: DateTime @cost(weight: "10") + notIn: [DateTime] @cost(weight: "10") + notLessThan: DateTime @cost(weight: "10") notLessThanOrEqualTo: DateTime @cost(weight: "10") } input DecimalPropositionInput { equalTo: Decimal @cost(weight: "10") - notEqualTo: Decimal @cost(weight: "10") - in: [Decimal] @cost(weight: "10") - notIn: [Decimal] @cost(weight: "10") greaterThan: Decimal @cost(weight: "10") - notGreaterThan: Decimal @cost(weight: "10") greaterThanOrEqualTo: Decimal @cost(weight: "10") - notGreaterThanOrEqualTo: Decimal @cost(weight: "10") + in: [Decimal] @cost(weight: "10") lessThan: Decimal @cost(weight: "10") - notLessThan: Decimal @cost(weight: "10") lessThanOrEqualTo: Decimal @cost(weight: "10") + notEqualTo: Decimal @cost(weight: "10") + notGreaterThan: Decimal @cost(weight: "10") + notGreaterThanOrEqualTo: Decimal @cost(weight: "10") + notIn: [Decimal] @cost(weight: "10") + notLessThan: Decimal @cost(weight: "10") notLessThanOrEqualTo: Decimal @cost(weight: "10") } @@ -1273,10 +1682,19 @@ input DeleteGetHttpsResourceInput { getHttpsResourceId: Uuid! } -input EraPropositionInput { - and: [EraPropositionInput!] - or: [EraPropositionInput!] - name: StringPropositionInput +input DurationPropositionInput { + equalTo: Duration @cost(weight: "10") + greaterThan: Duration @cost(weight: "10") + greaterThanOrEqualTo: Duration @cost(weight: "10") + in: [Duration] @cost(weight: "10") + lessThan: Duration @cost(weight: "10") + lessThanOrEqualTo: Duration @cost(weight: "10") + notEqualTo: Duration @cost(weight: "10") + notGreaterThan: Duration @cost(weight: "10") + notGreaterThanOrEqualTo: Duration @cost(weight: "10") + notIn: [Duration] @cost(weight: "10") + notLessThan: Duration @cost(weight: "10") + notLessThanOrEqualTo: Duration @cost(weight: "10") } input FileMetaInformationInput { @@ -1286,210 +1704,202 @@ input FileMetaInformationInput { input FileMetaInformationPropositionInput { and: [FileMetaInformationPropositionInput!] + dataFormatId: UuidPropositionInput or: [FileMetaInformationPropositionInput!] path: StringsPropositionInput - dataFormatId: UuidPropositionInput } input FilterInputTypeOfCielabColorsPropositionInput { all: CielabColorPropositionInput @cost(weight: "10") + any: Boolean @cost(weight: "10") none: CielabColorPropositionInput @cost(weight: "10") some: CielabColorPropositionInput @cost(weight: "10") - any: Boolean @cost(weight: "10") } input FilterInputTypeOfDataApprovalsPropositionInput { all: DataApprovalPropositionInput @cost(weight: "10") + any: Boolean @cost(weight: "10") none: DataApprovalPropositionInput @cost(weight: "10") some: DataApprovalPropositionInput @cost(weight: "10") - any: Boolean @cost(weight: "10") -} - -input FilterInputTypeOfErasPropositionInput { - all: EraPropositionInput @cost(weight: "10") - none: EraPropositionInput @cost(weight: "10") - some: EraPropositionInput @cost(weight: "10") - any: Boolean @cost(weight: "10") } input FilterInputTypeOfFileMetaInformationsPropositionInput { all: FileMetaInformationPropositionInput @cost(weight: "10") + any: Boolean @cost(weight: "10") none: FileMetaInformationPropositionInput @cost(weight: "10") some: FileMetaInformationPropositionInput @cost(weight: "10") - any: Boolean @cost(weight: "10") } input FilterInputTypeOfGetHttpsResourcesPropositionInput { all: GetHttpsResourceTreeNonRootVertexPropositionInput @cost(weight: "10") + any: Boolean @cost(weight: "10") none: GetHttpsResourceTreeNonRootVertexPropositionInput @cost(weight: "10") some: GetHttpsResourceTreeNonRootVertexPropositionInput @cost(weight: "10") - any: Boolean @cost(weight: "10") } input FilterInputTypeOfNamedMethodArgumentsPropositionInput { all: NamedMethodArgumentPropositionInput @cost(weight: "10") + any: Boolean @cost(weight: "10") none: NamedMethodArgumentPropositionInput @cost(weight: "10") some: NamedMethodArgumentPropositionInput @cost(weight: "10") - any: Boolean @cost(weight: "10") } input FilterInputTypeOfNamedMethodSourcesPropositionInput { all: NamedMethodSourcePropositionInput @cost(weight: "10") + any: Boolean @cost(weight: "10") none: NamedMethodSourcePropositionInput @cost(weight: "10") some: NamedMethodSourcePropositionInput @cost(weight: "10") - any: Boolean @cost(weight: "10") } input FloatPropositionInput { equalTo: Float @cost(weight: "10") - notEqualTo: Float @cost(weight: "10") - in: [Float] @cost(weight: "10") - notIn: [Float] @cost(weight: "10") greaterThan: Float @cost(weight: "10") - notGreaterThan: Float @cost(weight: "10") greaterThanOrEqualTo: Float @cost(weight: "10") - notGreaterThanOrEqualTo: Float @cost(weight: "10") + in: [Float] @cost(weight: "10") + inClosedInterval: ClosedIntervalInput @cost(weight: "10") lessThan: Float @cost(weight: "10") - notLessThan: Float @cost(weight: "10") lessThanOrEqualTo: Float @cost(weight: "10") + notEqualTo: Float @cost(weight: "10") + notGreaterThan: Float @cost(weight: "10") + notGreaterThanOrEqualTo: Float @cost(weight: "10") + notIn: [Float] @cost(weight: "10") + notLessThan: Float @cost(weight: "10") notLessThanOrEqualTo: Float @cost(weight: "10") - inClosedInterval: ClosedIntervalInput @cost(weight: "10") } input FloatsPropositionInput { all: FloatPropositionInput @cost(weight: "10") + any: Boolean @cost(weight: "10") none: FloatPropositionInput @cost(weight: "10") some: FloatPropositionInput @cost(weight: "10") - any: Boolean @cost(weight: "10") } input GeometricDataPropositionInput { and: [GeometricDataPropositionInput!] - or: [GeometricDataPropositionInput!] + appliedMethod: AppliedMethodPropositionInput + approvals: FilterInputTypeOfDataApprovalsPropositionInput + componentId: UuidPropositionInput + createdAt: DateTimePropositionInput + creatorId: UuidPropositionInput + description: StringPropositionInput id: UuidPropositionInput - userId: UuidPropositionInput locale: StringPropositionInput name: StringPropositionInput - description: StringPropositionInput - componentId: UuidPropositionInput - creatorId: UuidPropositionInput - createdAt: OffsetDateTimePropositionInput - appliedMethod: AppliedMethodPropositionInput - approvals: FilterInputTypeOfDataApprovalsPropositionInput + or: [GeometricDataPropositionInput!] resources: FilterInputTypeOfGetHttpsResourcesPropositionInput - warnings: StringsPropositionInput thicknesses: FloatsPropositionInput + updatedAt: DateTimePropositionInput + userId: UuidPropositionInput + warnings: StringsPropositionInput } input GeometricDataSortInput { + appliedMethod: AppliedMethodSortInput @cost(weight: "10") + componentId: SortEnumType @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") + creatorId: SortEnumType @cost(weight: "10") + description: SortEnumType @cost(weight: "10") id: SortEnumType @cost(weight: "10") locale: SortEnumType @cost(weight: "10") name: SortEnumType @cost(weight: "10") - description: SortEnumType @cost(weight: "10") - componentId: SortEnumType @cost(weight: "10") - creatorId: SortEnumType @cost(weight: "10") - createdAt: SortEnumType @cost(weight: "10") - appliedMethod: AppliedMethodSortInput @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input GetHttpsResourcePropositionInput { and: [GetHttpsResourcePropositionInput!] - or: [GetHttpsResourcePropositionInput!] - id: UuidPropositionInput - description: StringPropositionInput - hashValue: StringPropositionInput - dataFormatId: UuidPropositionInput appliedConversionMethod: ToTreeVertexAppliedConversionMethodPropositionInput archivedFilesMetaInformation: FilterInputTypeOfFileMetaInformationsPropositionInput - parent: GetHttpsResourceTreeNonRootVertexPropositionInput calorimetricData: CalorimetricDataPropositionInput + dataFormatId: UuidPropositionInput + description: StringPropositionInput geometricData: GeometricDataPropositionInput + hashValue: StringPropositionInput hygrothermalData: HygrothermalDataPropositionInput lifeCycleData: LifeCycleDataPropositionInput opticalData: OpticalDataPropositionInput + or: [GetHttpsResourcePropositionInput!] + parent: GetHttpsResourceTreeNonRootVertexPropositionInput photovoltaicData: PhotovoltaicDataPropositionInput } input GetHttpsResourceSortInput { - id: SortEnumType @cost(weight: "10") - description: SortEnumType @cost(weight: "10") - hashValue: SortEnumType @cost(weight: "10") - dataFormatId: SortEnumType @cost(weight: "10") appliedConversionMethod: ToTreeVertexAppliedConversionMethodSortInput @cost(weight: "10") - parent: GetHttpsResourceTreeNonRootVertSortInput @cost(weight: "10") -} - -input GetHttpsResourceTreeNonRootVertSortInput { - id: SortEnumType @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") + dataFormatId: SortEnumType @cost(weight: "10") description: SortEnumType @cost(weight: "10") hashValue: SortEnumType @cost(weight: "10") - dataFormatId: SortEnumType @cost(weight: "10") - appliedConversionMethod: ToTreeVertexAppliedConversionMethodSortInput @cost(weight: "10") + id: SortEnumType @cost(weight: "10") parent: GetHttpsResourceTreeNonRootVertSortInput @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input GetHttpsResourceTreeNonRootVertexPropositionInput { and: [GetHttpsResourceTreeNonRootVertexPropositionInput!] - or: [GetHttpsResourceTreeNonRootVertexPropositionInput!] - id: UuidPropositionInput - description: StringPropositionInput - hashValue: StringPropositionInput - dataFormatId: UuidPropositionInput appliedConversionMethod: ToTreeVertexAppliedConversionMethodPropositionInput archivedFilesMetaInformation: FilterInputTypeOfFileMetaInformationsPropositionInput + dataFormatId: UuidPropositionInput + description: StringPropositionInput + hashValue: StringPropositionInput + or: [GetHttpsResourceTreeNonRootVertexPropositionInput!] parent: GetHttpsResourceTreeNonRootVertexPropositionInput } +input GetHttpsResourceTreeNonRootVertSortInput { + appliedConversionMethod: ToTreeVertexAppliedConversionMethodSortInput @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") + dataFormatId: SortEnumType @cost(weight: "10") + description: SortEnumType @cost(weight: "10") + hashValue: SortEnumType @cost(weight: "10") + id: SortEnumType @cost(weight: "10") + parent: GetHttpsResourceTreeNonRootVertSortInput @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") +} + input HygrothermalDataPropositionInput { and: [HygrothermalDataPropositionInput!] - or: [HygrothermalDataPropositionInput!] + appliedMethod: AppliedMethodPropositionInput + approvals: FilterInputTypeOfDataApprovalsPropositionInput + componentId: UuidPropositionInput + createdAt: DateTimePropositionInput + creatorId: UuidPropositionInput + description: StringPropositionInput id: UuidPropositionInput - userId: UuidPropositionInput locale: StringPropositionInput name: StringPropositionInput - description: StringPropositionInput - componentId: UuidPropositionInput - creatorId: UuidPropositionInput - createdAt: OffsetDateTimePropositionInput - appliedMethod: AppliedMethodPropositionInput - approvals: FilterInputTypeOfDataApprovalsPropositionInput + or: [HygrothermalDataPropositionInput!] resources: FilterInputTypeOfGetHttpsResourcesPropositionInput + updatedAt: DateTimePropositionInput + userId: UuidPropositionInput warnings: StringsPropositionInput } input HygrothermalDataSortInput { + appliedMethod: AppliedMethodSortInput @cost(weight: "10") + componentId: SortEnumType @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") + creatorId: SortEnumType @cost(weight: "10") + description: SortEnumType @cost(weight: "10") id: SortEnumType @cost(weight: "10") locale: SortEnumType @cost(weight: "10") name: SortEnumType @cost(weight: "10") - description: SortEnumType @cost(weight: "10") - componentId: SortEnumType @cost(weight: "10") - creatorId: SortEnumType @cost(weight: "10") - createdAt: SortEnumType @cost(weight: "10") - appliedMethod: AppliedMethodSortInput @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input IntPropositionInput { equalTo: Int @cost(weight: "10") - notEqualTo: Int @cost(weight: "10") - in: [Int] @cost(weight: "10") - notIn: [Int] @cost(weight: "10") greaterThan: Int @cost(weight: "10") - notGreaterThan: Int @cost(weight: "10") greaterThanOrEqualTo: Int @cost(weight: "10") - notGreaterThanOrEqualTo: Int @cost(weight: "10") + in: [Int] @cost(weight: "10") lessThan: Int @cost(weight: "10") - notLessThan: Int @cost(weight: "10") lessThanOrEqualTo: Int @cost(weight: "10") + notEqualTo: Int @cost(weight: "10") + notGreaterThan: Int @cost(weight: "10") + notGreaterThanOrEqualTo: Int @cost(weight: "10") + notIn: [Int] @cost(weight: "10") + notLessThan: Int @cost(weight: "10") notLessThanOrEqualTo: Int @cost(weight: "10") } -input IsoDayOfWeekPropositionInput { - equalTo: IsoDayOfWeek @cost(weight: "10") - notEqualTo: IsoDayOfWeek @cost(weight: "10") - in: [IsoDayOfWeek!] @cost(weight: "10") - notIn: [IsoDayOfWeek!] @cost(weight: "10") -} - input JsonElementPropositionInput { and: [JsonElementPropositionInput!] or: [JsonElementPropositionInput!] @@ -1498,107 +1908,103 @@ input JsonElementPropositionInput { input JsonValueKindPropositionInput { equalTo: JsonValueKind @cost(weight: "10") - notEqualTo: JsonValueKind @cost(weight: "10") in: [JsonValueKind!] @cost(weight: "10") + notEqualTo: JsonValueKind @cost(weight: "10") notIn: [JsonValueKind!] @cost(weight: "10") } -input KeyValuePairOfGuidAndNullableOfUInt32Input { +input KeyValuePairOfGuidAndNullableUInt32Input { key: Uuid! value: NonNegativeInt } input LifeCycleDataPropositionInput { and: [LifeCycleDataPropositionInput!] - or: [LifeCycleDataPropositionInput!] + appliedMethod: AppliedMethodPropositionInput + approvals: FilterInputTypeOfDataApprovalsPropositionInput + componentId: UuidPropositionInput + createdAt: DateTimePropositionInput + creatorId: UuidPropositionInput + description: StringPropositionInput id: UuidPropositionInput - userId: UuidPropositionInput locale: StringPropositionInput name: StringPropositionInput - description: StringPropositionInput - componentId: UuidPropositionInput - creatorId: UuidPropositionInput - createdAt: OffsetDateTimePropositionInput - appliedMethod: AppliedMethodPropositionInput - approvals: FilterInputTypeOfDataApprovalsPropositionInput + or: [LifeCycleDataPropositionInput!] resources: FilterInputTypeOfGetHttpsResourcesPropositionInput + updatedAt: DateTimePropositionInput + userId: UuidPropositionInput warnings: StringsPropositionInput } input LifeCycleDataSortInput { + appliedMethod: AppliedMethodSortInput @cost(weight: "10") + componentId: SortEnumType @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") + creatorId: SortEnumType @cost(weight: "10") + description: SortEnumType @cost(weight: "10") id: SortEnumType @cost(weight: "10") locale: SortEnumType @cost(weight: "10") name: SortEnumType @cost(weight: "10") - description: SortEnumType @cost(weight: "10") - componentId: SortEnumType @cost(weight: "10") - creatorId: SortEnumType @cost(weight: "10") - createdAt: SortEnumType @cost(weight: "10") - appliedMethod: AppliedMethodSortInput @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input LocalDatePropositionInput { - and: [LocalDatePropositionInput!] - or: [LocalDatePropositionInput!] - calendar: CalendarSystemPropositionInput - year: IntPropositionInput - month: IntPropositionInput - day: IntPropositionInput - dayOfWeek: IsoDayOfWeekPropositionInput - yearOfEra: IntPropositionInput - era: EraPropositionInput - dayOfYear: IntPropositionInput + equalTo: LocalDate @cost(weight: "10") + greaterThan: LocalDate @cost(weight: "10") + greaterThanOrEqualTo: LocalDate @cost(weight: "10") + in: [LocalDate] @cost(weight: "10") + lessThan: LocalDate @cost(weight: "10") + lessThanOrEqualTo: LocalDate @cost(weight: "10") + notEqualTo: LocalDate @cost(weight: "10") + notGreaterThan: LocalDate @cost(weight: "10") + notGreaterThanOrEqualTo: LocalDate @cost(weight: "10") + notIn: [LocalDate] @cost(weight: "10") + notLessThan: LocalDate @cost(weight: "10") + notLessThanOrEqualTo: LocalDate @cost(weight: "10") } input LocalDateTimePropositionInput { - and: [LocalDateTimePropositionInput!] - or: [LocalDateTimePropositionInput!] - calendar: CalendarSystemPropositionInput - year: IntPropositionInput - yearOfEra: IntPropositionInput - era: EraPropositionInput - month: IntPropositionInput - dayOfYear: IntPropositionInput - day: IntPropositionInput - dayOfWeek: IsoDayOfWeekPropositionInput - hour: IntPropositionInput - clockHourOfHalfDay: IntPropositionInput - minute: IntPropositionInput - second: IntPropositionInput - millisecond: IntPropositionInput - tickOfSecond: IntPropositionInput - tickOfDay: LongPropositionInput - nanosecondOfSecond: IntPropositionInput - nanosecondOfDay: LongPropositionInput - timeOfDay: LocalTimePropositionInput - date: LocalDatePropositionInput + equalTo: LocalDateTime @cost(weight: "10") + greaterThan: LocalDateTime @cost(weight: "10") + greaterThanOrEqualTo: LocalDateTime @cost(weight: "10") + in: [LocalDateTime] @cost(weight: "10") + lessThan: LocalDateTime @cost(weight: "10") + lessThanOrEqualTo: LocalDateTime @cost(weight: "10") + notEqualTo: LocalDateTime @cost(weight: "10") + notGreaterThan: LocalDateTime @cost(weight: "10") + notGreaterThanOrEqualTo: LocalDateTime @cost(weight: "10") + notIn: [LocalDateTime] @cost(weight: "10") + notLessThan: LocalDateTime @cost(weight: "10") + notLessThanOrEqualTo: LocalDateTime @cost(weight: "10") } input LocalTimePropositionInput { - and: [LocalTimePropositionInput!] - or: [LocalTimePropositionInput!] - hour: IntPropositionInput - clockHourOfHalfDay: IntPropositionInput - minute: IntPropositionInput - second: IntPropositionInput - millisecond: IntPropositionInput - tickOfSecond: IntPropositionInput - tickOfDay: LongPropositionInput - nanosecondOfSecond: IntPropositionInput - nanosecondOfDay: LongPropositionInput + equalTo: LocalTime @cost(weight: "10") + greaterThan: LocalTime @cost(weight: "10") + greaterThanOrEqualTo: LocalTime @cost(weight: "10") + in: [LocalTime] @cost(weight: "10") + lessThan: LocalTime @cost(weight: "10") + lessThanOrEqualTo: LocalTime @cost(weight: "10") + notEqualTo: LocalTime @cost(weight: "10") + notGreaterThan: LocalTime @cost(weight: "10") + notGreaterThanOrEqualTo: LocalTime @cost(weight: "10") + notIn: [LocalTime] @cost(weight: "10") + notLessThan: LocalTime @cost(weight: "10") + notLessThanOrEqualTo: LocalTime @cost(weight: "10") } input LongPropositionInput { equalTo: Long @cost(weight: "10") - notEqualTo: Long @cost(weight: "10") - in: [Long] @cost(weight: "10") - notIn: [Long] @cost(weight: "10") greaterThan: Long @cost(weight: "10") - notGreaterThan: Long @cost(weight: "10") greaterThanOrEqualTo: Long @cost(weight: "10") - notGreaterThanOrEqualTo: Long @cost(weight: "10") + in: [Long] @cost(weight: "10") lessThan: Long @cost(weight: "10") - notLessThan: Long @cost(weight: "10") lessThanOrEqualTo: Long @cost(weight: "10") + notEqualTo: Long @cost(weight: "10") + notGreaterThan: Long @cost(weight: "10") + notGreaterThanOrEqualTo: Long @cost(weight: "10") + notIn: [Long] @cost(weight: "10") + notLessThan: Long @cost(weight: "10") notLessThanOrEqualTo: Long @cost(weight: "10") } @@ -1609,8 +2015,8 @@ input NamedMethodArgumentInput { input NamedMethodArgumentPropositionInput { and: [NamedMethodArgumentPropositionInput!] - or: [NamedMethodArgumentPropositionInput!] name: StringPropositionInput + or: [NamedMethodArgumentPropositionInput!] } input NamedMethodArgumentSortInput { @@ -1624,43 +2030,43 @@ input NamedMethodSourceInput { input NamedMethodSourcePropositionInput { and: [NamedMethodSourcePropositionInput!] - or: [NamedMethodSourcePropositionInput!] name: StringPropositionInput + or: [NamedMethodSourcePropositionInput!] value: CrossDatabaseDataReferencePropositionInput } input NullableOfCalorimetricObserverPropositionInput { equalTo: CalorimetricObserver @cost(weight: "10") - notEqualTo: CalorimetricObserver @cost(weight: "10") in: [CalorimetricObserver] @cost(weight: "10") + notEqualTo: CalorimetricObserver @cost(weight: "10") notIn: [CalorimetricObserver] @cost(weight: "10") } input NullableOfCoatedSidePropositionInput { equalTo: CoatedSide @cost(weight: "10") - notEqualTo: CoatedSide @cost(weight: "10") in: [CoatedSide] @cost(weight: "10") + notEqualTo: CoatedSide @cost(weight: "10") notIn: [CoatedSide] @cost(weight: "10") } input NullableOfIlluminantPropositionInput { equalTo: Illuminant @cost(weight: "10") - notEqualTo: Illuminant @cost(weight: "10") in: [Illuminant] @cost(weight: "10") + notEqualTo: Illuminant @cost(weight: "10") notIn: [Illuminant] @cost(weight: "10") } input NullableOfOpticalComponentSubtypePropositionInput { equalTo: OpticalComponentSubtype @cost(weight: "10") - notEqualTo: OpticalComponentSubtype @cost(weight: "10") in: [OpticalComponentSubtype] @cost(weight: "10") + notEqualTo: OpticalComponentSubtype @cost(weight: "10") notIn: [OpticalComponentSubtype] @cost(weight: "10") } input NullableOfOpticalComponentTypePropositionInput { equalTo: OpticalComponentType @cost(weight: "10") - notEqualTo: OpticalComponentType @cost(weight: "10") in: [OpticalComponentType] @cost(weight: "10") + notEqualTo: OpticalComponentType @cost(weight: "10") notIn: [OpticalComponentType] @cost(weight: "10") } @@ -1672,117 +2078,86 @@ input NumerationInput { input NumerationPropositionInput { and: [NumerationPropositionInput!] + mainNumber: StringPropositionInput or: [NumerationPropositionInput!] prefix: StringPropositionInput - mainNumber: StringPropositionInput suffix: StringPropositionInput } input NumerationSortInput { - prefix: SortEnumType @cost(weight: "10") mainNumber: SortEnumType @cost(weight: "10") + prefix: SortEnumType @cost(weight: "10") suffix: SortEnumType @cost(weight: "10") } -input OffsetDateTimePropositionInput { - and: [OffsetDateTimePropositionInput!] - or: [OffsetDateTimePropositionInput!] - calendar: CalendarSystemPropositionInput - year: IntPropositionInput - month: IntPropositionInput - day: IntPropositionInput - dayOfWeek: IsoDayOfWeekPropositionInput - yearOfEra: IntPropositionInput - era: EraPropositionInput - dayOfYear: IntPropositionInput - hour: IntPropositionInput - clockHourOfHalfDay: IntPropositionInput - minute: IntPropositionInput - second: IntPropositionInput - millisecond: IntPropositionInput - tickOfSecond: IntPropositionInput - tickOfDay: LongPropositionInput - nanosecondOfSecond: IntPropositionInput - nanosecondOfDay: LongPropositionInput - localDateTime: LocalDateTimePropositionInput - date: LocalDatePropositionInput - timeOfDay: LocalTimePropositionInput - offset: OffsetPropositionInput -} - -input OffsetPropositionInput { - and: [OffsetPropositionInput!] - or: [OffsetPropositionInput!] - seconds: IntPropositionInput - milliseconds: IntPropositionInput - ticks: LongPropositionInput - nanoseconds: LongPropositionInput -} - input OpticalDataPropositionInput { and: [OpticalDataPropositionInput!] - or: [OpticalDataPropositionInput!] - id: UuidPropositionInput - userId: UuidPropositionInput - locale: StringPropositionInput - name: StringPropositionInput - description: StringPropositionInput - componentId: UuidPropositionInput - creatorId: UuidPropositionInput - createdAt: OffsetDateTimePropositionInput appliedMethod: AppliedMethodPropositionInput approvals: FilterInputTypeOfDataApprovalsPropositionInput - resources: FilterInputTypeOfGetHttpsResourcesPropositionInput - warnings: StringsPropositionInput - type: NullableOfOpticalComponentTypePropositionInput - subtype: NullableOfOpticalComponentSubtypePropositionInput + cielabColors: FilterInputTypeOfCielabColorsPropositionInput coatedSide: NullableOfCoatedSidePropositionInput + colorRenderingIndices: FloatsPropositionInput + componentId: UuidPropositionInput + createdAt: DateTimePropositionInput + creatorId: UuidPropositionInput + description: StringPropositionInput + id: UuidPropositionInput + infraredEmittances: FloatsPropositionInput + locale: StringPropositionInput + name: StringPropositionInput nearnormalHemisphericalSolarReflectances: FloatsPropositionInput nearnormalHemisphericalSolarTransmittances: FloatsPropositionInput nearnormalHemisphericalVisibleReflectances: FloatsPropositionInput nearnormalHemisphericalVisibleTransmittances: FloatsPropositionInput - infraredEmittances: FloatsPropositionInput - colorRenderingIndices: FloatsPropositionInput - cielabColors: FilterInputTypeOfCielabColorsPropositionInput + or: [OpticalDataPropositionInput!] + resources: FilterInputTypeOfGetHttpsResourcesPropositionInput + subtype: NullableOfOpticalComponentSubtypePropositionInput + type: NullableOfOpticalComponentTypePropositionInput + updatedAt: DateTimePropositionInput + userId: UuidPropositionInput + warnings: StringsPropositionInput } input OpticalDataSortInput { + appliedMethod: AppliedMethodSortInput @cost(weight: "10") + componentId: SortEnumType @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") + creatorId: SortEnumType @cost(weight: "10") + description: SortEnumType @cost(weight: "10") id: SortEnumType @cost(weight: "10") locale: SortEnumType @cost(weight: "10") name: SortEnumType @cost(weight: "10") - description: SortEnumType @cost(weight: "10") - componentId: SortEnumType @cost(weight: "10") - creatorId: SortEnumType @cost(weight: "10") - createdAt: SortEnumType @cost(weight: "10") - appliedMethod: AppliedMethodSortInput @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input PhotovoltaicDataPropositionInput { and: [PhotovoltaicDataPropositionInput!] - or: [PhotovoltaicDataPropositionInput!] + appliedMethod: AppliedMethodPropositionInput + approvals: FilterInputTypeOfDataApprovalsPropositionInput + componentId: UuidPropositionInput + createdAt: DateTimePropositionInput + creatorId: UuidPropositionInput + description: StringPropositionInput id: UuidPropositionInput - userId: UuidPropositionInput locale: StringPropositionInput name: StringPropositionInput - description: StringPropositionInput - componentId: UuidPropositionInput - creatorId: UuidPropositionInput - createdAt: OffsetDateTimePropositionInput - appliedMethod: AppliedMethodPropositionInput - approvals: FilterInputTypeOfDataApprovalsPropositionInput + or: [PhotovoltaicDataPropositionInput!] resources: FilterInputTypeOfGetHttpsResourcesPropositionInput + updatedAt: DateTimePropositionInput + userId: UuidPropositionInput warnings: StringsPropositionInput } input PhotovoltaicDataSortInput { + appliedMethod: AppliedMethodSortInput @cost(weight: "10") + componentId: SortEnumType @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") + creatorId: SortEnumType @cost(weight: "10") + description: SortEnumType @cost(weight: "10") id: SortEnumType @cost(weight: "10") locale: SortEnumType @cost(weight: "10") name: SortEnumType @cost(weight: "10") - description: SortEnumType @cost(weight: "10") - componentId: SortEnumType @cost(weight: "10") - creatorId: SortEnumType @cost(weight: "10") - createdAt: SortEnumType @cost(weight: "10") - appliedMethod: AppliedMethodSortInput @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input PublicationInput { @@ -1797,36 +2172,36 @@ input PublicationInput { } input PublicationPropositionInput { + abstract: StringPropositionInput and: [PublicationPropositionInput!] - or: [PublicationPropositionInput!] + "The website arXiv.org is a free and open-access archive for publications. The arXiv identifier can be used to define a publication." + arXiv: StringPropositionInput authors: StringsPropositionInput "The Digital Object Identifier (DOI) is a very important persistent identifier for publications. It MUST be defined here if it is available for a publication." doi: StringPropositionInput - "The website arXiv.org is a free and open-access archive for publications. The arXiv identifier can be used to define a publication." - arXiv: StringPropositionInput + or: [PublicationPropositionInput!] + "Referenced section" + section: StringPropositionInput + title: StringPropositionInput "A Uniform Resource Name (URN) can be used to define a publication. TODO: Improve the regex pattern to further restrict the string." urn: StringPropositionInput "If a persistent identifiert like DOI is defined above, this webAdress can define a convenient web address to access the publication. However, if no persistent identifier exist, this web address is the only identifier of this publication. In this case, it is important to choose a web address with a high probability to persist long." webAddress: UrlPropositionInput - title: StringPropositionInput - abstract: StringPropositionInput - "Referenced section" - section: StringPropositionInput } input PublicationSortInput { - "The Digital Object Identifier (DOI) is a very important persistent identifier for publications. It MUST be defined here if it is available for a publication." - doi: SortEnumType @cost(weight: "10") + abstract: SortEnumType @cost(weight: "10") "The website arXiv.org is a free and open-access archive for publications. The arXiv identifier can be used to define a publication." arXiv: SortEnumType @cost(weight: "10") + "The Digital Object Identifier (DOI) is a very important persistent identifier for publications. It MUST be defined here if it is available for a publication." + doi: SortEnumType @cost(weight: "10") + "Referenced section" + section: SortEnumType @cost(weight: "10") + title: SortEnumType @cost(weight: "10") "A Uniform Resource Name (URN) can be used to define a publication. TODO: Improve the regex pattern to further restrict the string." urn: SortEnumType @cost(weight: "10") "If a persistent identifiert like DOI is defined above, this webAdress can define a convenient web address to access the publication. However, if no persistent identifier exist, this web address is the only identifier of this publication. In this case, it is important to choose a web address with a high probability to persist long." webAddress: UriSortInput @cost(weight: "10") - title: SortEnumType @cost(weight: "10") - abstract: SortEnumType @cost(weight: "10") - "Referenced section" - section: SortEnumType @cost(weight: "10") } input PublishDataInput { @@ -1836,19 +2211,18 @@ input PublishDataInput { input RecomputeGetHttpsResourceHashValuesPropositionInput { and: [RecomputeGetHttpsResourceHashValuesPropositionInput!] - or: [RecomputeGetHttpsResourceHashValuesPropositionInput!] - id: UuidPropositionInput - description: StringPropositionInput - hashValue: StringPropositionInput - dataFormatId: UuidPropositionInput appliedConversionMethod: ToTreeVertexAppliedConversionMethodPropositionInput archivedFilesMetaInformation: FilterInputTypeOfFileMetaInformationsPropositionInput - parent: GetHttpsResourceTreeNonRootVertexPropositionInput calorimetricData: CalorimetricDataPropositionInput + dataFormatId: UuidPropositionInput + description: StringPropositionInput geometricData: GeometricDataPropositionInput + hashValue: StringPropositionInput hygrothermalData: HygrothermalDataPropositionInput lifeCycleData: LifeCycleDataPropositionInput opticalData: OpticalDataPropositionInput + or: [RecomputeGetHttpsResourceHashValuesPropositionInput!] + parent: GetHttpsResourceTreeNonRootVertexPropositionInput photovoltaicData: PhotovoltaicDataPropositionInput } @@ -1860,13 +2234,13 @@ input ReferenceInput { input ReferencePropositionInput { and: [ReferencePropositionInput!] or: [ReferencePropositionInput!] - standard: StandardPropositionInput publication: PublicationPropositionInput + standard: StandardPropositionInput } input ReferenceSortInput { - standard: StandardSortInput @cost(weight: "10") publication: PublicationSortInput @cost(weight: "10") + standard: StandardSortInput @cost(weight: "10") } input RemoveDataApprovalInput { @@ -1894,16 +2268,16 @@ input SetGetHttpsResourceParentInput { input ShortPropositionInput { equalTo: Short @cost(weight: "10") - notEqualTo: Short @cost(weight: "10") - in: [Short] @cost(weight: "10") - notIn: [Short] @cost(weight: "10") greaterThan: Short @cost(weight: "10") - notGreaterThan: Short @cost(weight: "10") greaterThanOrEqualTo: Short @cost(weight: "10") - notGreaterThanOrEqualTo: Short @cost(weight: "10") + in: [Short] @cost(weight: "10") lessThan: Short @cost(weight: "10") - notLessThan: Short @cost(weight: "10") lessThanOrEqualTo: Short @cost(weight: "10") + notEqualTo: Short @cost(weight: "10") + notGreaterThan: Short @cost(weight: "10") + notGreaterThanOrEqualTo: Short @cost(weight: "10") + notIn: [Short] @cost(weight: "10") + notLessThan: Short @cost(weight: "10") notLessThanOrEqualTo: Short @cost(weight: "10") } @@ -1917,82 +2291,67 @@ input StandardInput { year: Int } +input StandardizerPropositionInput { + equalTo: Standardizer @cost(weight: "10") + in: [Standardizer!] @cost(weight: "10") + notEqualTo: Standardizer @cost(weight: "10") + notIn: [Standardizer!] @cost(weight: "10") +} + +input StandardizersPropositionInput { + all: StandardizerPropositionInput @cost(weight: "10") + any: Boolean @cost(weight: "10") + none: StandardizerPropositionInput @cost(weight: "10") + some: StandardizerPropositionInput @cost(weight: "10") +} + "`ISO 52022` is an example of the abbreviation of a standardizer and the main number of the identifier." input StandardPropositionInput { + abstract: StringPropositionInput and: [StandardPropositionInput!] - or: [StandardPropositionInput!] - "It is important to define the year in which the standard was issued because there can be relevant updates of one standard." - year: IntPropositionInput - numeration: NumerationPropositionInput - standardizers: StandardizersPropositionInput locator: UrlPropositionInput - title: StringPropositionInput - abstract: StringPropositionInput + numeration: NumerationPropositionInput + or: [StandardPropositionInput!] "The section of the standard to which the reference refers to." section: StringPropositionInput + standardizers: StandardizersPropositionInput + title: StringPropositionInput + "It is important to define the year in which the standard was issued because there can be relevant updates of one standard." + year: IntPropositionInput } "`ISO 52022` is an example of the abbreviation of a standardizer and the main number of the identifier." input StandardSortInput { - "It is important to define the year in which the standard was issued because there can be relevant updates of one standard." - year: SortEnumType @cost(weight: "10") - numeration: NumerationSortInput @cost(weight: "10") - locator: UriSortInput @cost(weight: "10") - title: SortEnumType @cost(weight: "10") abstract: SortEnumType @cost(weight: "10") + locator: UriSortInput @cost(weight: "10") + numeration: NumerationSortInput @cost(weight: "10") "The section of the standard to which the reference refers to." section: SortEnumType @cost(weight: "10") -} - -input StandardizerPropositionInput { - equalTo: Standardizer @cost(weight: "10") - notEqualTo: Standardizer @cost(weight: "10") - in: [Standardizer!] @cost(weight: "10") - notIn: [Standardizer!] @cost(weight: "10") -} - -input StandardizersPropositionInput { - all: StandardizerPropositionInput @cost(weight: "10") - none: StandardizerPropositionInput @cost(weight: "10") - some: StandardizerPropositionInput @cost(weight: "10") - any: Boolean @cost(weight: "10") + title: SortEnumType @cost(weight: "10") + "It is important to define the year in which the standard was issued because there can be relevant updates of one standard." + year: SortEnumType @cost(weight: "10") } input StringPropositionInput { and: [StringPropositionInput!] - or: [StringPropositionInput!] - equalTo: String @cost(weight: "10") - notEqualTo: String @cost(weight: "10") contains: String @cost(weight: "20") doesNotContain: String @cost(weight: "20") + doesNotEndWith: String @cost(weight: "20") + doesNotStartWith: String @cost(weight: "20") + endsWith: String @cost(weight: "20") + equalTo: String @cost(weight: "10") in: [String] @cost(weight: "10") + notEqualTo: String @cost(weight: "10") notIn: [String] @cost(weight: "10") + or: [StringPropositionInput!] startsWith: String @cost(weight: "20") - doesNotStartWith: String @cost(weight: "20") - endsWith: String @cost(weight: "20") - doesNotEndWith: String @cost(weight: "20") } input StringsPropositionInput { all: StringPropositionInput @cost(weight: "10") + any: Boolean @cost(weight: "10") none: StringPropositionInput @cost(weight: "10") some: StringPropositionInput @cost(weight: "10") - any: Boolean @cost(weight: "10") -} - -input TimeSpanPropositionInput { - equalTo: TimeSpan @cost(weight: "10") - notEqualTo: TimeSpan @cost(weight: "10") - in: [TimeSpan] @cost(weight: "10") - notIn: [TimeSpan] @cost(weight: "10") - greaterThan: TimeSpan @cost(weight: "10") - notGreaterThan: TimeSpan @cost(weight: "10") - greaterThanOrEqualTo: TimeSpan @cost(weight: "10") - notGreaterThanOrEqualTo: TimeSpan @cost(weight: "10") - lessThan: TimeSpan @cost(weight: "10") - notLessThan: TimeSpan @cost(weight: "10") - lessThanOrEqualTo: TimeSpan @cost(weight: "10") - notLessThanOrEqualTo: TimeSpan @cost(weight: "10") } input ToTreeVertexAppliedConversionMethodInput { @@ -2003,9 +2362,9 @@ input ToTreeVertexAppliedConversionMethodInput { input ToTreeVertexAppliedConversionMethodPropositionInput { and: [ToTreeVertexAppliedConversionMethodPropositionInput!] - or: [ToTreeVertexAppliedConversionMethodPropositionInput!] - methodId: UuidPropositionInput arguments: FilterInputTypeOfNamedMethodArgumentsPropositionInput + methodId: UuidPropositionInput + or: [ToTreeVertexAppliedConversionMethodPropositionInput!] sourceName: StringPropositionInput } @@ -2025,11 +2384,18 @@ input UpdateChildGetHttpsResourceInput { input UpdateDataAccessRightsInput { allowedApplications: [String!] allowedInstitutions: [Uuid!] - allowedUserAndQuantity: [KeyValuePairOfGuidAndNullableOfUInt32Input!] + allowedUserAndQuantity: [KeyValuePairOfGuidAndNullableUInt32Input!] dataId: Uuid! dataKind: DataKind! } +input UpdateDatabaseInput { + databaseId: Uuid! + description: String! + locator: Url! + name: String! +} + input UpdateDataInput { componentId: Uuid! createdAt: DateTime! @@ -2042,13 +2408,6 @@ input UpdateDataInput { warnings: [String!]! } -input UpdateDatabaseInput { - databaseId: Uuid! - description: String! - locator: Url! - name: String! -} - input UpdateInstitutionAccessRightsInput { allowedDatasetsPerTimeSpan: NonNegativeInt allowedUserCount: NonNegativeInt @@ -2058,18 +2417,16 @@ input UpdateInstitutionAccessRightsInput { input UpdateResponseApprovalsPropositionInput { and: [UpdateResponseApprovalsPropositionInput!] - or: [UpdateResponseApprovalsPropositionInput!] - id: UuidPropositionInput - userId: UuidPropositionInput - locale: StringPropositionInput - name: StringPropositionInput - description: StringPropositionInput - componentId: UuidPropositionInput - creatorId: UuidPropositionInput - createdAt: OffsetDateTimePropositionInput appliedMethod: AppliedMethodPropositionInput approvals: FilterInputTypeOfDataApprovalsPropositionInput + componentId: UuidPropositionInput + creatorId: UuidPropositionInput + description: StringPropositionInput + locale: StringPropositionInput + name: StringPropositionInput + or: [UpdateResponseApprovalsPropositionInput!] resources: FilterInputTypeOfGetHttpsResourcesPropositionInput + userId: UuidPropositionInput warnings: StringsPropositionInput } @@ -2083,54 +2440,54 @@ input UpdateRootGetHttpsResourceInput { input UriSortInput { absolutePath: SortEnumType @cost(weight: "10") absoluteUri: SortEnumType @cost(weight: "10") - localPath: SortEnumType @cost(weight: "10") authority: SortEnumType @cost(weight: "10") + dnsSafeHost: SortEnumType @cost(weight: "10") + fragment: SortEnumType @cost(weight: "10") + host: SortEnumType @cost(weight: "10") hostNameType: SortEnumType @cost(weight: "10") + idnHost: SortEnumType @cost(weight: "10") + isAbsoluteUri: SortEnumType @cost(weight: "10") isDefaultPort: SortEnumType @cost(weight: "10") isFile: SortEnumType @cost(weight: "10") isLoopback: SortEnumType @cost(weight: "10") - pathAndQuery: SortEnumType @cost(weight: "10") isUnc: SortEnumType @cost(weight: "10") - host: SortEnumType @cost(weight: "10") + localPath: SortEnumType @cost(weight: "10") + originalString: SortEnumType @cost(weight: "10") + pathAndQuery: SortEnumType @cost(weight: "10") port: SortEnumType @cost(weight: "10") query: SortEnumType @cost(weight: "10") - fragment: SortEnumType @cost(weight: "10") scheme: SortEnumType @cost(weight: "10") - originalString: SortEnumType @cost(weight: "10") - dnsSafeHost: SortEnumType @cost(weight: "10") - idnHost: SortEnumType @cost(weight: "10") - isAbsoluteUri: SortEnumType @cost(weight: "10") userEscaped: SortEnumType @cost(weight: "10") userInfo: SortEnumType @cost(weight: "10") } input UrlPropositionInput { equalTo: Url @cost(weight: "10") - notEqualTo: Url @cost(weight: "10") - in: [Url] @cost(weight: "10") - notIn: [Url] @cost(weight: "10") greaterThan: Url @cost(weight: "10") - notGreaterThan: Url @cost(weight: "10") greaterThanOrEqualTo: Url @cost(weight: "10") - notGreaterThanOrEqualTo: Url @cost(weight: "10") + in: [Url] @cost(weight: "10") lessThan: Url @cost(weight: "10") - notLessThan: Url @cost(weight: "10") lessThanOrEqualTo: Url @cost(weight: "10") + notEqualTo: Url @cost(weight: "10") + notGreaterThan: Url @cost(weight: "10") + notGreaterThanOrEqualTo: Url @cost(weight: "10") + notIn: [Url] @cost(weight: "10") + notLessThan: Url @cost(weight: "10") notLessThanOrEqualTo: Url @cost(weight: "10") } input UuidPropositionInput { equalTo: Uuid @cost(weight: "10") - notEqualTo: Uuid @cost(weight: "10") - in: [Uuid] @cost(weight: "10") - notIn: [Uuid] @cost(weight: "10") greaterThan: Uuid @cost(weight: "10") - notGreaterThan: Uuid @cost(weight: "10") greaterThanOrEqualTo: Uuid @cost(weight: "10") - notGreaterThanOrEqualTo: Uuid @cost(weight: "10") + in: [Uuid] @cost(weight: "10") lessThan: Uuid @cost(weight: "10") - notLessThan: Uuid @cost(weight: "10") lessThanOrEqualTo: Uuid @cost(weight: "10") + notEqualTo: Uuid @cost(weight: "10") + notGreaterThan: Uuid @cost(weight: "10") + notGreaterThanOrEqualTo: Uuid @cost(weight: "10") + notIn: [Uuid] @cost(weight: "10") + notLessThan: Uuid @cost(weight: "10") notLessThanOrEqualTo: Uuid @cost(weight: "10") } @@ -2293,6 +2650,11 @@ enum CreateResponseApprovalsErrorCode { CREATING_RESPONSE_APPROVAL_FAILED } +enum DatabaseVerificationState { + PENDING + VERIFIED +} + enum DataKind { CALORIMETRIC_DATA GEOMETRIC_DATA @@ -2302,11 +2664,6 @@ enum DataKind { PHOTOVOLTAIC_DATA } -enum DatabaseVerificationState { - PENDING - VERIFIED -} - enum DeleteDataErrorCode { UNAUTHENTICATED UNAUTHORIZED @@ -2330,17 +2687,6 @@ enum Illuminant { D65 } -enum IsoDayOfWeek { - NONE - MONDAY - TUESDAY - WEDNESDAY - THURSDAY - FRIDAY - SATURDAY - SUNDAY -} - enum JsonValueKind { UNDEFINED OBJECT @@ -2475,6 +2821,15 @@ enum UpdateDataAccessRightsErrorCode { UNAUTHENTICATED UNAUTHORIZED UNKNOWN_DATA + UNKNOWN_INSTITUTION + UNKNOWN_USER + UNKNOWN_APPLICATION +} + +enum UpdateDatabaseErrorCode { + UNKNOWN + UNAUTHORIZED + UNKNOWN_DATABASE } enum UpdateDataErrorCode { @@ -2484,12 +2839,6 @@ enum UpdateDataErrorCode { CREATING_RESPONSE_APPROVAL_FAILED } -enum UpdateDatabaseErrorCode { - UNKNOWN - UNAUTHORIZED - UNKNOWN_DATABASE -} - enum UpdateInstitutionAccessRightsErrorCode { UNKNOWN UNAUTHENTICATED @@ -2514,63 +2863,83 @@ enum UpdateRootGetHttpsResourceErrorCode { CREATING_RESPONSE_APPROVAL_FAILED } -"The purpose of the `cost` directive is to define a `weight` for GraphQL types, fields, and arguments. Static analysis can use these weights when calculating the overall cost of a query or response." -directive @cost("The `weight` argument defines what value to add to the overall cost for every appearance, or possible appearance, of a type, field, argument, etc." weight: String!) on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM | INPUT_FIELD_DEFINITION - -"The purpose of the `@listSize` directive is to either inform the static analysis about the size of returned lists (if that information is statically available), or to point the analysis to where to find that information." -directive @listSize("The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." assumedSize: Int "The `requireOneSlicingArgument` argument can be used to inform the static analysis that it should expect that exactly one of the defined slicing arguments is present in a query. If that is not the case (i.e., if none or multiple slicing arguments are present), the static analysis may throw an error." requireOneSlicingArgument: Boolean! = true "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." sizedFields: [String!] "The `slicingArgumentDefaultValue` argument can be used to define a default value for a slicing argument, which is used if the argument is not present in a query." slicingArgumentDefaultValue: Int "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." slicingArguments: [String!]) on FIELD_DEFINITION - -"The `@specifiedBy` directive is used within the type system definition language to provide a URL for specifying the behavior of custom scalar definitions." -directive @specifiedBy("The specifiedBy URL points to a human-readable specification. This field will only read a result for scalar types." url: String!) on SCALAR - -scalar Any +scalar Any @specifiedBy(url: "https://scalars.graphql.org/chillicream/any.html") -"The `Byte` scalar type represents non-fractional whole numeric values. Byte can represent values between 0 and 255." +"The `Byte` scalar type represents a signed 8-bit integer." scalar Byte + @specifiedBy(url: "https://scalars.graphql.org/chillicream/byte.html") -"The `DateTime` scalar represents an ISO-8601 compliant date time type." -scalar DateTime @specifiedBy(url: "https:\/\/www.graphql-scalars.com\/date-time") - -""" -Represents a time zone - a mapping between UTC and local time. -A time zone maps UTC instants to local times - or, equivalently, to the offset from UTC at any particular instant. - -Example: `Europe/Zurich` -""" -scalar DateTimeZone +"The `DateTime` scalar type represents a date and time with time zone offset information." +scalar DateTime + @specifiedBy(url: "https://scalars.graphql.org/chillicream/date-time.html") -"The `Decimal` scalar type represents a decimal floating-point number." +"The `Decimal` scalar type represents a decimal floating-point number with high precision." scalar Decimal + @specifiedBy(url: "https://scalars.graphql.org/chillicream/decimal.html") -""" -Represents a fixed (and calendar-independent) length of time. +"The `Duration` scalar type represents a duration of time." +scalar Duration + @specifiedBy(url: "https://scalars.graphql.org/chillicream/duration.html") -Allowed patterns: -- `-D:hh:mm:ss.sssssssss` +"The `LocalDate` scalar type represents a date without time or time zone information." +scalar LocalDate + @specifiedBy(url: "https://scalars.graphql.org/chillicream/local-date.html") -Examples: -- `-1:20:00:00.999999999` -""" -scalar Duration +"The `LocalDateTime` scalar type represents a date and time without time zone information." +scalar LocalDateTime + @specifiedBy( + url: "https://scalars.graphql.org/chillicream/local-date-time.html" + ) "BCP 47 compliant Language Tag string" scalar Locale -"The `Long` scalar type represents non-fractional signed whole 64-bit numeric values. Long can represent values between -(2^63) and 2^63 - 1." +"The `LocalTime` scalar type represents a time of day without date or time zone information." +scalar LocalTime + @specifiedBy(url: "https://scalars.graphql.org/chillicream/local-time.html") + +"The `Long` scalar type represents a signed 64-bit integer." scalar Long + @specifiedBy(url: "https://scalars.graphql.org/chillicream/long.html") -"The NonNegativeInt scalar type represents a unsigned 32-bit numeric non-fractional value equal to or greater than 0." +"The `NonNegativeInt` scalar type represents an unsigned 32-bit numeric non-fractional value." scalar NonNegativeInt -"The `Short` scalar type represents non-fractional signed whole 16-bit numeric values. Short can represent values between -(2^15) and 2^15 - 1." +"The `Short` scalar type represents a signed 16-bit integer." scalar Short - -"The `TimeSpan` scalar represents an ISO-8601 compliant duration type." -scalar TimeSpan + @specifiedBy(url: "https://scalars.graphql.org/chillicream/short.html") "The `Upload` scalar type represents a file upload." scalar Upload -scalar Url +"The `Url` scalar type represents a Uniform Resource Identifier (URI) as defined by RFC 3986." +scalar Url @specifiedBy(url: "https://tools.ietf.org/html/rfc3986") -scalar Uuid \ No newline at end of file +scalar Uuid + @specifiedBy(url: "https://scalars.graphql.org/chillicream/uuid.html") + +"The purpose of the `cost` directive is to define a `weight` for GraphQL types, fields, and arguments. Static analysis can use these weights when calculating the overall cost of a query or response." +directive @cost( + "The `weight` argument defines what value to add to the overall cost for every appearance, or possible appearance, of a type, field, argument, etc." + weight: String! +) on + | SCALAR + | OBJECT + | FIELD_DEFINITION + | ARGUMENT_DEFINITION + | ENUM + | INPUT_FIELD_DEFINITION + +"The purpose of the `@listSize` directive is to either inform the static analysis about the size of returned lists (if that information is statically available), or to point the analysis to where to find that information." +directive @listSize( + "The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." + assumedSize: Int + "The `requireOneSlicingArgument` argument can be used to inform the static analysis that it should expect that exactly one of the defined slicing arguments is present in a query. If that is not the case (i.e., if none or multiple slicing arguments are present), the static analysis may throw an error." + requireOneSlicingArgument: Boolean = true + "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." + sizedFields: [String!] + "The `slicingArgumentDefaultValue` argument can be used to define a default value for a slicing argument, which is used if the argument is not present in a query." + slicingArgumentDefaultValue: Int + "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." + slicingArguments: [String!] +) on FIELD_DEFINITION \ No newline at end of file diff --git a/frontend/codegen.ts b/frontend/codegen.ts index 9e23c42d..8662a5aa 100644 --- a/frontend/codegen.ts +++ b/frontend/codegen.ts @@ -1,5 +1,54 @@ import type { CodegenConfig } from "@graphql-codegen/cli"; +const scalars = { + ID: { + input: "string", + output: "string", + }, + Any: "object", + Byte: "number", + DateTime: "string", + DateTimeZone: "string", + Decimal: "string", + Duration: "string", + LocalDate: "string", + LocalDateTime: "string", + LocalTime: "string", + Locale: "string", + Long: "number", + NonNegativeInt: "number", + Short: "number", + TimeSpan: "string", + UnsignedByte: "string", + Upload: "unknown", + Url: "string", + Uuid: "string", +}; + +// "Apollo Client" conform config +// https://the-guild.dev/graphql/codegen/plugins/typescript/typescript +const graphQlConfig = { + avoidOptionals: { + // Use `null` for nullable fields instead of optionals + field: true, + // Allow nullable input fields to remain unspecified + inputValue: false, + }, + // Use `unknown` instead of `any` for unconfigured scalars + defaultScalarType: "unknown", + // Apollo Client always includes `__typename` fields + nonOptionalTypename: true, + // Apollo Client doesn't add the `__typename` field to root types so + // don't generate a type for the `__typename` for root operation types. + skipTypeNameForRoot: true, + // ... + strictScalars: true, + scalars: scalars, + useTypeImports: true, + // immutableTypes: true, + // nullability: true, +}; + const config: CodegenConfig = { overwrite: true, schema: "./type-defs.graphqls", @@ -10,55 +59,18 @@ const config: CodegenConfig = { // Inspired by https://www.apollographql.com/docs/react/development-testing/graphql-codegen#generating-precompiled-graphql-documents-with-their-type-definitions "./__generated__/graphql.ts": { plugins: ["typescript"], + config: graphQlConfig, }, "./queries/": { preset: "near-operation-file", + // https://the-guild.dev/graphql/codegen/docs/migration/apollo-tooling#per-operation-file-generation-with-near-operation-file presetConfig: { // This should be the file generated by the "typescript" plugin above, // relative to the directory specified for this configuration baseTypesPath: "../__generated__/graphql.ts", }, plugins: ["typescript-operations", "typed-document-node"], - config: { - avoidOptionals: { - // Use `null` for nullable fields instead of optionals - field: true, - // Allow nullable input fields to remain unspecified - inputValue: false, - }, - // Use `unknown` instead of `any` for unconfigured scalars - defaultScalarType: "unknown", - // Apollo Client always includes `__typename` fields - nonOptionalTypename: true, - // Apollo Client doesn't add the `__typename` field to root types so - // don't generate a type for the `__typename` for root operation types. - skipTypeNameForRoot: true, - // ... - strictScalars: true, - scalars: { - ID: { - input: "string", - output: "string", - }, - Any: "unknown", - Byte: "number", - DateTime: "string", - DateTimeZone: "string", - Decimal: "string", - Duration: "string", - Locale: "string", - Long: "number", - NonNegativeInt: "number", - Short: "number", - TimeSpan: "string", - Upload: "unknown", - Url: "string", - Uuid: "string", - }, - useTypeImports: true, - // immutableTypes: true, - // nullability: true, - }, + config: graphQlConfig, }, "./__generated__/fragmentMatcherTypes.ts": { plugins: ["fragment-matcher"], @@ -69,6 +81,9 @@ const config: CodegenConfig = { deterministic: true, }, }, + "./__generated__/apolloHelpers.ts": { + plugins: ["typescript-apollo-client-helpers"], + }, // './queries/': { // preset: 'near-operation-file', // presetConfig: { diff --git a/frontend/components/ActiveFilterAndSortBar.tsx b/frontend/components/ActiveFilterAndSortBar.tsx new file mode 100644 index 00000000..3a4da899 --- /dev/null +++ b/frontend/components/ActiveFilterAndSortBar.tsx @@ -0,0 +1,132 @@ +import { Tag, Space, Typography, Flex } from "antd"; +import { + ObjectFilterState, + getFilterOperatorLabel, + createFilterStateReducer, + FilterDefinition, + FilterStateReducerContext, +} from "../lib/filter"; +import { formatSortDirection, SortState, SortDefinition } from "../lib/sort"; +import DeleteButton from "./DeleteButton"; +import { Key } from "react"; +import { getLabel } from "../lib/string"; + +const stringifySort = ( + sort: SortState, + definitions: readonly SortDefinition[], +) => `${String(definitions[sort.index].field)}|${sort.direction}`; + +const renderSort = ( + sort: SortState, + definitions: readonly SortDefinition[], +) => ( + <> + {getLabel(definitions[sort.index], "none-upper")} ( + {formatSortDirection(sort.direction)}) + +); + +const stringifyFilter = ( + filter: ObjectFilterState, + definitions: readonly FilterDefinition[], +): Key => + createFilterStateReducer( + (scalar) => + `${getFilterOperatorLabel(scalar.operator)}|${JSON.stringify(scalar.value)}`, + (list, stringify) => + `${getFilterOperatorLabel(list.operator)}|${stringify(list.value)}`, + (object, context, stringify) => + `${String(context[object.index].field)}|${stringify(object.value)}`, + )(filter, definitions as FilterStateReducerContext); + +const renderFilter = ( + value: ObjectFilterState, + definitions: readonly FilterDefinition[], +) => + createFilterStateReducer( + (scalar) => ( + <> + {getFilterOperatorLabel(scalar.operator)}{" "} + {Array.isArray(scalar.value) + ? `{${scalar.value.join(", ")}}` + : scalar.value} + + ), + (list, render) => ( + <> + {getFilterOperatorLabel(list.operator)} {render(list.value)} + + ), + (object, context, render) => ( + <> + {getLabel(context[object.index], "none-upper")} {render(object.value)} + + ), + )(value, definitions as FilterStateReducerContext); + +export default function ActiveFilterAndSortBar({ + values, + filterDefinitions, + sortDefinitions, + onRemoveFilter, + onRemoveSort, + onRemoveAll, +}: { + values: { + filters: readonly ObjectFilterState[]; + sorts: readonly SortState[]; + }; + filterDefinitions: readonly FilterDefinition[]; + sortDefinitions: readonly SortDefinition[]; + onRemoveFilter: (index: number) => void; + onRemoveSort: (index: number) => void; + onRemoveAll: () => void; +}) { + return ( + + + {values.filters.length > 0 && ( + <> + Filtered by + {values.filters.map((filter, index: number) => ( + + Remove + + } + key={stringifyFilter(filter, filterDefinitions)} + onClose={() => onRemoveFilter(index)} + > + {renderFilter(filter, filterDefinitions)} + + ))} + + )} + {values.sorts.length > 0 && ( + <> + Sorted by + {values.sorts.map((sort, index: number) => ( + } + key={stringifySort(sort, sortDefinitions)} + onClose={() => onRemoveSort(index)} + > + {renderSort(sort, sortDefinitions)} + + ))} + + )} + + {(values.filters.length > 0 || values.sorts.length > 0) && ( + + Remove All + + )} + + ); +} diff --git a/frontend/components/CielabColorView.tsx b/frontend/components/CielabColorView.tsx new file mode 100644 index 00000000..8dc5fbc4 --- /dev/null +++ b/frontend/components/CielabColorView.tsx @@ -0,0 +1,19 @@ +import Copyable from "./Copyable"; +import { CielabColor } from "../__generated__/graphql"; +import Float, { Unit } from "./Float"; + +export default function CielabColorView({ value }: { value: CielabColor }) { + return ( + { + if (key === "__typename") return undefined; + return value; + })} + > + (L* , a*{" "} + , b*{" "} + ) + + ); +} diff --git a/frontend/components/CodeView.tsx b/frontend/components/CodeView.tsx new file mode 100644 index 00000000..fb828e3d --- /dev/null +++ b/frontend/components/CodeView.tsx @@ -0,0 +1,32 @@ +import Copyable from "./Copyable"; +import CopyableBlock from "./CopyableBlock"; + +export default function CodeView({ + code, + inline = false, +}: { + code: string; + inline?: boolean; +}) { + if (inline) { + return ( + + {code} + + ); + } else { + return ( + +
+          
+            {code}
+          
+        
+
+ ); + } +} diff --git a/frontend/components/CopyButton.tsx b/frontend/components/CopyButton.tsx new file mode 100644 index 00000000..952d734e --- /dev/null +++ b/frontend/components/CopyButton.tsx @@ -0,0 +1,85 @@ +import { Button, Tooltip } from "antd"; +import { CopyOutlined, CheckOutlined } from "@ant-design/icons"; +import { ReactNode, useState } from "react"; + +export default function CopyButton({ + getText, + type = "text", + size = "small", + copyIcon = , + onlyIcon = false, + color = undefined, + children, +}: { + getText: () => string; + type?: "text" | "default"; + size?: "small" | "medium" | "large"; + copyIcon?: ReactNode; + onlyIcon?: boolean; + color?: "white"; + children?: ReactNode; +}) { + const [copied, setCopied] = useState(false); + + return onlyIcon ? ( + + + ); +} diff --git a/frontend/components/Copyable.tsx b/frontend/components/Copyable.tsx new file mode 100644 index 00000000..29d321a1 --- /dev/null +++ b/frontend/components/Copyable.tsx @@ -0,0 +1,26 @@ +import { Space } from "antd"; +import { ReactNode } from "react"; +import CopyButton from "./CopyButton"; + +export default function Copyable({ + text, + onlyIcon, + color, + children, +}: { + text: string; + onlyIcon?: boolean; + color?: "white"; + children?: ReactNode; +}) { + const content = ( + <> + {children == null ? text : children} + text} onlyIcon={onlyIcon} color={color}> + Copy + + + ); + + return {onlyIcon ? content : {content}}; +} diff --git a/frontend/components/CopyableBlock.tsx b/frontend/components/CopyableBlock.tsx new file mode 100644 index 00000000..edc69062 --- /dev/null +++ b/frontend/components/CopyableBlock.tsx @@ -0,0 +1,53 @@ +import { Button } from "antd"; +import { CheckOutlined, CopyOutlined } from "@ant-design/icons"; +import { ReactNode, useState } from "react"; + +export default function CopyableBlock({ + text, + color, + children, +}: { + text: string; + color?: "white"; + children: ReactNode; +}) { + const [copied, setCopied] = useState(false); + + return ( +
+ {children} + +
+ ); +} diff --git a/frontend/components/DateTimeX.tsx b/frontend/components/DateTimeX.tsx new file mode 100644 index 00000000..fdd12a3e --- /dev/null +++ b/frontend/components/DateTimeX.tsx @@ -0,0 +1,34 @@ +import { Calendar, Popover } from "antd"; +import dayjs from "dayjs"; +import { Scalars } from "../__generated__/graphql"; + +interface DateTimeProps { + value: Scalars["DateTime"]["output"]; +} + +export default function DateTimeX({ value }: DateTimeProps) { + const parsedValue = dayjs(value); + + const calendarContent = ( +
+ null} + /> +
+ ); + + return ( + + + {parsedValue.format("YYYY-MM-DD HH:mm")} + + + ); +} diff --git a/frontend/components/DeleteButton.tsx b/frontend/components/DeleteButton.tsx new file mode 100644 index 00000000..d38c06d0 --- /dev/null +++ b/frontend/components/DeleteButton.tsx @@ -0,0 +1,63 @@ +import { Button, Tooltip } from "antd"; +import { DeleteOutlined, SyncOutlined } from "@ant-design/icons"; +import { capitalize } from "../lib/string"; +import { CSSProperties, forwardRef } from "react"; + +interface DeleteButtonProps { + kind?: "delete" | "remove"; + type?: "primary" | "text" | "default" | "icon"; + deleting?: boolean; + style?: CSSProperties; + onClick?: (e?: React.MouseEvent) => void; + children?: React.ReactNode; + // This allows Popconfirm to inject its internal event handlers + [key: string]: any; +} + +const DeleteButton = forwardRef( + ( + { + kind = "delete", + type = "primary", + deleting = false, + style, + onClick, + children, + ...rest + }, + ref, + ) => { + const label = children ?? capitalize(kind); + + const commonProps = { + ...rest, // contains Popconfirm's events + ref, // allows Popconfirm to measure position + danger: true, + loading: deleting, + style, + onClick, + }; + + switch (type) { + case "icon": + return ( + + + ); + } + }, +); + +export default DeleteButton; diff --git a/frontend/components/EditButton.tsx b/frontend/components/EditButton.tsx new file mode 100644 index 00000000..21a085b8 --- /dev/null +++ b/frontend/components/EditButton.tsx @@ -0,0 +1,34 @@ +import { EditOutlined } from "@ant-design/icons"; +import { Button, ButtonProps, Tooltip } from "antd"; + +interface EditButtonProps extends Omit< + ButtonProps, + "type" | "icon" | "shape" | "children" +> { + type?: "text" | "default" | "icon"; +} + +export default function EditButton({ + type = "default", + ...rest +}: EditButtonProps) { + switch (type) { + case "icon": + return ( + + + ); + } +} diff --git a/frontend/components/EnumSelect.tsx b/frontend/components/EnumSelect.tsx new file mode 100644 index 00000000..6c6476c7 --- /dev/null +++ b/frontend/components/EnumSelect.tsx @@ -0,0 +1,52 @@ +import { Select, SelectProps } from "antd"; +import { humanize } from "../lib/string"; + +export const allEnumValues = ( + enumObject: Record, +) => + Object.entries(enumObject) + .filter(([key]) => isNaN(Number(key))) + .map(([_, value]) => value); + +export const allEnumSelectOptions = ( + values: T[], + filter?: (value: T) => boolean, +) => + values + // exclude reverse mappings + .filter((value) => filter?.(value) ?? true) + .map((value) => ({ + label: humanize(String(value), "all-upper"), + value: value, + })); + +interface BaseProps extends Omit< + SelectProps, + "options" +> {} + +type EnumSelectProps = + | (BaseProps & { + enumObject: Record; + filter?: (value: T) => boolean; + }) + | (BaseProps & { + values: T[]; + }); + +export default function EnumSelect( + props: EnumSelectProps, +) { + if ("enumObject" in props) { + const { enumObject, filter, ...rest } = props; + return ( + + {...rest} + options={allEnumSelectOptions(allEnumValues(enumObject))} + /> + ); + } else { + const { values, ...rest } = props; + return {...rest} options={allEnumSelectOptions(values)} />; + } +} diff --git a/frontend/components/EnumTag.tsx b/frontend/components/EnumTag.tsx new file mode 100644 index 00000000..5cb07837 --- /dev/null +++ b/frontend/components/EnumTag.tsx @@ -0,0 +1,16 @@ +import { Tag, TagProps } from "antd"; +import { humanize } from "../lib/string"; + +export default function EnumTag(props: TagProps) { + return ( + + {props.children && humanize(props.children.toString(), "none-upper")} + + ); +} diff --git a/frontend/components/Float.tsx b/frontend/components/Float.tsx new file mode 100644 index 00000000..5b11ac08 --- /dev/null +++ b/frontend/components/Float.tsx @@ -0,0 +1,63 @@ +import Copyable from "./Copyable"; +import { Scalars } from "../__generated__/graphql"; +import { Tooltip } from "antd"; + +export enum Unit { + UNITLESS, + METER, + WATT_PER_SQUARE_METER_KELVIN, +} + +function formatSmartCut(value: number) { + const string = value.toString(); + const dotIndex = string.indexOf("."); + if (dotIndex === -1) return string; + const firstNonZero = string.search(/[1-9]/); + const significantCut = firstNonZero + 3; + const minDecimalCut = dotIndex + 4; + const finalCutIndex = Math.max(significantCut, minDecimalCut); + if (string.length > finalCutIndex) { + return string.substring(0, finalCutIndex) + "…"; + } + return string; +} + +const unitLabels = { + [Unit.METER]: "m", + [Unit.WATT_PER_SQUARE_METER_KELVIN]: ( + <> + W·m-2·K-1 + {/* W/(m2·K) */} + + ), +}; + +export default function Float({ + value, + unit, +}: { + value: Scalars["Float"]["output"]; + unit: Unit; +}) { + return ( + + {value} + + } + styles={{ + container: { + whiteSpace: "nowrap", + minWidth: "max-content", + maxWidth: "none", + }, + }} + > + + {formatSmartCut(value)} + {unit != Unit.UNITLESS && <> {unitLabels[unit]}} + + + ); +} diff --git a/frontend/components/Footer.tsx b/frontend/components/Footer.tsx index 5544d81c..8505147a 100644 --- a/frontend/components/Footer.tsx +++ b/frontend/components/Footer.tsx @@ -1,16 +1,13 @@ -import { Typography } from "antd"; +import { Space, Typography } from "antd"; import paths from "../paths"; -export type FooterProps = {}; - export default function Footer() { return ( - <> - Legal Notice{" "} - ·{" "} + + Legal Notice Data Protection Information - + ); } diff --git a/frontend/components/GnuPgKeyLink.tsx b/frontend/components/GnuPgKeyLink.tsx new file mode 100644 index 00000000..0b98e78d --- /dev/null +++ b/frontend/components/GnuPgKeyLink.tsx @@ -0,0 +1,24 @@ +import Link from "next/link"; +import Copyable from "./Copyable"; +import paths from "../paths"; +import CopyableBlock from "./CopyableBlock"; + +export default function GnuPgKeyLink({ + fingerprint, + block = false, +}: { + fingerprint: string; + block?: boolean; +}) { + const link = ( + + {fingerprint} + + ); + + return block ? ( + {link} + ) : ( + {link} + ); +} diff --git a/frontend/components/Highlight.tsx b/frontend/components/Highlight.tsx deleted file mode 100644 index d75e078e..00000000 --- a/frontend/components/Highlight.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { forwardRef } from "react"; -import Highlighter from "react-highlight-words"; - -export type HighlightProps = { - text: string | null | undefined; - snippet: string | null | undefined; -}; - -export const Highlight = forwardRef( - ({ text, snippet }, ref) => ( - - ), -); diff --git a/frontend/components/Iconize.tsx b/frontend/components/Iconize.tsx new file mode 100644 index 00000000..11e394fc --- /dev/null +++ b/frontend/components/Iconize.tsx @@ -0,0 +1,16 @@ +import { Space } from "antd"; + +export const Iconize = ({ + icon, + children, +}: { + icon: React.ReactNode; + children: React.ReactNode; +}) => { + return ( + + {icon} + {children} + + ); +}; diff --git a/frontend/components/Id.tsx b/frontend/components/Id.tsx new file mode 100644 index 00000000..452ac3ff --- /dev/null +++ b/frontend/components/Id.tsx @@ -0,0 +1,5 @@ +import { Scalars } from "../__generated__/graphql"; + +export default function Id({ value }: { value: Scalars["Uuid"]["output"] }) { + return {value}; +} diff --git a/frontend/components/IdentifierItem.tsx b/frontend/components/IdentifierItem.tsx new file mode 100644 index 00000000..526d8a8b --- /dev/null +++ b/frontend/components/IdentifierItem.tsx @@ -0,0 +1,95 @@ +import { Typography } from "antd"; +import Icon, { BarcodeOutlined, GlobalOutlined } from "@ant-design/icons"; +import { CustomIconComponentProps } from "@ant-design/icons/lib/components/Icon"; +import { Iconize } from "./Iconize"; +import { addPrefix, removePrefix } from "../lib/string"; +import CopyButton from "./CopyButton"; + +const ArXivSvg = () => ( + + + +); + +const DoiSvg = () => ( + + + +); + +const ArXivIcon = (props: Partial) => ( + +); +const DoiIcon = (props: Partial) => ( + +); + +const IDENTIFIER_CONFIG = { + arXiv: { + label: null, + prefix: "arXiv:", + icon: , + url: ({ valueWithoutPrefix }: { valueWithoutPrefix: string }) => + `https://arxiv.org/abs/${valueWithoutPrefix}`, + }, + doi: { + label: null, + prefix: "doi:", + icon: , + url: ({ valueWithoutPrefix }: { valueWithoutPrefix: string }) => + `https://doi.org/${valueWithoutPrefix}`, + }, + urn: { + label: null, + prefix: "urn:", + icon: , + url: ({ + valueWithPrefix, + valueWithoutPrefix, + }: { + valueWithPrefix: string; + valueWithoutPrefix: string; + }) => { + if (valueWithPrefix.startsWith("urn:isbn:")) { + return `https://isbnsearch.org/isbn/${removePrefix(valueWithoutPrefix, "isbn:")}`; + } + if (valueWithPrefix.startsWith("urn:issn:")) { + return `https://urn.issn.org/${valueWithPrefix}`; + } + if (valueWithPrefix.startsWith("urn:nbn:")) { + return `https://nbn-resolving.org/${valueWithPrefix}`; + } + return `https://www.ecosia.org/search?q=${valueWithPrefix}`; + }, + }, + webAddress: { + label: "Web", + prefix: "https://", + icon: , + url: ({ value }: { value: string }) => value, + }, +}; + +export default function IdentifierItem({ + type, + value, +}: { + type: keyof typeof IDENTIFIER_CONFIG; + value: string; +}) { + const config = IDENTIFIER_CONFIG[type]; + const valueWithoutPrefix = removePrefix(value, config.prefix); + const valueWithPrefix = addPrefix(value, config.prefix); + const href = config.url({ value, valueWithPrefix, valueWithoutPrefix }); + + return ( + + + + {config.label ? config.label : valueWithPrefix} + + + value} /> + + ); +} diff --git a/frontend/components/InlineList.tsx b/frontend/components/InlineList.tsx new file mode 100644 index 00000000..e6542433 --- /dev/null +++ b/frontend/components/InlineList.tsx @@ -0,0 +1,35 @@ +import { Fragment } from "react/jsx-runtime"; + +export default function InlineList({ + items, + renderItem, +}: { + items: readonly TItem[]; + renderItem: (item: TItem, index: number) => React.ReactNode; +}) { + return ( + <> + {/* */} + + {items.map((item, index) => { + const isLast = index === items.length - 1; + const isSecondToLast = index === items.length - 2; + const isOnlyTwo = items.length === 2; + return ( + + {renderItem(item, index)} + {!isLast && + (isSecondToLast ? (isOnlyTwo ? " and " : ", and ") : ", ")} + + ); + })} + + + ); +} diff --git a/frontend/components/JsonView.tsx b/frontend/components/JsonView.tsx new file mode 100644 index 00000000..dc53cfb3 --- /dev/null +++ b/frontend/components/JsonView.tsx @@ -0,0 +1,36 @@ +import { Tooltip } from "antd"; +import CopyableBlock from "./CopyableBlock"; + +export default function JsonView({ + data, + inline = false, + color, +}: { + data: object; + inline?: boolean; + color?: "white"; +}) { + if (inline) { + const jsonString = JSON.stringify(data); + return ( + }> + {jsonString} + + ); + } else { + const jsonString = JSON.stringify(data, null, 2); + return ( + +
+          
+            {jsonString}
+          
+        
+
+ ); + } +} diff --git a/frontend/components/JumpToId.tsx b/frontend/components/JumpToId.tsx new file mode 100644 index 00000000..5356e671 --- /dev/null +++ b/frontend/components/JumpToId.tsx @@ -0,0 +1,47 @@ +import { CSSProperties, useState } from "react"; +import { Button, Input, Space } from "antd"; +import { useRouter } from "next/router"; +import { Scalars } from "../__generated__/graphql"; +import { Route } from "next"; +import PaginatedIdSelect, { PaginatedSelectProps } from "./PaginatedIdSelect"; +import { isUuid } from "../lib/string"; + +export type JumpToIdProps = { + query?: PaginatedSelectProps["query"]; + route: (id: Scalars["Uuid"]["output"]) => Route; + style?: CSSProperties; +}; + +export default function JumpToId({ query, route, style }: JumpToIdProps) { + const router = useRouter(); + const [id, setId] = useState(undefined); + + const handleJump = () => { + if (id && isUuid(id)) { + router.push(route(id)); + } + }; + + return ( + + {/* 36 characters is what a UUID of the form "ffffffff-ffff-ffff-ffff-ffffffffffff" has */} + {query ? ( + + ) : ( + setId(e.target.value)} + /> + )} + + + ); +} diff --git a/frontend/components/Layout.tsx b/frontend/components/Layout.tsx index 71d68fad..3ffccb4a 100644 --- a/frontend/components/Layout.tsx +++ b/frontend/components/Layout.tsx @@ -2,7 +2,7 @@ import Head from "next/head"; import { ReactNode, useEffect } from "react"; import Footer from "./Footer"; import NavBar from "./NavBar"; -import { App, Layout as AntLayout, Typography } from "antd"; +import { App, Layout as AntLayout, Typography, Flex, Divider } from "antd"; import paths from "../paths"; import { useCookies } from "react-cookie"; @@ -12,27 +12,27 @@ const navItems = [ label: "Home", }, { - path: paths.calorimetricData, + path: paths.allCalorimetricData, label: "Calorimetric Data", }, { - path: paths.geometricData, + path: paths.allGeometricData, label: "Geometric Data", }, { - path: paths.hygrothermalData, + path: paths.allHygrothermalData, label: "Hygrothermal Data", }, { - path: paths.lifeCycleData, + path: paths.allLifeCycleData, label: "Life-Cycle Data", }, { - path: paths.opticalData, + path: paths.allOpticalData, label: "Optical Data", }, { - path: paths.photovoltaicData, + path: paths.allPhotovoltaicData, label: "Photovoltaic Data", }, { @@ -42,13 +42,14 @@ const navItems = [ ]; export type LayoutProps = { + pageTitles?: string[]; children?: ReactNode; }; const cookieConsentName = "consent"; const cookieConsentValue = "yes"; -export default function Layout({ children }: LayoutProps) { +export default function Layout({ pageTitles = [], children }: LayoutProps) { const appTitle = "TestLab Solar Façades"; const [cookies, setCookie] = useCookies([cookieConsentName]); @@ -61,7 +62,7 @@ export default function Layout({ children }: LayoutProps) { modal.info({ title: "Cookie Consent", content: ( - + This website employs cookies to make it work securely. As these cookies are essential you need to agree to their usage to use this website. @@ -73,23 +74,41 @@ export default function Layout({ children }: LayoutProps) { }, }); } - }, [shouldShowCookieConsent, setCookie]); + }, [shouldShowCookieConsent, setCookie, modal]); return ( - {appTitle} + {[...pageTitles, appTitle].join(" • ")} - + + + - - {children} + + +
{children}
+
-