diff --git a/.env.development.sample b/.env.development.sample index 088320e61..b41b53694 100644 --- a/.env.development.sample +++ b/.env.development.sample @@ -19,8 +19,14 @@ TESTLAB_SOLAR_FACADES_OPEN_ID_CONNECT_CLIENT_SECRET=secret IGSDB_API_TOKEN=secret IGSDB_OPEN_ID_CONNECT_CLIENT_SECRET=secret +EPEA_USER_ID=christoph.maurer@ise.fraunhofer.de +EPEA_API_KEY=secret +EPEA_OPEN_ID_CONNECT_CLIENT_SECRET=secret + OPEN_TELEMETRY_HOST=host.docker.internal OPEN_TELEMETRY_GRPC_PORT=4017 +EMAIL_ADDRESS=christoph.maurer@ise.fraunhofer.de + SMTP_HOST=email SMTP_PORT=25 diff --git a/.env.production.sample b/.env.production.sample index 0fbc3b58e..c2e30f140 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -18,10 +18,16 @@ TESTLAB_SOLAR_FACADES_OPEN_ID_CONNECT_CLIENT_SECRET= IGSDB_API_TOKEN= IGSDB_OPEN_ID_CONNECT_CLIENT_SECRET= +EPEA_USER_ID=christoph.maurer@ise.fraunhofer.de +EPEA_API_KEY= +EPEA_OPEN_ID_CONNECT_CLIENT_SECRET= + OPEN_TELEMETRY_HOST=host.docker.internal OPEN_TELEMETRY_GRPC_PORT=4317 -SMTP_HOST=mail-mtaKA26.fraunhofer.de +EMAIL_ADDRESS=christoph.maurer@ise.fraunhofer.de + +SMTP_HOST=mta01.mx.fraunhofer.de SMTP_PORT=25 RELAY_SMTP_HOST= RELAY_SMTP_PORT=25 diff --git a/.env.staging.sample b/.env.staging.sample index f732c1e61..cfb07d415 100644 --- a/.env.staging.sample +++ b/.env.staging.sample @@ -18,11 +18,17 @@ TESTLAB_SOLAR_FACADES_OPEN_ID_CONNECT_CLIENT_SECRET= IGSDB_API_TOKEN= IGSDB_OPEN_ID_CONNECT_CLIENT_SECRET= +EPEA_USER_ID=christoph.maurer@ise.fraunhofer.de +EPEA_API_KEY= +EPEA_OPEN_ID_CONNECT_CLIENT_SECRET= + OPEN_TELEMETRY_HOST=host.docker.internal OPEN_TELEMETRY_GRPC_PORT=4317 +EMAIL_ADDRESS=christoph.maurer@ise.fraunhofer.de + SMTP_HOST=email SMTP_PORT=25 -RELAY_SMTP_HOST=mail-mtaKA26.fraunhofer.de +RELAY_SMTP_HOST=mta01.mx.fraunhofer.de RELAY_SMTP_PORT=25 RELAY_ALLOWED_EMAILS= diff --git a/CHANGELOG.md b/CHANGELOG.md index 13a1503c2..1a2b3fcd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -119,6 +119,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - - +## [v1.2.0] - 2026-06-30 + +### Changed + +- **BREAKING**: Use `String` for cursors and `Int` for `first` and `last` parameters to conform to the Relay connection specification, simplify GraphQL filter input type names, and correct plural names ending before in `ys` and now in `ies` [#274](https://github.com/building-envelope-data/metabase/pull/274) + ## [v1.1.1] - 2026-04-13 ### Added @@ -154,10 +160,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [v1.0.0] - 2022-02-28 -[Unreleased]: https://github.com/building-envelope-data/metabase/compare/v1.1.1...HEAD - +[Unreleased]: https://github.com/building-envelope-data/metabase/compare/v1.2.0...HEAD +[v1.2.0]: https://github.com/building-envelope-data/metabase/compare/v1.1.1...v1.2.0 [v1.1.1]: https://github.com/building-envelope-data/metabase/compare/v1.1.0...v1.1.1 - [v1.1.0]: https://github.com/building-envelope-data/metabase/compare/v1.0.0...v1.1.0 - [v1.0.0]: https://github.com/building-envelope-data/metabase/compare/5e4c6579bc7b596502c0bc9318cf24435bed2c80...v1.0.0 diff --git a/README.md b/README.md index b0853e348..7f0948b61 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ When doing so, please adhere to our - OpenAPI reference at `https://www.local.buildingenvelopedata.org:${HTTPS_PORT}/openapi/docs/`, - dummy email server at `https://www.local.buildingenvelopedata.org:${HTTPS_PORT}/email/` (to view for example the confirmation email sent during registration), - - OpenId Connect configuration at + - OpenID Connect configuration at `https://www.local.buildingenvelopedata.org:${HTTPS_PORT}/.well-known/openid-configuration`, - telemetry web frontend at `https://telemetry.local.buildingenvelopedata.org:${HTTPS_PORT}`, - staging web frontend at `https://staging.local.buildingenvelopedata.org:${HTTPS_PORT}`, @@ -275,17 +275,23 @@ and the pages following it. some information on what the variables mean - `NAME` is the name Docker project name, in particular, it is the prefix of the Docker container names listed by `docker ps --all`; + - `ENVIRONMENT` is either `staging` or `production`; + - `TARGET` is the deployed tag or commit. It is set later by running + `./deploy.mk do TARGET=${TAG}`. The corresponding Docker images named + `${NAME}-backend:${TARGET}` and `${NAME}-frontend:${TARGET}` are built + on a build or development machine and pushed to the server later with + GNU Make targets from `./forge.mk`; - `HOST` is the domain name with sub-domain of the deployment, in - particular, it is used by the OpenId Connect provider and to make + particular, it is used by the OpenID Connect provider and to make URLs absolute in emails sent for example when a user registers; - `HTTP_PORT` is the HTTP port on which the reverse proxy NGINX listens for requests; - `BOOTSTRAP_USER_PASSWORD` is the password used by the database seeder for the administrator account if there is none yet; - - `OPEN_ID_CONNECT_CLIENT_SECRET` is the OpenId Connect client secret of + - `OPEN_ID_CONNECT_CLIENT_SECRET` is the OpenID Connect client secret of the metabase as a client of itself as identity provider; - `TESTLAB_SOLAR_FACADES_HOST` is the host with sub-domain of the TestLab - Solar Façades used by the database seeder to add it as an OpenId + Solar Façades used by the database seeder to add it as an OpenID Connect client; - `TESTLAB_SOLAR_FACADES_OPEN_ID_CONNECT_CLIENT_SECRET` is the corresponding client secret; @@ -302,7 +308,17 @@ and the pages following it. - `ln --symbolic ./docker.mk ./Makefile` and - `ln --symbolic ./docker-compose.production.yaml ./docker-compose.yaml`. - 1. Create the PostgreSQL database by running `./database.mk create`. + 1. Switch to the Git branch, tag, or commit you want to deploy by either running + `git switch ${BRANCH}`, + `git switch release/${TAG}`, or + `git switch --detach ${COMMIT_HASH}`, + where, for example, `${BRANCH}` is `develop` or `${TAG}` is `v1.0.0` or + `${COMMIT_HASH}` is `5e14d7d0858f26c00c82ab9c248cd750606a24b6`. + + 1. Create the PostgreSQL database and schema by running + `./database.mk create migrate`. + + 1. Build and start all services by running `make build up`. ### Creating a release @@ -318,7 +334,7 @@ and the pages following it. a new release" on [Releases](https://github.com/building-envelope-data/metabase/releases). 1. Fetch the release branch by running `git fetch` and switch to it by running - `git switch --detach release/v*.*.*`, where `*.*.*` is the version. + `git switch release/v*.*.*`, where `*.*.*` is the version. 1. If the databases have not diverged, then apply pending migrations with `./database.mk migrate`. Otherwise, recreate the database by running `./database.mk drop create migrate`. @@ -330,11 +346,11 @@ and the pages following it. ### Deploying a release 1. Fetch the release branch by running `git fetch` and switch to it by running - `git switch --detach release/${TAG}`, where `${TAG}` is the release tag to + `git switch release/${TAG}`, where `${TAG}` is the release tag to be deployed, for example, `v1.0.0`. 1. Build and push the Docker images for the services `frontend` and `backend` - services by running `./forge.mk all USER=cloud HOST=${IP}`, where `${IP}` is - the server's IP address. + by running `./forge.mk all USER=cloud HOST=${IP}`, where `${IP}` is the + server's IP address. 1. Enter a shell on the production machine using `ssh`. 1. Navigate into `/app/production` by running `cd /app/production`. 1. Back up the production database by running @@ -347,34 +363,57 @@ and the pages following it. 1. Deploy the new release in the staging environment by running `./deploy.mk do TARGET=${TAG}`, where `${TAG}` is the release tag to be deployed, for example, `v1.0.0`. -1. If it fails _after_ the database backup was made, rollback to the previous - state by running - `./deploy.mk rollback`, - figure out what went wrong, apply the necessary fixes to the codebase, - create a new release, and try to deploy that release instead. +1. If it fails rollback to the previous state by running + `./deploy.mk rollback`, figure out what went wrong, apply the necessary + fixes to the codebase, create a new release, and try to deploy that release + instead. 1. If it succeeds, deploy the new reverse proxy that handles sub-domains by running `cd /app/machine && ./deploy.mk do` and test whether everything works - as expected and if that is the case, continue. Note that in the - staging environment sent emails can be viewed in the web browser under - `https://staging.buildingenvelopedata.org/email/` and emails to addresses in - the variable `RELAY_ALLOWED_EMAILS` in `./.env` are delivered to the - respective inboxes (the variable's value is a comma separated list of email - addresses). Note that in order for OpenId Connect to work as expected in - staging, make sure that the redirect URIs use the sub-domain `staging` - (instead of `www`) by entering `psql` with `./database.mk psql`, examining - the output of the SQL statement - `select * from metabase."OpenIddictApplications";` - and if necessary executing SQL statements along the lines - `update metabase."OpenIddictApplications" set "RedirectUris"='["https://staging.buildingenvelopedata.org/connect/callback/login/metabase"]', "PostLogoutRedirectUris"='["https://staging.buildingenvelopedata.org/connect/callback/logout/metabase"]' where "ClientId"='metabase';` - and - `update metabase."OpenIddictApplications" set "RedirectUris"='["https://staging.solarbuildingenvelopes.com/connect/callback/login/metabase"]', "PostLogoutRedirectUris"='["https://staging.solarbuildingenvelopes.com/connect/callback/logout/metabase"]' where "ClientId"='testlab-solar-facades';` - And if you want the staging environment to forward queries to the staging - environments of product-data databases, then examine the output of the SQL - statement `select * from metabase.database;` and if necessary execute SQL - statements along the lines - `update metabase.database set "Locator"='https://staging.solarbuildingenvelopes.com/graphql/' where "Locator"='https://www.solarbuildingenvelopes.com/graphql/';` - and - `update metabase.database set "Locator"='https://igsdb-v2-staging.herokuapp.com/graphql/' where "Locator"='https://igsdb-v2.herokuapp.com/graphql/';` + as expected and if that is the case, continue. + - In the staging environment sent emails can be viewed in the web browser + under `https://staging.buildingenvelopedata.org/email/` and emails to + addresses in the variable `RELAY_ALLOWED_EMAILS` in `./.env` are delivered + to the respective inboxes (the variable's value is a comma separated list of + email addresses). + + - If you want the staging environment to forward queries to the staging + environments of product-data databases, then examine the output of the SQL + statement `select * from metabase.database;` and if necessary execute SQL + statements along the lines + + ``` + update metabase.database set "Locator" = 'https://staging.solarbuildingenvelopes.com/graphql/' where "Locator" = 'https://www.solarbuildingenvelopes.com/graphql/'; + update metabase.database set "Locator" = 'https://igsdb-v2-staging.herokuapp.com/graphql/' where "Locator" = 'https://igsdb-v2.herokuapp.com/graphql/'; + ``` + + - In order for OpenID Connect to work in staging, + - replace `OPEN_ID_CONNECT_CLIENT_SECRET` in `/app/staging/.env` by the + respective value in `/app/production/.env` and run `make up` to recreate + Docker compose services which depend on that value. If you have a + staging deployment of a + [product-data database](https://github.com/building-envelope-data/database) + that communicates with the staging deployment of the metabase, then + replace `OPEN_ID_CONNECT_CLIENT_SECRET` there also. Both changes are + necessary to make the OpenID Connect clients in metabase and product-data + database use the client secret whose hash value is stored in the metabase; + + - make sure that the redirect URIs and resource permissions use the + sub-domain `staging` (instead of `www`) by entering `psql` with + `./database.mk psql`, examining the output of the SQL statement + `select * from metabase."OpenIddictApplications";` + and, if necessary, executing SQL statements along the lines + + ``` + update metabase."OpenIddictApplications" set "RedirectUris" = '["https://staging.buildingenvelopedata.org/connect/callback/login/metabase"]', "PostLogoutRedirectUris" = '["https://staging.buildingenvelopedata.org/connect/callback/logout/metabase"]' where "ClientId" = 'metabase'; + update metabase."OpenIddictApplications" set "RedirectUris" = '["https://staging.solarbuildingenvelopes.com/connect/callback/login/metabase"]', "PostLogoutRedirectUris" = '["https://staging.solarbuildingenvelopes.com/connect/callback/logout/metabase"]' where "ClientId" = 'testlab-solar-facades'; + ``` + + and + + ``` + update metabase."OpenIddictApplications" set "Permissions" = ("Permissions"::jsonb - 'rsrc:https://www.buildingenvelopedata.org/graphql/' || '"rsrc:https://staging.buildingenvelopedata.org/graphql/"'::jsonb)::text where "ClientId" = 'metabase'; + ``` + 1. Change to the production environment by running `cd /app/production`. 1. Adapt the environment file `./.env` if necessary by comparing it with the `./.env.production.sample` file of the release to be deployed. @@ -489,7 +528,7 @@ calculations. The access right management of the product data network is based on the framework [OpenID Connect](https://openid.net/developers/how-connect-works/). The general idea is that users and applications can authenticate at the -metabase (OpenId Connect Provider) and receive an access token (security +metabase (OpenID Connect Provider) and receive an access token (security credentials). When an application sends queries and mutations to product data servers, it can attach the token. The product data server receives the token and determines the access rights accordingly (authorization) using information @@ -540,11 +579,11 @@ data server, please to your institution. For `${UUID_OF_YOUR_INSTITUTION}` please use the UUID which your institution has received when it was created. -1. equip your product data server with an OpenId Connect Client partly configuring +1. equip your product data server with an OpenID Connect Client partly configuring it via OpenID Connect Discovery using the [Well-Known Configuration Endpoint](https://www.buildingenvelopedata.org/.well-known/openid-configuration). -When adding an OpenId Connect Application, you need to make various decisions: +When adding an OpenID Connect Application, you need to make various decisions: [Which OAuth 2.0 Flow Should I Use?](https://auth0.com/docs/get-started/authentication-and-authorization-flow/which-oauth-2-0-flow-should-i-use) We support the [Authorization Code Flow with Pushed Authorization Requests diff --git a/backend/Directory.Build.props b/backend/Directory.Build.props index 7538c469d..1de6f3cae 100644 --- a/backend/Directory.Build.props +++ b/backend/Directory.Build.props @@ -13,6 +13,8 @@ true latest-Recommended true + + true diff --git a/backend/Dockerfile.development b/backend/Dockerfile.development index f0c306079..1f0628a7f 100644 --- a/backend/Dockerfile.development +++ b/backend/Dockerfile.development @@ -1,4 +1,4 @@ -# syntax=docker/dockerfile:1.21 +# syntax=docker/dockerfile:1.23 # check=error=true;skip=SecretsUsedInArgOrEnv # Available versions are listed on https://hub.docker.com/r/docker/dockerfile @@ -144,6 +144,28 @@ RUN \ --force \ /var/lib/apt/lists/* +# for available versions see the tags on https://github.com/stripe/pg-schema-diff +ENV PG_SCHEMA_DIFF_VERSION=v1.0.5 +RUN \ + apt-get update && \ + apt-get install \ + --assume-yes \ + --no-install-recommends \ + golang-go && \ + go install \ + github.com/stripe/pg-schema-diff/cmd/pg-schema-diff@${PG_SCHEMA_DIFF_VERSION} && \ + cp /root/go/bin/pg-schema-diff /usr/local/bin/ && \ + rm --recursive --force \ + /root/go \ + /root/.cache && \ + apt-get purge \ + --assume-yes \ + golang-go && \ + rm \ + --recursive \ + --force \ + /var/lib/apt/lists/* + ENV HOME=/home/me RUN mkdir --parents ${HOME}/app \ && chown \ diff --git a/backend/Dockerfile.production b/backend/Dockerfile.production index 1eb05b61e..787aede3e 100644 --- a/backend/Dockerfile.production +++ b/backend/Dockerfile.production @@ -1,4 +1,4 @@ -# syntax=docker/dockerfile:1.21 +# syntax=docker/dockerfile:1.23 # check=error=true # Available versions are listed on https://hub.docker.com/r/docker/dockerfile diff --git a/backend/Makefile b/backend/Makefile index c5bf26845..88a177e4f 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -7,7 +7,7 @@ MAKEFLAGS += --warn-undefined-variables # Taken from https://www.client9.com/self-documenting-makefiles/ help : ## Print this help - @awk -F ':|##' '/^[^\t].+?:.*?##/ {\ + @awk -F ':.*?## ' '/^[^\t].+?:.*?##/ {\ printf "\033[36m%-30s\033[0m %s\n", $$1, $$NF \ }' $(MAKEFILE_LIST) .PHONY : help @@ -22,7 +22,7 @@ help : ## Print this help # See https://learn.microsoft.com/en-us/dotnet/core/testing/microsoft-testing-platform-run-and-debug?tabs=dotnetcli list : ARGS = -list : ## List tests, for example, `make list-tests ARGS='--filter "FullyQualifiedName~LoggedInUser_CreatesComponent"'` +list : ## List tests, for example, `make list ARGS='--filter "FullyQualifiedName~LoggedInUser_CreatesComponent"'` dotnet run \ --project ./test/*.Tests.csproj \ --list-tests \ @@ -88,7 +88,7 @@ analyze : ## Analyze with Roslynator # https://www.jetbrains.com/help/resharper/ReSharper_Command_Line_Tools.html#install-and-use-resharper-command-line-tools-as-net-core-tools inspect : ## Inspect with ReSharper, for example, `make inspect OUTPUT=./inspection.xml` - dotnet tool run jb inspectcode --build ./*.sln -o ${OUTPUT} + dotnet tool run jb inspectcode --build --output "${OUTPUT}" ./*.sln .PHONY : inspect editorconfig : ## Debug editorconfig @@ -171,13 +171,9 @@ dedup : ## Dedeuplicate code lines matching the pattern `${PATTERN}`, for exampl }" .PHONY : dedup -# See https://github.com/hasura/graphqurl#export-schema -schema : ## Fetch GraphQL schema using `hasura/graphqurl` - gq \ - http://backend:8080/graphql/ \ - --introspect \ - > ./schema.graphql -.PHONY : schema +check-database : ## Check whether there have been any model changes made since the last migration was added and whether the C# domain model matches the PostgreSQL database schema + $(MAKE) test ARGS='--filter "FullyQualifiedName~EnsureMigrationsAreUpToDate|FullyQualifiedName~EnsureDomainModelMatchesDatabaseSchema"' +.PHONY : check-database has-pending-model-changes : ## Check whether there have been any model changes made since the last migration was added dotnet ef \ diff --git a/backend/dotnet-tools.json b/backend/dotnet-tools.json index 6761c3045..1cf679f35 100644 --- a/backend/dotnet-tools.json +++ b/backend/dotnet-tools.json @@ -24,14 +24,14 @@ "rollForward": false }, "dotnet-ef": { - "version": "10.0.5", + "version": "10.0.9", "commands": [ "dotnet-ef" ], "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": "2025.3.3", + "version": "2026.1.3", "commands": [ "jb" ], @@ -87,7 +87,7 @@ "rollForward": false }, "dotnet-outdated-tool": { - "version": "4.7.1", + "version": "4.8.1", "commands": [ "dotnet-outdated" ], diff --git a/backend/src/AppSettings.cs b/backend/src/AppSettings.cs index ad018b9c1..920878b25 100644 --- a/backend/src/AppSettings.cs +++ b/backend/src/AppSettings.cs @@ -1,6 +1,7 @@ // Inspired by https://weblog.west-wind.com/posts/2017/dec/12/easy-configuration-binding-in-aspnet-core-revisited using System; +using Npgsql; namespace Metabase; @@ -11,12 +12,16 @@ public sealed record AppSettings public string Host { get; init; } = ""; public string Subdomain { private get; init; } = ""; public Uri Uri => new($"https://{Subdomain}.{Host}", UriKind.Absolute); + public Uri NonSubdomainUri => new Uri($"https://{Host}", UriKind.Absolute); public Uri GraphQlEndpoint => new UriBuilder(Uri) { Path = GraphQlPathSegment }.Uri; public string BootstrapUserPassword { get; init; } = ""; public string OpenIdConnectClientSecret { get; init; } = ""; + public string EmailAddress { get; init; } = ""; + public TestlabSolarFacadesSettings TestlabSolarFacades { get; init; } = new(); public IgsdbSettings Igsdb { get; init; } = new(); + public EpeaSettings Epea { get; init; } = new(); public LoggingSettings Logging { get; init; } = new(); public EmailSettings Email { get; init; } = new(); public DatabaseSettings Database { get; init; } = new(); @@ -35,6 +40,13 @@ public sealed record IgsdbSettings public string ApiToken { get; init; } = ""; }; + public sealed record EpeaSettings + { + public string OpenIdConnectClientSecret { get; init; } = ""; + public string UserId { get; init; } = ""; + public string ApiKey { get; init; } = ""; + }; + public sealed record LoggingSettings { public bool EnableSensitiveDataLogging { get; init; } @@ -54,7 +66,19 @@ public sealed record DatabaseSettings public string UserName { get; init; } = ""; public string Password { get; init; } = ""; public string SchemaName { get; init; } = ""; - }; + + public string ConnectionString(string? customDatabaseName = null) + { + var connectionStringBuilder = new NpgsqlConnectionStringBuilder(); + connectionStringBuilder.Host = Host; + connectionStringBuilder.Port = Port; + connectionStringBuilder.Database = customDatabaseName ?? Name; + connectionStringBuilder.Username = UserName; + connectionStringBuilder.Password = Password; + connectionStringBuilder.MaxPoolSize = 90; + return connectionStringBuilder.ConnectionString; + } + } public sealed record OpenTelemetrySettings { diff --git a/backend/src/Authentication/AuthenticationConstants.cs b/backend/src/Authentication/AuthenticationConstants.cs index 64e85fa6a..6f956803e 100644 --- a/backend/src/Authentication/AuthenticationConstants.cs +++ b/backend/src/Authentication/AuthenticationConstants.cs @@ -1,5 +1,3 @@ -using System.Collections.Immutable; - namespace Metabase.Authentication; internal static class AuthenticationConstants diff --git a/backend/src/Authentication/AuthenticationHandler.cs b/backend/src/Authentication/AuthenticationHandler.cs index 88924d810..c4ee8d384 100644 --- a/backend/src/Authentication/AuthenticationHandler.cs +++ b/backend/src/Authentication/AuthenticationHandler.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; +using NodaTime; using OpenIddict.Abstractions; using OpenIddict.Client; using OpenIddict.Client.AspNetCore; @@ -54,6 +55,7 @@ string userId } public sealed class AuthenticationHandler( + IClock clock, UserManager userManager, OpenIddictClientService openIddictClientService, ILogger logger @@ -184,7 +186,7 @@ CancellationToken cancellationToken var expirationDate = await GetAccessTokenExpirationDateAsync(user, providerName); if (accessToken is not null && expirationDate is not null - && TimeProvider.System.GetUtcNow() <= expirationDate?.Subtract(OpenIdConnectConstants.AccessAndIdentityTokenLifetime.Divide(3)) + && clock.GetUtcNow().ToDateTimeOffset() <= expirationDate?.Subtract(OpenIdConnectConstants.AccessAndIdentityTokenLifetime.Divide(3)) ) { return accessToken; @@ -214,7 +216,7 @@ CancellationToken cancellationToken RefreshToken: refreshTokenAuthenticationResult.RefreshToken ) ); - if (errors.Count >= 1) + if (errors.Count > 0) { logger.FailedToSetAuthenticationTokens( string.Join( @@ -278,4 +280,4 @@ CancellationToken cancellationToken // `OpenIddictBuilder#AddValidation`. return await httpContext.AuthenticateAsync(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme); } -} \ No newline at end of file +} diff --git a/backend/src/Authorization/ApprovalAuthorization.cs b/backend/src/Authorization/ApprovalAuthorization.cs index f0935f850..aaa9bb86b 100644 --- a/backend/src/Authorization/ApprovalAuthorization.cs +++ b/backend/src/Authorization/ApprovalAuthorization.cs @@ -24,11 +24,11 @@ CancellationToken cancellationToken claimsPrincipal, async user => (await Context.InstitutionRepresentatives.AsQueryable() .SingleOrDefaultAsync( - x => x.UserId == user.Id, + _ => _.UserId == user.Id, cancellationToken )) is not null, application => Task.FromResult(false), cancellationToken ); } -} \ No newline at end of file +} diff --git a/backend/src/Authorization/AuthorizationPolicies.cs b/backend/src/Authorization/AuthorizationPolicies.cs index 6f69a50e8..6240e6d59 100644 --- a/backend/src/Authorization/AuthorizationPolicies.cs +++ b/backend/src/Authorization/AuthorizationPolicies.cs @@ -7,6 +7,7 @@ public static class AuthorizationPolicies public const string WriteScopePolicy = "WriteScope"; public const string AdministrateScopePolicy = "AdministrateScope"; public const string VerifyScopePolicy = "VerifyScope"; + public const string SupportScopePolicy = "SupportScope"; public const string ManageDatabaseScopePolicy = "ManageDatabaseScope"; public const string ManageGnuPgScopePolicy = "ManageGnuPgScope"; public const string ManageInstitutionRepresentativeScopePolicy = "ManageInstitutionRepresentativeScope"; diff --git a/backend/src/Authorization/CommonAuthorization.cs b/backend/src/Authorization/CommonAuthorization.cs index 21b36d645..571e80518 100644 --- a/backend/src/Authorization/CommonAuthorization.cs +++ b/backend/src/Authorization/CommonAuthorization.cs @@ -1,10 +1,11 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.Resolvers; using Metabase.Data; using Metabase.Data.OpenIdConnect; using Metabase.Enumerations; @@ -21,38 +22,62 @@ public abstract class CommonAuthorization( IDbContextFactory dbContextFactory, UserManager userManager, OpenIddictApplicationManager applicationManager - ) +) +: IDisposable, IAsyncDisposable { - protected ApplicationDbContext Context { get => dbContextFactory.CreateDbContext(); } + // This is the same code that HotChocolate returns when autheorization via the attribute `[Authorize(Policy = ...)]` fails. + private const string UNAUTHORIZED_CODE = "AUTH_NOT_AUTHENTICATED"; + + protected ApplicationDbContext Context { get; } = dbContextFactory.CreateDbContext(); protected UserManager UserManager { get; } = userManager; protected OpenIddictApplicationManager ApplicationManager { get; } = applicationManager; - internal const string ClientSubjectPrefix = "client:"; + // [Implement a DisposeAsync method](https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-disposeasync) + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public async ValueTask DisposeAsync() + { + await DisposeAsyncCore(); + Dispose(false); + GC.SuppressFinalize(this); + } - public async Task SwitchUserOrApplicationAsync( + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + Context.Dispose(); + } + } + + protected virtual ValueTask DisposeAsyncCore() + { + return Context.DisposeAsync(); + } + + public Task SwitchUserOrApplicationAsync( ClaimsPrincipal claimsPrincipal, Func> handleUser, Func> handleApplication, CancellationToken cancellationToken ) { - var userOrPrefixedClientId = claimsPrincipal.GetClaim(Claims.Subject); - // Note that a user ID is a UUID and thus cannot start with the client-subject prefix. - if (userOrPrefixedClientId is not null - && userOrPrefixedClientId.StartsWith(ClientSubjectPrefix, ignoreCase: false, culture: CultureInfo.InvariantCulture) - ) - { - var clientId = userOrPrefixedClientId[ClientSubjectPrefix.Length..]; - return await handleApplication( + return IOpenIdConnectSubject.SwitchSubjectAsync( + claimsPrincipal.GetClaim(Claims.Subject), + async (_) => await handleUser( + await GetUserAsync(claimsPrincipal) + ), + async (clientId) => await handleApplication( await ApplicationManager.FindByClientIdAsync(clientId, cancellationToken) - ); - } - else - { - return await handleUser( + ), + async () => await handleUser( await GetUserAsync(claimsPrincipal) - ); - } + ) + ); } protected Task AuthorizeAsync( @@ -87,6 +112,19 @@ Guid userId return user.Id == userId; } + public Task CanAdministrate( + ClaimsPrincipal claimsPrincipal, + CancellationToken cancellationToken + ) + { + return AuthorizeAsync( + claimsPrincipal, + user => CanAdministrate(user, claimsPrincipal), + application => Task.FromResult(false), + cancellationToken + ); + } + public async Task CanAdministrate( User user, ClaimsPrincipal claimsPrincipal @@ -113,6 +151,19 @@ ClaimsPrincipal claimsPrincipal ); } + public async Task CanSupport( + User user, + ClaimsPrincipal claimsPrincipal + ) + { + return + claimsPrincipal.HasScope(OpenIdConnectScope.SupportApiScope) + && await IsInRole( + user, + UserRole.SUPPORTER + ); + } + private Task IsInRole( User user, UserRole role @@ -289,4 +340,17 @@ await context.InstitutionRepresentatives.AsNoTracking() .SingleOrDefaultAsync(cancellationToken); return wrappedManagerRole?.Role; } + + public void ReportUnauthorizedError( + IResolverContext resolverContext + ) + { + resolverContext.ReportError( + ErrorBuilder.New() + .SetCode(UNAUTHORIZED_CODE) + .SetPath(resolverContext.Path) + .SetMessage($"The current user is not authorized to access this resource.") + .Build() + ); + } } \ No newline at end of file diff --git a/backend/src/Authorization/CommonComponentAuthorization.cs b/backend/src/Authorization/CommonComponentAuthorization.cs index ecc9b95b4..6b67add05 100644 --- a/backend/src/Authorization/CommonComponentAuthorization.cs +++ b/backend/src/Authorization/CommonComponentAuthorization.cs @@ -25,7 +25,7 @@ CancellationToken cancellationToken { var wrappedManagerId = await Context.Components.AsNoTracking() - .Where(x => x.Id == componentId) + .Where(_ => _.Id == componentId) .Select(x => new { x.ManagerId }) .SingleOrDefaultAsync(cancellationToken); if (wrappedManagerId is null) @@ -43,7 +43,7 @@ protected Task BelongsToVerifiedComponentManager( CancellationToken cancellationToken ) { - return Context.Methods.AsNoTracking() + return Context.Components.AsNoTracking() .Where(component => component.Id == componentId) .Where(component => component.Manager != null && component.Manager.State == InstitutionState.VERIFIED) .Where(component => component.Manager != null && ( diff --git a/backend/src/Authorization/CommonMethodAuthorization.cs b/backend/src/Authorization/CommonMethodAuthorization.cs index facc7b0b7..b7db289af 100644 --- a/backend/src/Authorization/CommonMethodAuthorization.cs +++ b/backend/src/Authorization/CommonMethodAuthorization.cs @@ -25,7 +25,7 @@ CancellationToken cancellationToken { var wrappedManagerId = await Context.Methods.AsNoTracking() - .Where(x => x.Id == methodId) + .Where(_ => _.Id == methodId) .Select(x => new { x.ManagerId }) .SingleOrDefaultAsync(cancellationToken); if (wrappedManagerId is null) @@ -53,4 +53,4 @@ CancellationToken cancellationToken )) .AnyAsync(cancellationToken); } -} \ No newline at end of file +} diff --git a/backend/src/Authorization/DataFormatAuthorization.cs b/backend/src/Authorization/DataFormatAuthorization.cs index 8c73411bb..728b1ee83 100644 --- a/backend/src/Authorization/DataFormatAuthorization.cs +++ b/backend/src/Authorization/DataFormatAuthorization.cs @@ -77,7 +77,7 @@ CancellationToken cancellationToken { return ( await Context.DataFormats.AsNoTracking() - .Where(x => x.Id == dataFormatId) + .Where(_ => _.Id == dataFormatId) .Select(x => new { x.ManagerId }) .SingleOrDefaultAsync(cancellationToken) )?.ManagerId; @@ -99,4 +99,4 @@ CancellationToken cancellationToken )) .AnyAsync(cancellationToken); } -} \ No newline at end of file +} diff --git a/backend/src/Authorization/OpenIdConnectAuthorization.cs b/backend/src/Authorization/OpenIdConnectAuthorization.cs index 859b4f67a..03afaa43c 100644 --- a/backend/src/Authorization/OpenIdConnectAuthorization.cs +++ b/backend/src/Authorization/OpenIdConnectAuthorization.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading; @@ -14,7 +15,9 @@ namespace Metabase.Authorization; public sealed class OpenIdConnectAuthorization( IDbContextFactory dbContextFactory, UserManager userManager, - OpenIddictApplicationManager applicationManager + OpenIddictApplicationManager applicationManager, + OpenIddictAuthorizationManager authorizationManager, + OpenIddictTokenManager tokenManager ) : CommonAuthorization(dbContextFactory, userManager, applicationManager) { internal Task IsAuthorizedToManageOpenIdConnect( @@ -74,7 +77,6 @@ CancellationToken cancellationToken internal async Task IsAuthorizedToManageAuthorization( ClaimsPrincipal claimsPrincipal, Guid authorizationId, - OpenIddictAuthorizationManager authorizationManager, CancellationToken cancellationToken ) { @@ -97,17 +99,15 @@ CancellationToken cancellationToken internal Task IsAuthorizedToManageTokensOfAuthorization( ClaimsPrincipal claimsPrincipal, Guid authorizationId, - OpenIddictAuthorizationManager authorizationManager, CancellationToken cancellationToken ) { - return IsAuthorizedToManageAuthorization(claimsPrincipal, authorizationId, authorizationManager, cancellationToken); + return IsAuthorizedToManageAuthorization(claimsPrincipal, authorizationId, cancellationToken); } internal async Task IsAuthorizedToManageToken( ClaimsPrincipal claimsPrincipal, Guid tokenId, - OpenIddictTokenManager tokenManager, CancellationToken cancellationToken ) { @@ -133,4 +133,63 @@ CancellationToken cancellationToken ) .AnyAsync(cancellationToken); } + + internal async Task> AuthorizedConsentTypes( + ClaimsPrincipal claimsPrincipal, + CancellationToken cancellationToken + ) + { + return await CanAdministrate(claimsPrincipal, cancellationToken) + ? Enum.GetValues().ToList().AsReadOnly() + : [GraphQl.OpenIdConnect.Applications.OpenIdConnectConsentType.EXPLICIT]; + } + + internal static async Task> AuthorizedEndpoints( + ClaimsPrincipal claimsPrincipal, + CancellationToken cancellationToken + ) + { + return Enum.GetValues().ToList().AsReadOnly(); + } + + internal async Task> AuthorizedGrantTypes( + ClaimsPrincipal claimsPrincipal, + CancellationToken cancellationToken + ) + { + return await CanAdministrate(claimsPrincipal, cancellationToken) + ? Enum.GetValues().ToList().AsReadOnly() + : Enum.GetValues() + .Where(_ => + _ is not GraphQl.OpenIdConnect.Applications.OpenIdConnectGrantType.TOKEN_EXCHANGE + ) + .ToList() + .AsReadOnly(); + } + + internal static async Task> AuthorizedResponseTypes( + ClaimsPrincipal claimsPrincipal, + CancellationToken cancellationToken + ) + { + return Enum.GetValues().ToList().AsReadOnly(); + } + + internal async Task> AuthorizedScopes( + ClaimsPrincipal claimsPrincipal, + CancellationToken cancellationToken + ) + { + return await CanAdministrate(claimsPrincipal, cancellationToken) + ? Enum.GetValues().ToList().AsReadOnly() + : Enum.GetValues() + .Where(_ => + _ is not GraphQl.OpenIdConnect.OpenIdConnectScope.ADMINISTRATE_API + && _ is not GraphQl.OpenIdConnect.OpenIdConnectScope.SUPPORT_API + && _ is not GraphQl.OpenIdConnect.OpenIdConnectScope.MANAGE_USER_API + ) + .ToList() + .AsReadOnly(); + } + } \ No newline at end of file diff --git a/backend/src/Authorization/UserAuthorization.cs b/backend/src/Authorization/UserAuthorization.cs index 0d0324247..f7d702646 100644 --- a/backend/src/Authorization/UserAuthorization.cs +++ b/backend/src/Authorization/UserAuthorization.cs @@ -8,7 +8,6 @@ using Metabase.Data.OpenIdConnect; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; -using OpenIddict.Abstractions; using OpenIddict.Core; using UserRole = Metabase.Enumerations.UserRole; @@ -71,6 +70,8 @@ CancellationToken cancellationToken await CanAdministrate(user, claimsPrincipal), UserRole.VERIFIER => await CanVerify(user, claimsPrincipal), + UserRole.SUPPORTER => + await CanSupport(user, claimsPrincipal), _ => throw new ArgumentOutOfRangeException(nameof(role), $"Unknown role `{role}.`") }, application => Task.FromResult(false), diff --git a/backend/src/Configuration/AuthConfiguration.cs b/backend/src/Configuration/AuthConfiguration.cs index 37ff2b1d9..8f933decd 100644 --- a/backend/src/Configuration/AuthConfiguration.cs +++ b/backend/src/Configuration/AuthConfiguration.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using NodaTime; using OpenIddict.Abstractions; using OpenIddict.Client; using Quartz; @@ -28,6 +29,7 @@ public static class AuthConfiguration { AuthorizationPolicies.WriteScopePolicy, OpenIdConnectScope.WriteApiScope }, { AuthorizationPolicies.AdministrateScopePolicy, OpenIdConnectScope.AdministrateApiScope }, { AuthorizationPolicies.VerifyScopePolicy, OpenIdConnectScope.VerifyApiScope }, + { AuthorizationPolicies.SupportScopePolicy, OpenIdConnectScope.SupportApiScope }, { AuthorizationPolicies.ManageDatabaseScopePolicy, OpenIdConnectScope.ManageDatabaseApiScope }, { AuthorizationPolicies.ManageGnuPgScopePolicy, OpenIdConnectScope.ManageGnuPgApiScope }, { AuthorizationPolicies.ManageInstitutionRepresentativeScopePolicy, OpenIdConnectScope.ManageInstitutionRepresentativeApiScope }, @@ -35,7 +37,7 @@ public static class AuthConfiguration { AuthorizationPolicies.ManageUserScopePolicy, OpenIdConnectScope.ManageUserApiScope }, }; - private static void BootstrapCertificates() + private static void BootstrapCertificates(IClock clock) { using var store = new X509Store(OpenIdConnectConstants.CertificateStoreName, OpenIdConnectConstants.CertificateStoreLocation); try @@ -51,11 +53,12 @@ private static void BootstrapCertificates() distinguishedName, validOnly: true ); - if (certificates.Count == 0) + if (certificates.Count is 0) { store.Add( JwtSigningAndEncryptionCertificateRotationJob.CreateSigningCertificate( - distinguishedName + distinguishedName, + clock ) ); } @@ -70,11 +73,12 @@ private static void BootstrapCertificates() distinguishedName, validOnly: true ); - if (certificates.Count == 0) + if (certificates.Count is 0) { store.Add( JwtSigningAndEncryptionCertificateRotationJob.CreateEncryptionCertificate( - distinguishedName + distinguishedName, + clock ) ); } @@ -113,10 +117,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(); ConfigureIdentityServices(services); @@ -459,25 +464,24 @@ AppSettings appSettings // Note: when issuing access tokens used by third-party APIs you don't own, you // can disable access token encryption: _.DisableAccessTokenEncryption(); // Note: To decrypt a JWT token, like an application code or an access token, - // you can execute ``` var jwt = - // "eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwidHlwIjoiYXQrand0In0.tn0jqCR5-01c_SDRwex6sPBNl1Vl1KSRA5Zs2UoXri9F4tG4v9B6qbLmxzsfkd0rLe55BozRV8VCChsZAt_mCZfoVGOYewwP.ogGZQ5Q2p7Yue7D6zPJlSQ.uNX1Qs9R30vZxAPj-LuJSwWnisoKHQ5qNC_K9WvA0JDSCS_orw6TsboSVCe5b_aUg3JvrkJX_Ir0c-bFMf0HVhOVNy1UJwxo9zegJOsm7MybPzK58H4ubt_PRnrSueLgnQX8aDcjbM38Imy3RN6a3r3aKawdWGcyA23sIH8XnVmGk0lDB_PqFrFE7x2MmG4fyVJINoki441UI-7x0sLFUi4o98Z-2vTFuCd9cLRY5LAeb0ZIuWwOI7dv2Q54w7uV765kHS3VIPtupzSSXgQmfPBJOzDeV_-sCZGwUuC0jL8x1vJw573fejPwpPmXj3EKgzXgbGfAHmjoIzkNvcfr--Dy7O8WxLjSERoBgW45Tq0xcCLL9Vx4JGWehOw1jY-KaKIzfjUW9CTSwLgWdhqonetAULZFRJAYOEJ8PtP49jXlvkdjmavRXaX2UD8FD5TUx30TkGQ_xyuo6HvokURQqIYILZiL6R_kVqh7kwUnj4dgBqni-56M1GYFuX2UzynagL6c6t4OlO7RhQJpgz1pzmGNdEb5nvZplXru8KVEt_e9bQOy54EWudAemEVAyX-4P6FdWWazU5vMaRnDg53Y8gy4rt6LCwXK9WHNRbhkdAmTNNsfiuUd588lzMJVSVPeqzbl70yW8IH7hbwfscLjSf2-gP9evwDKoLMKjqyBtbsqGx-qtBE47y_-LKyP3TOJug0SBtiHIMb3xzN2rJaykAMRRz4kMaMF4_TZRV8kqZQqzF_xBoMJQD3nLYaN-G0qJYIse1JhVu4yHuH7vSvXdHA0x5dnBwwq4P3g35W5zv-cw4-b1cXnLq7TYUvHNVe6DcMAJogxW5ovA7wtEjF4yKxSbvlRaO065Jed5siLrIL17RahoHqew34kMzqXL0OUHvxId0A7myvLFy0YqhLnKg.1jfT6-IVamiQbS84hfX4lLtmoTiAmf0Ea0rpLPgYEek"; - // var handler = new JwtSecurityTokenHandler(); var claimsPrincipal = - // handler.ValidateToken( jwt, new TokenValidationParameters { IssuerSigningKey - // = signingKey, TokenDecryptionKey = encryptionKey, // ValidIssuer = - // environment.IsEnvironment(Program.TestEnvironment) ? "http://localhost/" : - // appSettings.Host, ValidateActor = false, ValidateAudience = false, - // ValidateIssuer = false, ValidateIssuerSigningKey = false, ValidateLifetime = - // false, ValidateTokenReplay = false, }, out var validatedToken ); - // Console.WriteLine(validatedToken.ToString()); ``` which as of this writing - // outputs ``` - // {"alg":"A256KW","enc":"A256CBC-HS512","typ":"at+jwt"}.{"sub":"075561fa-98c0-40db-ad3d-9dc8abf240fd","name":"sw@ise.de","http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress":"sw@ise.de","oi_prst":"metabase","iss":"https://localhost:4041/","oi_au_id":"5dc00347-30fa-4ddb-91c1-471c505e7842","client_id":"metabase","oi_tkn_id":"e7430a26-4e13-4c21-8e6d-0f07dca66ff6","aud":"metabase","scope":"openid - // email profile roles api:read api:write - // offline_access","exp":1615405869,"iat":1615402269} ``` + // you can execute + // ``` + // var jwt = "eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwidHlwIjoiYXQrand0In0.tn0jqCR5-01c_SDRwex6sPBNl1Vl1KSRA5Zs2UoXri9F4tG4v9B6qbLmxzsfkd0rLe55BozRV8VCChsZAt_mCZfoVGOYewwP.ogGZQ5Q2p7Yue7D6zPJlSQ.uNX1Qs9R30vZxAPj-LuJSwWnisoKHQ5qNC_K9WvA0JDSCS_orw6TsboSVCe5b_aUg3JvrkJX_Ir0c-bFMf0HVhOVNy1UJwxo9zegJOsm7MybPzK58H4ubt_PRnrSueLgnQX8aDcjbM38Imy3RN6a3r3aKawdWGcyA23sIH8XnVmGk0lDB_PqFrFE7x2MmG4fyVJINoki441UI-7x0sLFUi4o98Z-2vTFuCd9cLRY5LAeb0ZIuWwOI7dv2Q54w7uV765kHS3VIPtupzSSXgQmfPBJOzDeV_-sCZGwUuC0jL8x1vJw573fejPwpPmXj3EKgzXgbGfAHmjoIzkNvcfr--Dy7O8WxLjSERoBgW45Tq0xcCLL9Vx4JGWehOw1jY-KaKIzfjUW9CTSwLgWdhqonetAULZFRJAYOEJ8PtP49jXlvkdjmavRXaX2UD8FD5TUx30TkGQ_xyuo6HvokURQqIYILZiL6R_kVqh7kwUnj4dgBqni-56M1GYFuX2UzynagL6c6t4OlO7RhQJpgz1pzmGNdEb5nvZplXru8KVEt_e9bQOy54EWudAemEVAyX-4P6FdWWazU5vMaRnDg53Y8gy4rt6LCwXK9WHNRbhkdAmTNNsfiuUd588lzMJVSVPeqzbl70yW8IH7hbwfscLjSf2-gP9evwDKoLMKjqyBtbsqGx-qtBE47y_-LKyP3TOJug0SBtiHIMb3xzN2rJaykAMRRz4kMaMF4_TZRV8kqZQqzF_xBoMJQD3nLYaN-G0qJYIse1JhVu4yHuH7vSvXdHA0x5dnBwwq4P3g35W5zv-cw4-b1cXnLq7TYUvHNVe6DcMAJogxW5ovA7wtEjF4yKxSbvlRaO065Jed5siLrIL17RahoHqew34kMzqXL0OUHvxId0A7myvLFy0YqhLnKg.1jfT6-IVamiQbS84hfX4lLtmoTiAmf0Ea0rpLPgYEek"; + // var handler = new JwtSecurityTokenHandler(); + // var claimsPrincipal = handler.ValidateToken( jwt, new TokenValidationParameters { IssuerSigningKey = signingKey, TokenDecryptionKey = encryptionKey, ValidIssuer = environment.IsEnvironment(Program.TestEnvironment) ? "http://localhost/" : appSettings.Host, ValidateActor = false, ValidateAudience = false, ValidateIssuer = false, ValidateIssuerSigningKey = false, ValidateLifetime = false, ValidateTokenReplay = false, }, out var validatedToken ); + // Console.WriteLine(validatedToken.ToString()); + // ``` which as of this writing + // outputs + // ``` + // {"alg":"A256KW","enc":"A256CBC-HS512","typ":"at+jwt"}.{"sub":"075561fa-98c0-40db-ad3d-9dc8abf240fd","name":"sw@ise.de","http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress":"sw@ise.de","oi_prst":"metabase","iss":"https://localhost:4041/","oi_au_id":"5dc00347-30fa-4ddb-91c1-471c505e7842","client_id":"metabase","oi_tkn_id":"e7430a26-4e13-4c21-8e6d-0f07dca66ff6","aud":"metabase","scope":"openid email profile roles api:read api:write offline_access","exp":1615405869,"iat":1615402269} + // ``` } ) // Register the OpenIddict validation components. .AddValidation(_ => { + // The validation handler uses OpenID Connect discovery to + // retrieve the issuer signing keys used to validate tokens. _.SetIssuer(appSettings.Uri); // Configure the audience accepted by this resource server. _.AddAudiences(OpenIdConnectConstants.Client.MetabaseClientId); @@ -524,6 +528,7 @@ AppSettings appSettings .EnableStatusCodePagesIntegration() // https://documentation.openiddict.com/integrations/aspnet-core#status-code-pages-middleware-integration .EnableRedirectionEndpointPassthrough() // https://documentation.openiddict.com/integrations/aspnet-core#pass-through-mode .EnablePostLogoutRedirectionEndpointPassthrough(); + // .DisableTransportSecurityRequirement(); // https://documentation.openiddict.com/integrations/aspnet-core#transport-security-requirement // Register the System.Net.Http integration and use the identity of the current diff --git a/backend/src/Configuration/GraphQlConfiguration.cs b/backend/src/Configuration/GraphQlConfiguration.cs index e9962bcc3..3f8b748e9 100644 --- a/backend/src/Configuration/GraphQlConfiguration.cs +++ b/backend/src/Configuration/GraphQlConfiguration.cs @@ -1,4 +1,6 @@ using System; +using System.Text.RegularExpressions; +using HotChocolate.AspNetCore; using HotChocolate.Configuration; using HotChocolate.Data; using HotChocolate.Data.Filters; @@ -8,18 +10,21 @@ 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 Metabase.Authentication; using Metabase.Data; using Metabase.GraphQl; using Metabase.GraphQl.DataX; using Metabase.GraphQl.Filters; +using Metabase.GraphQl.Scalars; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using NodaTime; +using DurationType = HotChocolate.Types.NodaTime.DurationType; namespace Metabase.Configuration; @@ -32,8 +37,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 +47,95 @@ 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 - .AddProjections() + .AddNodaTime() + .BindRuntimeType() + .AddTypeConverter(_ => _.ToTimeSpan()) + .AddTypeConverter(_ => Duration.FromTimeSpan(_)) + // .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 = 40000; + _.MaxTypeCost = 40000; } ) // Configure @@ -108,11 +148,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,26 +160,15 @@ await httpContext.RequestServices ) ) // Scalar Types - // TODO Use `MyUuidType` and `MyUrlType` (see code below) + // 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() - .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(_) - ) - // Object Types - .AddType() + .BindRuntimeType() // Query, Mutation, Subscription, Object, and Input Types .AddQueryType(_ => _.Name(nameof(Query))) .AddMutationType(_ => _.Name(nameof(Mutation))) @@ -153,10 +177,11 @@ await httpContext.RequestServices .AddTypes() // Paging .AddDbContextCursorPagingProvider() + // .AddCursorKeySerializer(new OffsetDateTimeCursorKeySerializer()) .ModifyPagingOptions(_ => { - _.MaxPageSize = int.MaxValue - 1; - _.DefaultPageSize = 100; + _.MaxPageSize = (int)GraphQlConstants.MaximumPageSize; + _.DefaultPageSize = (int)GraphQlConstants.MaximumPageSize; _.IncludeTotalCount = true; _.IncludeNodesField = false; _.InferConnectionNameFromField = true; @@ -167,6 +192,7 @@ await httpContext.RequestServices .AddInMemoryOperationDocumentStorage(); // Needed by the automatic persisted operation pipeline } + // // private sealed class MyUuidType : UuidType // { // private const string SpecifiedByString = "https://tools.ietf.org/html/rfc4122"; @@ -185,19 +211,6 @@ await httpContext.RequestServices // } // } - // private sealed class MyUrlType : UrlType - // { - // private const string SpecifiedByString = "https://tools.ietf.org/html/rfc3986"; - // - // public MyUrlType( - // string name, - // string? description = null, - // BindingBehavior bind = BindingBehavior.Explicit) - // : base(name, description, bind) - // { - // SpecifiedBy = new Uri(SpecifiedByString, UriKind.Absolute); - // } - // } } // https://github.com/ChilliCream/graphql-platform/blob/main/src/HotChocolate/Core/src/Types/Configuration/TypeInterceptor.cs @@ -209,25 +222,41 @@ 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}'"); } } -public sealed class CustomNamingConventions +public sealed partial class CustomNamingConventions : DefaultNamingConventions { + public static string CorrectPlural(string name) => name + .Replace("Assemblys", "Assemblies") + .Replace($"{nameof(FileMetaInformation)}s", "FilesMetaInformation"); + + // `(?!...)` is a negative lookahead. It is a zero-width assertion, meaning + // that it does not move the regex engine's cursor (nor consume text). + [GeneratedRegex(@$"^({nameof(FilterInputType)}Of|{nameof(Nullable)}Of(?!{nameof(DataKind)}))")] + private static partial Regex PrefixesToRemoveRegex(); + + public override string GetTypeName(Type type) => + PrefixesToRemoveRegex().Replace( + base.GetTypeName(type), + string.Empty + ); } // See https://chillicream.com/docs/hotchocolate/fetching-data/filtering/#filter-conventions public partial class CustomFilterConvention : FilterConvention { + [GeneratedRegex(@$"({nameof(FilterInputType)}|{GraphQlConstants.FilterInputSuffix}|FilterType|FilterInput)$")] + private static partial Regex FilterInputSuffixesToReplaceRegex(); private const string InputPostFix = "FilterInput"; private const string InputTypePostFix = "FilterInputType"; @@ -254,66 +283,55 @@ protected override void Configure(IFilterConventionDescriptor descriptor) descriptor.Provider( new QueryableFilterProvider(_ => _ .AddDefaultFieldHandlers() - .AddFieldHandler>() + .AddFieldHandler>(context => + new QueryableComparableInClosedIntervalHandler(context.TypeConverter, context.InputParser) + ) ) ); } - // For the base implementation see https://github.com/ChilliCream/hotchocolate/blob/f0dff93a14cb7ddecc7b3a0530a687a5bc4bad71/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConvention.cs#L129 - public override string GetTypeName(Type runtimeType) - { - // return base.GetTypeName(runtimeType); - return GetTypeName(runtimeType, plural: false); - } + // For the base implementation see https://github.com/ChilliCream/graphql-platform/blob/develop/src/HotChocolate/Data/src/Data/Filters/Convention/FilterConvention.cs#L129 + public override string GetTypeName(Type runtimeType) => + CustomNamingConventions.CorrectPlural( + GetTypeName(runtimeType, plural: false) + ); - public string GetTypeName(Type runtimeType, bool plural) + private string GetTypeName(Type runtimeType, bool plural) { ArgumentNullException.ThrowIfNull(runtimeType); var pluralSuffix = plural ? "s" : ""; if (typeof(IEnumOperationFilterInputType).IsAssignableFrom(runtimeType) && runtimeType.GenericTypeArguments.Length == 1 - && runtimeType.GetGenericTypeDefinition() == typeof(EnumOperationFilterInputType<>)) + && runtimeType.GetGenericTypeDefinition() == typeof(EnumOperationFilterInputType<>) + ) { var genericName = _namingConventions.GetTypeName(runtimeType.GenericTypeArguments[0]); - return $"{genericName}{pluralSuffix}{GraphQlConstants.FilterInputSuffix}"; ; - // return genericName + "OperationFilterInput"; + return $"{genericName}{pluralSuffix}{GraphQlConstants.FilterInputSuffix}"; } if (typeof(IComparableOperationFilterInputType).IsAssignableFrom(runtimeType) && runtimeType.GenericTypeArguments.Length == 1 && runtimeType.GetGenericTypeDefinition() - == typeof(ComparableOperationFilterInputType<>)) + == typeof(ComparableOperationFilterInputType<>) + ) { var genericName = _namingConventions.GetTypeName(runtimeType.GenericTypeArguments[0]); return $"Comparable{genericName}{pluralSuffix}{GraphQlConstants.FilterInputSuffix}"; - // return $"Comparable{genericName}OperationFilterInput"; } if (typeof(IListFilterInputType).IsAssignableFrom(runtimeType) - && runtimeType.GenericTypeArguments.Length == 1) + && runtimeType.GenericTypeArguments.Length == 1 + ) { var genericType = runtimeType.GenericTypeArguments[0]; - var genericName = typeof(FilterInputType).IsAssignableFrom(genericType) + return typeof(FilterInputType).IsAssignableFrom(genericType) ? GetTypeName(genericType, plural: true) - : "List" + _namingConventions.GetTypeName(genericType); - return $"{genericName}"; - // return "List" + genericName; + : $"{_namingConventions.GetTypeName(genericType)}{pluralSuffix}"; } var name = _namingConventions.GetTypeName(runtimeType); - var isInputObjectType = typeof(FilterInputType).IsAssignableFrom(runtimeType); - var isEndingInput = name.EndsWith(InputPostFix, StringComparison.Ordinal); - var isEndingInputType = name.EndsWith(InputTypePostFix, StringComparison.Ordinal); - if (isInputObjectType && isEndingInputType) - { - return $"{name[..^"FilterInputType".Length]}{pluralSuffix}{GraphQlConstants.FilterInputSuffix}"; - } - if (isInputObjectType && !isEndingInput && !isEndingInputType) + if (typeof(FilterInputType).IsAssignableFrom(runtimeType) || !name.EndsWith("FilterInput", StringComparison.InvariantCulture)) { - return $"{name}{pluralSuffix}{GraphQlConstants.FilterInputSuffix}"; + return $"{FilterInputSuffixesToReplaceRegex().Replace(name, string.Empty)}{pluralSuffix}{GraphQlConstants.FilterInputSuffix}"; } - if (!isInputObjectType && !isEndingInput) - { - return $"{name}{pluralSuffix}{GraphQlConstants.FilterInputSuffix}"; - } - return name; + return $"{name}{pluralSuffix}"; } } @@ -384,6 +402,8 @@ this IFilterConventionDescriptor descriptor .BindRuntimeType() .BindRuntimeType() .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType() .BindRuntimeType() .BindRuntimeType() .BindRuntimeType() @@ -396,16 +416,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(); } } @@ -416,4 +444,10 @@ protected override void Configure(ISortConventionDescriptor descriptor) { descriptor.AddDefaults(); } + + // For the base implementation see https://github.com/ChilliCream/graphql-platform/blob/develop/src/HotChocolate/Data/src/Data/Sorting/Convention/SortConvention.cs#L134C5 + public override string GetTypeName(Type runtimeType) => + CustomNamingConventions.CorrectPlural( + base.GetTypeName(runtimeType) + ); } \ No newline at end of file diff --git a/backend/src/Controllers/AuthenticationController.cs b/backend/src/Controllers/AuthenticationController.cs index f61d69453..22a5ab3e8 100644 --- a/backend/src/Controllers/AuthenticationController.cs +++ b/backend/src/Controllers/AuthenticationController.cs @@ -27,7 +27,7 @@ AppSettings appSettings private readonly Uri _issuer = appSettings.Uri; [HttpGet("~/connect/client/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( @@ -41,7 +41,7 @@ public ActionResult LogIn(string? returnUrl) ) { // Only allow local return URLs to prevent open redirect attacks. - RedirectUri = SanitizeReturnUrl(returnUrl) + RedirectUri = SanitizeReturnUrl(returnTo) }, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme ); @@ -50,7 +50,7 @@ public ActionResult LogIn(string? returnUrl) [HttpPost("~/connect/client/logout")] [Authorize(AuthenticationSchemes = AuthenticationConstants.IdentityAndCookieAndBearerTokenAuthenticationScheme)] [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). @@ -59,7 +59,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. @@ -79,7 +79,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 ); @@ -198,7 +198,7 @@ public async Task LogInCallback(string provider) provider, AuthenticationTokens.From(accessToken, result) ); - if (errors.Count >= 1) + if (errors.Count > 0) { throw new InvalidOperationException( string.Join( @@ -239,11 +239,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/Controllers/AuthorizationController.cs b/backend/src/Controllers/AuthorizationController.cs index dded00788..e57f95f63 100644 --- a/backend/src/Controllers/AuthorizationController.cs +++ b/backend/src/Controllers/AuthorizationController.cs @@ -12,9 +12,10 @@ using System.Text.Json.Nodes; using System.Threading.Tasks; using Metabase.Authentication; -using Metabase.Authorization; using Metabase.Data; using Metabase.Data.OpenIdConnect; +using Metabase.Extensions; +using Metabase.GraphQl.OpenIdConnect; using Metabase.ViewModels.Authorization; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Antiforgery; @@ -30,6 +31,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Tokens; +using NodaTime; using OpenIddict.Abstractions; using OpenIddict.Core; using OpenIddict.Server.AspNetCore; @@ -38,9 +40,10 @@ namespace Metabase.Controllers; public sealed class AuthorizationController( + IClock clock, OpenIddictApplicationManager applicationManager, OpenIddictAuthorizationManager authorizationManager, - OpenIddictScopeManager scopeManager, + OpenIddictScopeManager scopeManager, SignInManager signInManager, UserManager userManager, ApplicationDbContext dbContext, @@ -89,7 +92,7 @@ private async Task CreateClaimsIdentityAsync( // `user.Name` instead of the default value `user.UserName` for the // claim `Claims.Name` because `user.UserName` is actually the email // address. - identity.SetClaim(Claims.Subject, await userManager.GetUserIdAsync(user)); + identity.SetClaim(Claims.Subject, IOpenIdConnectSubject.BuildUserSubject(user.Id)); identity.SetClaim(Claims.Name, user.Name); // identity.SetClaim(Claims.Email, await userManager.GetEmailAsync(user)); // identity.SetClaims(Claims.Role, [.. await userManager.GetRolesAsync(user)]); @@ -135,7 +138,7 @@ string applicationId var authorization = authorizations.LastOrDefault(); authorization ??= await authorizationManager.CreateAsync( identity, - subject: await userManager.GetUserIdAsync(user), + subject: IOpenIdConnectSubject.BuildUserSubject(user.Id), client: applicationId, type: AuthorizationTypes.Permanent, scopes: identity.GetScopes() @@ -190,7 +193,7 @@ public async Task Authorize() || ( request.MaxAge is not null && result.Properties?.IssuedUtc is not null - && TimeProvider.System.GetUtcNow() - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value) + && clock.GetUtcNow().ToDateTimeOffset() - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value) ) ) && TempData[IgnoreAuthenticationChallengeKey] is null or false @@ -315,11 +318,12 @@ await CreatePermanentAuthorization( // In every other case, render the consent form. default: - return View(new AuthorizeViewModel - { - ApplicationName = await applicationManager.GetLocalizedDisplayNameAsync(application), - Scope = request.Scope - }); + return View(new AuthorizeViewModel( + await applicationManager.GetLocalizedDisplayNameAsync(application) ?? application.ClientId ?? "Unknown", + request.GetScopes() + .Select(_ => _.ToOpenIdConnectScope()) + .ToArray() + )); } } @@ -405,7 +409,12 @@ public async Task Deny() // Remove the `AuthenticationConstants.IdentityApplicationScheme` // cookie as it was only needed to authenticate the user. await signInManager.SignOutAsync(); - return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + var properties = new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.AccessDenied, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user denied the authorization request." + }); + return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } #endregion @@ -650,9 +659,10 @@ public async Task Exchange() roleType: Claims.Role); // Add the claims that will be persisted in the tokens (use the client_id as the subject identifier). - var clientId = await applicationManager.GetClientIdAsync(application); + var clientId = await applicationManager.GetClientIdAsync(application) + ?? throw new InvalidOperationException("The application does not have a client ID."); var displayName = await applicationManager.GetDisplayNameAsync(application); - identity.SetClaim(Claims.Subject, $"{CommonAuthorization.ClientSubjectPrefix}{clientId}"); + identity.SetClaim(Claims.Subject, IOpenIdConnectSubject.BuildClientSubject(clientId)); identity.SetClaim(Claims.Name, displayName); // Note: In the original OAuth 2.0 specification, the client credentials grant diff --git a/backend/src/Controllers/ErrorController.cs b/backend/src/Controllers/ErrorController.cs new file mode 100644 index 000000000..8e19c32a9 --- /dev/null +++ b/backend/src/Controllers/ErrorController.cs @@ -0,0 +1,24 @@ +using Metabase.ViewModels.Common; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Mvc; + +namespace Metabase.Controllers; + +public class ErrorController : Controller +{ + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + [Route("~/error")] + public IActionResult Error() + { + var response = HttpContext.GetOpenIddictServerResponse() ?? HttpContext.GetOpenIddictClientResponse(); + if (response is not null) + { + return View(new ErrorViewModel + { + Error = response.Error, + ErrorDescription = response.ErrorDescription + }); + } + return View(new ErrorViewModel()); + } +} \ No newline at end of file diff --git a/backend/src/Controllers/GetHttpsResourceController.cs b/backend/src/Controllers/GetHttpsResourceController.cs new file mode 100644 index 000000000..b885d427a --- /dev/null +++ b/backend/src/Controllers/GetHttpsResourceController.cs @@ -0,0 +1,139 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Mime; +using System.Threading; +using System.Threading.Tasks; +using Metabase.Authentication; +using Metabase.Data; +using Metabase.GraphQl.DataX; +using Metabase.GraphQl.Requests; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Metabase.Controllers; + +// Inspired by https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-5.0#upload-large-files-with-streaming +// and https://github.com/dotnet/AspNetCore.Docs/blob/b4599432690b8753fc2eac23d52957f47e01997a/aspnetcore/mvc/models/file-uploads/samples/3.x/SampleApp/ +[ApiController] +public sealed class GetHttpsResourcesController : Controller +{ + private bool _disposed; + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (!_disposed) + { + // Dispose of resources held by this instance. + _disposed = true; + } + } + + // Disposable types implement a finalizer. + ~GetHttpsResourcesController() + { + Dispose(false); + } + + private const string GetByVertexIdRouteName = "GetResourceByVertexId"; + + [HttpGet("~/api/resources/{vertexId}", Name = GetByVertexIdRouteName)] + [Authorize(AuthenticationSchemes = AuthenticationConstants.IdentityAndCookieAndBearerTokenAuthenticationScheme)] + [AllowAnonymous] + [EndpointDescription("Get an HTTP resource in the media type of its data format from a database passing along an access token that identifies the logged-in user.")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task Get( + [FromRoute] string vertexId, + [FromQuery] Guid dataId, + [FromQuery] DataKind dataKind, + [FromQuery] Guid databaseId, + [FromServices] IDbContextFactory databaseContextFactory, + [FromServices] QueryingDatabases queryingDatabases, + [FromServices] DataQueries dataQueries, + CancellationToken cancellationToken + ) + { + await using var databaseContext = await databaseContextFactory.CreateDbContextAsync(cancellationToken); + var database = await databaseContext.Databases.AsNoTracking() + .Where(_ => _.Id == databaseId) + .SingleOrDefaultAsync(cancellationToken); + if (database is null) + { + return Problem( + title: "Database Not Found", + detail: $"There is no database with ID '{databaseId:D}'.", + statusCode: StatusCodes.Status404NotFound, + instance: HttpContext.Request.Path + ); + } + var data = await dataQueries.GetDataAsync(database, dataId, dataKind, null, null, cancellationToken); + if (data is null) + { + return Problem( + title: "Data Not Found", + detail: $"There is no data with ID '{dataId:D}' of kind '{dataKind}' in the database with ID '{databaseId:D}'.", + statusCode: StatusCodes.Status404NotFound, + instance: HttpContext.Request.Path + ); + } + var getHttpsResource = + data.ResourceTree.Root.VertexId == vertexId + ? data.ResourceTree.Root.Value + : data.ResourceTree.NonRootVertices.FirstOrDefault(_ => _.VertexId == vertexId)?.Value; + if (getHttpsResource is null) + { + return Problem( + title: "Resource Not Found", + detail: $"There is no GET HTTPS resource with vertex ID '{vertexId}' in the data with ID '{dataId:D}' of kind '{dataKind}' in the database with ID '{databaseId:D}'.", + statusCode: StatusCodes.Status404NotFound, + instance: HttpContext.Request.Path + ); + } + var dataFormat = await databaseContext.DataFormats.AsNoTracking() + .Where(_ => _.Id == getHttpsResource.DataFormatId) + .SingleOrDefaultAsync(cancellationToken); + if (dataFormat is null) + { + return Problem( + title: "Data Format Not Found", + detail: $"There is no data format with ID '{getHttpsResource.DataFormatId:D}'.", + statusCode: StatusCodes.Status404NotFound, + instance: HttpContext.Request.Path + ); + } + // Do not use `using var ...` below to not dispose objects when the associated response stream is still in use + var httpClient = await queryingDatabases.CreateHttpClientAsync(database, cancellationToken); + var httpResponseMessage = + await httpClient.GetAsync( + getHttpsResource.Locator, + HttpCompletionOption.ResponseHeadersRead, // do not buffer in memory before returning + cancellationToken + ); + if (httpResponseMessage.StatusCode is not HttpStatusCode.OK) + { + return Problem( + title: "Fetch Failure", + detail: $"Failed to fetch the GET HTTPS resource from '{getHttpsResource.Locator}'.", + statusCode: (int)httpResponseMessage.StatusCode, + instance: HttpContext.Request.Path + ); + } + var contentType = httpResponseMessage.Content.Headers.ContentType; + var responseStream = + await httpResponseMessage.Content + .ReadAsStreamAsync(cancellationToken); + var fileExtension = dataFormat?.Extension is null ? "unknown" : dataFormat.Extension; + return File( + responseStream, + contentType?.ToString() ?? MediaTypeNames.Application.Octet, + $"{getHttpsResource.HashValue}.{fileExtension}", + enableRangeProcessing: true + ); + } +} \ No newline at end of file diff --git a/backend/src/Data/ApplicationDbContext.cs b/backend/src/Data/ApplicationDbContext.cs index 8a4b749ba..b636e5ba8 100644 --- a/backend/src/Data/ApplicationDbContext.cs +++ b/backend/src/Data/ApplicationDbContext.cs @@ -3,10 +3,14 @@ using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using SchemaNameOptionsExtension = Metabase.Data.Extensions.SchemaNameOptionsExtension; using NodaTime; +using System.Threading; +using System.Threading.Tasks; +using System.Linq; +using Metabase.Data.Extensions; +using Metabase.Extensions; +using Npgsql.EntityFrameworkCore.PostgreSQL.ValueGeneration; namespace Metabase.Data; @@ -14,11 +18,12 @@ namespace Metabase.Data; // [Authentication and authorization for SPAs](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity-api-authorization?view=aspnetcore-3.0) // [Customize Identity Model](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/customize-identity-model?view=aspnetcore-3.0) public sealed class ApplicationDbContext - : IdentityDbContext, - IDataProtectionKeyContext +: IdentityDbContext, + IDataProtectionKeyContext { private const string DefaultSchemaName = "metabase"; private readonly string _schemaName; + private readonly IClock _clock; internal const string ComponentCategoryTypeName = "component_category"; internal const string DatabaseVerificationStateTypeName = "database_verification_state"; @@ -30,7 +35,8 @@ public sealed class ApplicationDbContext internal const string StandardizerTypeName = "standardizer"; public ApplicationDbContext( - DbContextOptions options + DbContextOptions options, + IClock clock ) : base(options) { @@ -38,6 +44,7 @@ DbContextOptions options // 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 @@ -114,22 +121,51 @@ public OffsetDateTimeUtcValueConverter() } } - private static - EntityTypeBuilder - ConfigureEntity( - EntityTypeBuilder builder - ) - where TEntity : Entity + public override int SaveChanges() + { + UpdateTimestamps(); + return base.SaveChanges(); + } + + public override Task SaveChangesAsync(CancellationToken cancellationToken = default) { - // https://www.npgsql.org/efcore/modeling/generated-properties.html#guiduuid-generation - builder - .Property(e => e.Id) - .HasDefaultValueSql("gen_random_uuid()"); - // https://www.npgsql.org/efcore/modeling/concurrency.html#the-postgresql-xmin-system-column - builder - .Property(e => e.Version) - .IsRowVersion(); - return builder; + 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; + } + } } private static void ConfigureIdentityEntities( @@ -137,10 +173,7 @@ ModelBuilder builder ) { // https://stackoverflow.com/questions/19902756/asp-net-identity-dbcontext-confusion/35722688#35722688 - builder.Entity() - .ToTable("user") - .Property(e => e.Version) - .IsRowVersion(); + builder.Entity().ToTable("user"); builder.Entity().ToTable("role"); builder.Entity().ToTable("user_claim"); builder.Entity().ToTable("user_role"); @@ -381,48 +414,106 @@ private static void ConfigureMethodManager(ModelBuilder builder) .OnDelete(DeleteBehavior.Restrict); } + private static void ConfigureOpenIdConnect(ModelBuilder builder) + { + // auto-include for GraphQL `OpenIdConnectAuthorizationType` + builder.Entity() + .Navigation(a => a.Application) + .AutoInclude(); + // auto-include for GraphQL `OpenIdConnectTokenType` + builder.Entity() + .Navigation(a => a.Application) + .AutoInclude(); + builder.Entity() + .Navigation(a => a.Authorization) + .AutoInclude(); + } + protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.HasDefaultSchema(_schemaName); builder.HasPostgresExtension("pgcrypto"); // https://www.npgsql.org/efcore/modeling/generated-properties.html#guiduuid-generation + builder.Entity().ToTable("component"); + builder.Entity().ToTable("database"); + builder.Entity().ToTable("gnu_pg_fingerprint"); + builder.Entity().ToTable("data_format"); + builder.Entity().ToTable("institution"); + builder.Entity().ToTable("method"); ConfigureIdentityEntities(builder); - ConfigureEntity( - builder.Entity() - ) - .ToTable("component"); ConfigureComponentAssembly(builder); ConfigureComponentConcretizationAndGeneralization(builder); ConfigureComponentManufacturer(builder); ConfigureComponentVariant(builder); - ConfigureEntity( - builder.Entity() - ) - .ToTable("database"); - ConfigureEntity( - builder.Entity() - ) - .ToTable("gnu_pg_fingerprint"); - ConfigureEntity( - builder.Entity() - ) - .ToTable("data_format"); - ConfigureEntity( - builder.Entity() - ) - .ToTable("institution"); ConfigureInstitutionMethodDeveloper(builder); ConfigureInstitutionRepresentative(builder); ConfigureOpenIdConnectApplicationOwner(builder); ConfigureDatabaseOperator(builder); - ConfigureEntity( - builder.Entity() - ) - .ToTable("method"); ConfigureUserMethodDeveloper(builder); ConfigureInstitutionManager(builder); ConfigureComponentManager(builder); ConfigureDataFormatManager(builder); ConfigureMethodManager(builder); + ConfigureOpenIdConnect(builder); + foreach (var entityType in builder.Model.GetEntityTypes()) + { + if (typeof(IEntity).IsAssignableFrom(entityType.ClrType)) + { + var entity = builder.Entity(entityType.ClrType); + entity.HasKey(nameof(IEntity.Id)); + // https://www.npgsql.org/efcore/modeling/generated-properties.html#guiduuid-generation + entity + .Property(nameof(IEntity.Id)) + .HasDefaultValueSql("uuidv7()") + .HasValueGenerator(); + // 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 = builder.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 = builder.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 = builder.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 = builder.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 000000000..e45524dd7 --- /dev/null +++ b/backend/src/Data/Association.cs @@ -0,0 +1,8 @@ +namespace Metabase.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 000000000..680b89c46 --- /dev/null +++ b/backend/src/Data/AuditableAssociation.cs @@ -0,0 +1,10 @@ +using System; + +namespace Metabase.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 000000000..f6b87de99 --- /dev/null +++ b/backend/src/Data/AuditableEntity.cs @@ -0,0 +1,20 @@ +using System; + +namespace Metabase.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/Component.cs b/backend/src/Data/Component.cs index 19c7d93f1..6ce11ae40 100644 --- a/backend/src/Data/Component.cs +++ b/backend/src/Data/Component.cs @@ -10,7 +10,8 @@ namespace Metabase.Data; public sealed class Component - : Entity + : AuditableEntity, + INamed { #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. public Component() diff --git a/backend/src/Data/ComponentAssembly.cs b/backend/src/Data/ComponentAssembly.cs index be482278e..4000dc364 100644 --- a/backend/src/Data/ComponentAssembly.cs +++ b/backend/src/Data/ComponentAssembly.cs @@ -4,7 +4,9 @@ namespace Metabase.Data; public sealed class ComponentAssembly +: AuditableAssociation, IAssociation { + public Guid AssembledComponentId { get; set; } public Component AssembledComponent { get; set; } = default!; diff --git a/backend/src/Data/ComponentConcretizationAndGeneralization.cs b/backend/src/Data/ComponentConcretizationAndGeneralization.cs index 7d880f10f..41cc8b0f1 100644 --- a/backend/src/Data/ComponentConcretizationAndGeneralization.cs +++ b/backend/src/Data/ComponentConcretizationAndGeneralization.cs @@ -3,6 +3,7 @@ namespace Metabase.Data; public sealed class ComponentConcretizationAndGeneralization +: AuditableAssociation, IAssociation { public Guid GeneralComponentId { get; set; } public Component GeneralComponent { get; set; } = default!; diff --git a/backend/src/Data/ComponentManufacturer.cs b/backend/src/Data/ComponentManufacturer.cs index af51c467a..b1ab4e4ce 100644 --- a/backend/src/Data/ComponentManufacturer.cs +++ b/backend/src/Data/ComponentManufacturer.cs @@ -3,6 +3,7 @@ namespace Metabase.Data; public sealed class ComponentManufacturer +: AuditableAssociation, IAssociation { public Guid ComponentId { get; set; } public Component Component { get; set; } = default!; diff --git a/backend/src/Data/ComponentVariant.cs b/backend/src/Data/ComponentVariant.cs index e7b572614..d06047106 100644 --- a/backend/src/Data/ComponentVariant.cs +++ b/backend/src/Data/ComponentVariant.cs @@ -1,8 +1,10 @@ +using EntityFrameworkCore.Projectables; using System; namespace Metabase.Data; public sealed class ComponentVariant +: AuditableAssociation, IAssociation { public Guid OfComponentId { get; set; } public Component OfComponent { get; set; } = default!; diff --git a/backend/src/Data/DataCopnstants.cs b/backend/src/Data/DataCopnstants.cs new file mode 100644 index 000000000..d8950aa06 --- /dev/null +++ b/backend/src/Data/DataCopnstants.cs @@ -0,0 +1,16 @@ +namespace Metabase.Data; + +public static class DataConstants +{ + public const string TestlabOpenIdConnectClientId = "testlab-solar-facades"; + public const string IgsdbOpenIdConnectClientId = "igsdb"; + + public const string IseInstitutionUuid = "5320d6fb-b96d-4aeb-a24c-eb7036d3437a"; + public const string TestlabInstitutionUuid = "82b9f95c-3261-463a-90fe-0e9da707af17"; + public const string LbnlInstitutionUuid = "c17af5ef-2f1d-4c73-bcc9-fcfb722420f3"; + public const string EpeaInstitutionUuid = "84580047-d1db-4275-bd29-4222c907cabe"; + + public const string TestlabDatabaseUuid = "8a27aa0d-6026-4124-b185-4efd5cead953"; + public const string IgsdbDatabaseUuid = "48994b60-670d-488d-aaf7-53333a64f1d6"; + public const string EpeaDatabaseUuid = "82490800-1712-48d0-8b4a-2ca821487c8d"; +} \ No newline at end of file diff --git a/backend/src/Data/DataFormat.cs b/backend/src/Data/DataFormat.cs index 2108f2643..3e53af3d4 100644 --- a/backend/src/Data/DataFormat.cs +++ b/backend/src/Data/DataFormat.cs @@ -5,7 +5,8 @@ namespace Metabase.Data; public sealed class DataFormat - : Entity + : AuditableEntity, + INamed { #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. public DataFormat() diff --git a/backend/src/Data/Database.cs b/backend/src/Data/Database.cs index dba9c25ad..50f8a2f66 100644 --- a/backend/src/Data/Database.cs +++ b/backend/src/Data/Database.cs @@ -7,7 +7,8 @@ namespace Metabase.Data; public sealed class Database - : Entity + : AuditableEntity, + INamed { #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. public Database() diff --git a/backend/src/Data/DbSeeder.cs b/backend/src/Data/DbSeeder.cs index fe5df256a..4a1bc9e0b 100644 --- a/backend/src/Data/DbSeeder.cs +++ b/backend/src/Data/DbSeeder.cs @@ -1,6 +1,7 @@ using System; using System.Collections.ObjectModel; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Metabase.Authentication; using Metabase.Data.OpenIdConnect; @@ -60,9 +61,6 @@ string scope public sealed class DbSeeder { - public const string TestlabSolarFacadesOpenIdConnectClientId = "testlab-solar-facades"; - public const string IgsdbOpenIdConnectClientId = "igsdb"; - public static readonly ReadOnlyCollection<(string Name, string EmailAddress, Enumerations.UserRole Role)> Users = Role.AllEnum.Select(role => ( Role.EnumToName(role), @@ -72,21 +70,19 @@ public sealed class DbSeeder public static readonly (string Name, string EmailAddress, Enumerations.UserRole Role) AdministratorUser = - Users.First(x => x.Role == Enumerations.UserRole.ADMINISTRATOR); + Users.First(_ => _.Role == Enumerations.UserRole.ADMINISTRATOR); public static readonly (string Name, string EmailAddress, Enumerations.UserRole Role) VerifierUser = - Users.First(x => x.Role == Enumerations.UserRole.VERIFIER); - - private const string IseInstitutionUuid = "5320d6fb-b96d-4aeb-a24c-eb7036d3437a"; - private const string TestlabInstitutionUuid = "82b9f95c-3261-463a-90fe-0e9da707af17"; - private const string LbnlInstitutionUuid = "c17af5ef-2f1d-4c73-bcc9-fcfb722420f3"; + Users.First(_ => _.Role == Enumerations.UserRole.VERIFIER); - private const string TestlabDatabaseUuid = "8a27aa0d-6026-4124-b185-4efd5cead953"; - private const string IgsdbDatabaseUuid = "48994b60-670d-488d-aaf7-53333a64f1d6"; + public static readonly (string Name, string EmailAddress, Enumerations.UserRole Role) + SupporterUser = + Users.First(_ => _.Role == Enumerations.UserRole.SUPPORTER); public static async Task DoAsync( - IServiceProvider services + IServiceProvider services, + CancellationToken cancellationToken ) { var logger = services.GetRequiredService>(); @@ -94,11 +90,11 @@ IServiceProvider services var environment = services.GetRequiredService(); var appSettings = services.GetRequiredService(); await CreateRolesAsync(services, logger); - await CreateUsersAsync(services, environment, appSettings, logger); - await CreateInstitutionsAsync(services, environment); - await CreateDatabasesAsync(services, environment, appSettings); - await CreateOpenIdConnectScopes(services, logger); - await CreateOpenIdConnectApplications(services, logger, environment, appSettings); + await CreateUsersAsync(services, environment, appSettings, logger, cancellationToken); + await CreateInstitutionsAsync(services, environment, cancellationToken); + await CreateDatabasesAsync(services, environment, appSettings, cancellationToken); + await CreateOpenIdConnectScopes(services, logger, cancellationToken); + await CreateOpenIdConnectApplications(services, logger, environment, appSettings, cancellationToken); } private static async Task CreateRolesAsync( @@ -123,15 +119,20 @@ private static async Task CreateUsersAsync( IServiceProvider services, IWebHostEnvironment environment, AppSettings appSettings, - ILogger logger + ILogger logger, + CancellationToken cancellationToken ) { + var context = services.GetRequiredService(); var manager = services.GetRequiredService>(); if (environment.IsProduction()) { - if ((await manager.GetUsersInRoleAsync(Role.Administrator)).Count == 0) + if (!await context.Users.AnyAsync(cancellationToken)) { - await CreateUserAsync(manager, AdministratorUser, appSettings.BootstrapUserPassword, logger); + if ((await manager.GetUsersInRoleAsync(Role.Administrator)).Count is 0) + { + await CreateUserAsync(manager, AdministratorUser, appSettings.BootstrapUserPassword, logger); + } } } else @@ -140,7 +141,12 @@ ILogger logger { if (await manager.FindByEmailAsync(userInfo.EmailAddress) is null) { - await CreateUserAsync(manager, userInfo, appSettings.BootstrapUserPassword, logger); + await CreateUserAsync( + manager, + userInfo, + appSettings.BootstrapUserPassword, + logger + ); } } } @@ -167,21 +173,22 @@ await manager.CreateAsync( private static async Task CreateInstitutionsAsync( IServiceProvider services, - IWebHostEnvironment environment + IWebHostEnvironment environment, + CancellationToken cancellationToken ) { var manager = services.GetRequiredService>(); var context = services.GetRequiredService(); - var iseInstitution = await context.Institutions.Where(_ => _.Id == new Guid(IseInstitutionUuid)).SingleOrDefaultAsync(); + var iseInstitution = await context.Institutions.Where(_ => _.Id == new Guid(DataConstants.IseInstitutionUuid)).SingleOrDefaultAsync(cancellationToken); if (iseInstitution is null) { iseInstitution = new Institution( - new Guid(IseInstitutionUuid), + new Guid(DataConstants.IseInstitutionUuid), "Fraunhofer ISE", "ISE", "Fraunhofer Institute for Solar Energy Systems (ISE)", new ContactInformation( - phoneNumber: "+49 761 45880", + phoneNumber: "+4976145880", isPhoneNumberConfirmed: true, postalAddress: "Heidenhofstraße 2, 79110 Freiburg im Breisgau", emailAddress: null, @@ -195,137 +202,174 @@ IWebHostEnvironment environment iseInstitution.RepresentativeEdges.Add( new InstitutionRepresentative { - UserId = (await context.Users.Where(x => x.Email == AdministratorUser.EmailAddress).SingleAsync()).Id, + UserId = (await context.Users.Where(_ => _.Email == AdministratorUser.EmailAddress).SingleAsync(cancellationToken)).Id, Role = InstitutionRepresentativeRole.OWNER, Pending = false } ); - var application = await manager.FindByClientIdAsync(OpenIdConnectConstants.Client.MetabaseClientId).AsTask(); + var application = await manager.FindByClientIdAsync(OpenIdConnectConstants.Client.MetabaseClientId, cancellationToken).AsTask(); if (application is not null) { iseInstitution.OpenIdConnectApplications.Add(application); } context.Institutions.Add(iseInstitution); - await context.SaveChangesAsync(); } - if (environment.IsDevelopment()) + if (!await context.Institutions.Where(_ => _.Id == new Guid(DataConstants.TestlabInstitutionUuid)).AnyAsync(cancellationToken)) { - if (!await context.Institutions.Where(x => x.Id == new Guid(TestlabInstitutionUuid)).AnyAsync()) + var institution = new Institution( + new Guid(DataConstants.TestlabInstitutionUuid), + "TestLab Solar Facades", + "TLSF", + "This institution represents the TestLab Solar Facades of Fraunhofer ISE", + new ContactInformation( + phoneNumber: "+4976145885673", + isPhoneNumberConfirmed: true, + postalAddress: "Heidenhofstraße 2, 79110 Freiburg im Breisgau", + emailAddress: null, + isEmailAddressConfirmed: false, + websiteLocator: new Uri("https://www.ise.fraunhofer.de/en/rd-infrastructure/accredited-labs/testlab-solar-facades.html", UriKind.Absolute) + ), + InstitutionState.VERIFIED, + InstitutionOperatingState.OPERATING, + null + ) { - var institution = new Institution( - new Guid(TestlabInstitutionUuid), - "TestLab Solar Facades", - "TLSF", - "This institution represents the TestLab Solar Facades of Fraunhofer ISE", - new ContactInformation( - phoneNumber: "+49 761 4588-5673", - isPhoneNumberConfirmed: true, - postalAddress: "Heidenhofstraße 2, 79110 Freiburg im Breisgau", - emailAddress: null, - isEmailAddressConfirmed: false, - websiteLocator: new Uri("https://www.ise.fraunhofer.de/en/rd-infrastructure/accredited-labs/testlab-solar-facades.html", UriKind.Absolute) - ), - InstitutionState.VERIFIED, - InstitutionOperatingState.OPERATING, - null - ) - { - ManagerId = iseInstitution.Id - }; - - var application = await manager.FindByClientIdAsync(TestlabSolarFacadesOpenIdConnectClientId).AsTask(); - if (application is not null) - { - institution.OpenIdConnectApplications.Add(application); - } - context.Institutions.Add(institution); - await context.SaveChangesAsync(); - } - if (!await context.Institutions.Where(x => x.Id == new Guid(LbnlInstitutionUuid)).AnyAsync()) + ManagerId = iseInstitution.Id + }; + var application = await manager.FindByClientIdAsync(DataConstants.TestlabOpenIdConnectClientId, cancellationToken).AsTask(); + if (application is not null) { - var institution = new Institution( - new Guid(LbnlInstitutionUuid), - "LBNL", - "LBNL", - "Lawrence Berkeley National Laboratory", - new ContactInformation( - phoneNumber: "(510) 486-4000", - isPhoneNumberConfirmed: true, - postalAddress: "1 Cyclotron Road, Berkeley, CA 94720", - emailAddress: null, - isEmailAddressConfirmed: false, - websiteLocator: new Uri("https://www.lbl.gov", UriKind.Absolute) - ), - InstitutionState.VERIFIED, - InstitutionOperatingState.OPERATING, - null - ) - { - ManagerId = iseInstitution.Id - }; - context.Institutions.Add(institution); - await context.SaveChangesAsync(); + institution.OpenIdConnectApplications.Add(application); } + context.Institutions.Add(institution); } + if (!await context.Institutions.Where(_ => _.Id == new Guid(DataConstants.LbnlInstitutionUuid)).AnyAsync(cancellationToken)) + { + var institution = new Institution( + new Guid(DataConstants.LbnlInstitutionUuid), + "LBNL", + "LBNL", + "Lawrence Berkeley National Laboratory", + new ContactInformation( + phoneNumber: "+5104864000", + isPhoneNumberConfirmed: true, + postalAddress: "1 Cyclotron Road, Berkeley, CA 94720", + emailAddress: null, + isEmailAddressConfirmed: false, + websiteLocator: new Uri("https://www.lbl.gov", UriKind.Absolute) + ), + InstitutionState.VERIFIED, + InstitutionOperatingState.OPERATING, + null + ) + { + ManagerId = iseInstitution.Id + }; + context.Institutions.Add(institution); + } + if (!await context.Institutions.Where(_ => _.Id == new Guid(DataConstants.EpeaInstitutionUuid)).AnyAsync(cancellationToken)) + { + var institution = new Institution( + new Guid(DataConstants.EpeaInstitutionUuid), + "EPEA - Part of Drees & Sommer", + "EPEA", + "Sustainability", + new ContactInformation( + phoneNumber: null, + isPhoneNumberConfirmed: false, + postalAddress: null, + emailAddress: null, + isEmailAddressConfirmed: false, + websiteLocator: null + ), + InstitutionState.VERIFIED, + InstitutionOperatingState.OPERATING, + null + ) + { + ManagerId = iseInstitution.Id + }; + context.Institutions.Add(institution); + } + await context.SaveChangesAsync(cancellationToken); } private static async Task CreateDatabasesAsync( IServiceProvider services, IWebHostEnvironment environment, - AppSettings appSettings + AppSettings appSettings, + CancellationToken cancellationToken ) { - if (environment.IsDevelopment()) + var context = services.GetRequiredService(); + if (!await context.Databases.AnyAsync(cancellationToken)) { - var context = services.GetRequiredService(); - if (!await context.Databases.Where(x => x.Id == new Guid(TestlabDatabaseUuid)).AnyAsync()) + if (!await context.Databases.Where(_ => _.Id == new Guid(DataConstants.TestlabDatabaseUuid)).AnyAsync(cancellationToken)) { var uriBuilder = new UriBuilder(appSettings.TestlabSolarFacades.Uri) { Path = "/graphql/" }; var database = new Database( - new Guid(TestlabDatabaseUuid), + new Guid(DataConstants.TestlabDatabaseUuid), "TestLab DB", "The database of the TestLab Solar Facades of Fraunhofer ISE", uriBuilder.Uri ) { - OperatorId = new Guid(TestlabInstitutionUuid) + OperatorId = new Guid(DataConstants.TestlabInstitutionUuid) }; database.Verify(); context.Databases.Add(database); - await context.SaveChangesAsync(); } - if (!await context.Databases.Where(x => x.Id == new Guid(IgsdbDatabaseUuid)).AnyAsync()) + if (!await context.Databases.Where(_ => _.Id == new Guid(DataConstants.IgsdbDatabaseUuid)).AnyAsync(cancellationToken)) { var uriBuilder = new UriBuilder(new Uri("https://igsdb-v2-staging.herokuapp.com", UriKind.Absolute)) { Path = "/graphql/" }; var database = new Database( - new Guid(IgsdbDatabaseUuid), + new Guid(DataConstants.IgsdbDatabaseUuid), "IGSDB", "The International Glazing and Shading Database (IGSDB)", uriBuilder.Uri ) { - OperatorId = new Guid(LbnlInstitutionUuid) + OperatorId = new Guid(DataConstants.LbnlInstitutionUuid) + }; + database.Verify(); + context.Databases.Add(database); + } + if (!await context.Databases.Where(_ => _.Id == new Guid(DataConstants.EpeaDatabaseUuid)).AnyAsync(cancellationToken)) + { + var uriBuilder = new UriBuilder(new Uri("https://app.conpli.eu/GraphQL", UriKind.Absolute)) + { + Path = "/graphql/" + }; + var database = new Database( + new Guid(DataConstants.EpeaDatabaseUuid), + "ProCA Database", + "Database for Life-Cycle data of components", + uriBuilder.Uri + ) + { + OperatorId = new Guid(DataConstants.EpeaInstitutionUuid) }; database.Verify(); context.Databases.Add(database); - await context.SaveChangesAsync(); } + await context.SaveChangesAsync(cancellationToken); } } private static async Task CreateOpenIdConnectScopes( IServiceProvider services, - ILogger logger + ILogger logger, + CancellationToken cancellationToken ) { var manager = services.GetRequiredService>(); - if (await manager.FindByNameAsync(OpenIdConnectScope.ReadApiScope) is null) + if (await manager.FindByNameAsync(OpenIdConnectScope.ReadApiScope, cancellationToken) is null) { logger.CreatingScope(OpenIdConnectScope.ReadApiScope); await manager.CreateAsync( @@ -337,11 +381,12 @@ await manager.CreateAsync( { OpenIdConnectConstants.Client.MetabaseClientId } - } + }, + cancellationToken ); } - if (await manager.FindByNameAsync(OpenIdConnectScope.WriteApiScope) is null) + if (await manager.FindByNameAsync(OpenIdConnectScope.WriteApiScope, cancellationToken) is null) { logger.CreatingScope(OpenIdConnectScope.WriteApiScope); await manager.CreateAsync( @@ -353,11 +398,12 @@ await manager.CreateAsync( { OpenIdConnectConstants.Client.MetabaseClientId } - } + }, + cancellationToken ); } - if (await manager.FindByNameAsync(OpenIdConnectScope.AdministrateApiScope) is null) + if (await manager.FindByNameAsync(OpenIdConnectScope.AdministrateApiScope, cancellationToken) is null) { logger.CreatingScope(OpenIdConnectScope.AdministrateApiScope); await manager.CreateAsync( @@ -369,11 +415,12 @@ await manager.CreateAsync( { OpenIdConnectConstants.Client.MetabaseClientId } - } + }, + cancellationToken ); } - if (await manager.FindByNameAsync(OpenIdConnectScope.VerifyApiScope) is null) + if (await manager.FindByNameAsync(OpenIdConnectScope.VerifyApiScope, cancellationToken) is null) { logger.CreatingScope(OpenIdConnectScope.VerifyApiScope); await manager.CreateAsync( @@ -385,11 +432,29 @@ await manager.CreateAsync( { OpenIdConnectConstants.Client.MetabaseClientId } - } + }, + cancellationToken + ); + } + + if (await manager.FindByNameAsync(OpenIdConnectScope.SupportApiScope, cancellationToken) is null) + { + logger.CreatingScope(OpenIdConnectScope.SupportApiScope); + await manager.CreateAsync( + new OpenIddictScopeDescriptor + { + DisplayName = "Allow customer support role", + Name = OpenIdConnectScope.SupportApiScope, + Resources = + { + OpenIdConnectConstants.Client.MetabaseClientId + } + }, + cancellationToken ); } - if (await manager.FindByNameAsync(OpenIdConnectScope.ManageUserApiScope) is null) + if (await manager.FindByNameAsync(OpenIdConnectScope.ManageUserApiScope, cancellationToken) is null) { logger.CreatingScope(OpenIdConnectScope.ManageUserApiScope); await manager.CreateAsync( @@ -401,11 +466,12 @@ await manager.CreateAsync( { OpenIdConnectConstants.Client.MetabaseClientId } - } + }, + cancellationToken ); } - if (await manager.FindByNameAsync(OpenIdConnectScope.ManageOpenIdConnectApiScope) is null) + if (await manager.FindByNameAsync(OpenIdConnectScope.ManageOpenIdConnectApiScope, cancellationToken) is null) { logger.CreatingScope(OpenIdConnectScope.ManageOpenIdConnectApiScope); await manager.CreateAsync( @@ -417,11 +483,12 @@ await manager.CreateAsync( { OpenIdConnectConstants.Client.MetabaseClientId } - } + }, + cancellationToken ); } - if (await manager.FindByNameAsync(OpenIdConnectScope.ManageInstitutionRepresentativeApiScope) is null) + if (await manager.FindByNameAsync(OpenIdConnectScope.ManageInstitutionRepresentativeApiScope, cancellationToken) is null) { logger.CreatingScope(OpenIdConnectScope.ManageInstitutionRepresentativeApiScope); await manager.CreateAsync( @@ -433,11 +500,12 @@ await manager.CreateAsync( { OpenIdConnectConstants.Client.MetabaseClientId } - } + }, + cancellationToken ); } - if (await manager.FindByNameAsync(OpenIdConnectScope.ManageGnuPgApiScope) is null) + if (await manager.FindByNameAsync(OpenIdConnectScope.ManageGnuPgApiScope, cancellationToken) is null) { logger.CreatingScope(OpenIdConnectScope.ManageGnuPgApiScope); await manager.CreateAsync( @@ -449,11 +517,12 @@ await manager.CreateAsync( { OpenIdConnectConstants.Client.MetabaseClientId } - } + }, + cancellationToken ); } - if (await manager.FindByNameAsync(OpenIdConnectScope.ManageDatabaseApiScope) is null) + if (await manager.FindByNameAsync(OpenIdConnectScope.ManageDatabaseApiScope, cancellationToken) is null) { logger.CreatingScope(OpenIdConnectScope.ManageDatabaseApiScope); await manager.CreateAsync( @@ -465,7 +534,8 @@ await manager.CreateAsync( { OpenIdConnectConstants.Client.MetabaseClientId } - } + }, + cancellationToken ); } } @@ -474,30 +544,33 @@ private static async Task CreateOpenIdConnectApplications( IServiceProvider services, ILogger logger, IWebHostEnvironment environment, - AppSettings appSettings + AppSettings appSettings, + CancellationToken cancellationToken ) { var context = services.GetRequiredService(); var manager = services.GetRequiredService>(); - if (await manager.FindByClientIdAsync(OpenIdConnectConstants.Client.MetabaseClientId) is null) + if (!await context.OpenIdConnectApplications.AnyAsync(cancellationToken)) { - logger.CreatingApplicationClient(OpenIdConnectConstants.Client.MetabaseClientId); - var host = appSettings.Uri; - var descriptor = new OpenIddictApplicationDescriptor + if (await manager.FindByClientIdAsync(OpenIdConnectConstants.Client.MetabaseClientId, cancellationToken) is null) { - ClientId = OpenIdConnectConstants.Client.MetabaseClientId, - ClientSecret = null, - ConsentType = OpenIddictConstants.ConsentTypes.Explicit, - DisplayName = "Metabase client application", - RedirectUris = + logger.CreatingApplicationClient(OpenIdConnectConstants.Client.MetabaseClientId); + var host = appSettings.Uri; + var descriptor = new OpenIddictApplicationDescriptor + { + ClientId = OpenIdConnectConstants.Client.MetabaseClientId, + ClientSecret = null, + ConsentType = OpenIddictConstants.ConsentTypes.Explicit, + DisplayName = "Metabase", + RedirectUris = { new UriBuilder(host) { Path = "/connect/callback/login/metabase" }.Uri }, - PostLogoutRedirectUris = + PostLogoutRedirectUris = { new UriBuilder(host) { Path = "/connect/callback/logout/metabase" }.Uri }, - Permissions = + Permissions = { OpenIddictConstants.Permissions.Endpoints.Authorization, OpenIddictConstants.Permissions.Endpoints.EndSession, @@ -509,45 +582,42 @@ AppSettings appSettings OpenIddictConstants.Permissions.ResponseTypes.IdToken, OpenIddictConstants.Permissions.ResponseTypes.Token, }, - Requirements = + Requirements = { OpenIddictConstants.Requirements.Features.ProofKeyForCodeExchange, OpenIddictConstants.Requirements.Features.PushedAuthorizationRequests, } + } + .AddGrantTypePermissions( + environment.IsEnvironment(Program.TestEnvironment) + ? OpenIddictConstants.GrantTypes.Password + : OpenIddictConstants.GrantTypes.AuthorizationCode, + OpenIddictConstants.GrantTypes.ClientCredentials, + OpenIddictConstants.GrantTypes.RefreshToken, + OpenIddictConstants.GrantTypes.TokenExchange + ) + .AddScopePermissions(OpenIdConnectScope.Scopes) + .AddAudiencePermissions(OpenIdConnectConstants.Client.MetabaseClientId) + .AddResourcePermissions(appSettings.GraphQlEndpoint.AbsoluteUri); + var application = new OpenIdConnectApplication + { + OwnerId = new Guid(DataConstants.IseInstitutionUuid) + }; + await manager.PopulateAsync(application, descriptor, cancellationToken); + // The secret is used in tests, see `IntegrationTests#RequestAuthToken` and in + // the metabase client, see `OPEN_ID_CONNECT_CLIENT_SECRET` in `.env.*`. + await manager.CreateAsync(application, appSettings.OpenIdConnectClientSecret, cancellationToken); } - .AddGrantTypePermissions( - environment.IsEnvironment(Program.TestEnvironment) - ? OpenIddictConstants.GrantTypes.Password - : OpenIddictConstants.GrantTypes.AuthorizationCode, - OpenIddictConstants.GrantTypes.ClientCredentials, - OpenIddictConstants.GrantTypes.RefreshToken, - OpenIddictConstants.GrantTypes.TokenExchange - ) - .AddScopePermissions(OpenIdConnectScope.Scopes) - .AddAudiencePermissions(OpenIdConnectConstants.Client.MetabaseClientId) - .AddResourcePermissions(appSettings.GraphQlEndpoint.AbsoluteUri); - var application = new OpenIdConnectApplication + if (await manager.FindByClientIdAsync(DataConstants.TestlabOpenIdConnectClientId, cancellationToken) is null) { - OwnerId = new Guid(IseInstitutionUuid) - }; - await manager.PopulateAsync(application, descriptor); - // The secret is used in tests, see `IntegrationTests#RequestAuthToken` and in - // the metabase client, see `OPEN_ID_CONNECT_CLIENT_SECRET` in `.env.*`. - await manager.CreateAsync(application, appSettings.OpenIdConnectClientSecret); - } - - if (environment.IsDevelopment()) - { - if (await manager.FindByClientIdAsync(TestlabSolarFacadesOpenIdConnectClientId) is null) - { - logger.CreatingApplicationClient(TestlabSolarFacadesOpenIdConnectClientId); + logger.CreatingApplicationClient(DataConstants.TestlabOpenIdConnectClientId); var host = appSettings.TestlabSolarFacades.Uri; var descriptor = new OpenIddictApplicationDescriptor { - ClientId = TestlabSolarFacadesOpenIdConnectClientId, + ClientId = DataConstants.TestlabOpenIdConnectClientId, ClientSecret = null, ConsentType = OpenIddictConstants.ConsentTypes.Explicit, - DisplayName = "Testlab-Solar-Facades client application", + DisplayName = "TestLab Solar Façades", RedirectUris = { new UriBuilder(host) { Path = "/connect/callback/login/metabase" }.Uri @@ -586,23 +656,22 @@ AppSettings appSettings .AddAudiencePermissions(OpenIdConnectConstants.Client.MetabaseClientId); var application = new OpenIdConnectApplication { - OwnerId = new Guid(TestlabInstitutionUuid) + OwnerId = new Guid(DataConstants.TestlabInstitutionUuid) }; - await manager.PopulateAsync(application, descriptor); + await manager.PopulateAsync(application, descriptor, cancellationToken); // The secret is used in the database client, see // `OPEN_ID_CONNECT_CLIENT_SECRET` in `.env.*`. - await manager.CreateAsync(application, appSettings.TestlabSolarFacades.OpenIdConnectClientSecret); + await manager.CreateAsync(application, appSettings.TestlabSolarFacades.OpenIdConnectClientSecret, cancellationToken); } - - if (await manager.FindByClientIdAsync(IgsdbOpenIdConnectClientId) is null) + if (await manager.FindByClientIdAsync(DataConstants.IgsdbOpenIdConnectClientId, cancellationToken) is null) { - logger.CreatingApplicationClient(IgsdbOpenIdConnectClientId); + logger.CreatingApplicationClient(DataConstants.IgsdbOpenIdConnectClientId); var descriptor = new OpenIddictApplicationDescriptor { - ClientId = IgsdbOpenIdConnectClientId, + ClientId = DataConstants.IgsdbOpenIdConnectClientId, ClientSecret = null, ConsentType = OpenIddictConstants.ConsentTypes.Explicit, - DisplayName = "IGSDB client application", + DisplayName = "IGSDB", RedirectUris = { }, PostLogoutRedirectUris = { }, Permissions = @@ -630,10 +699,10 @@ AppSettings appSettings .AddAudiencePermissions(OpenIdConnectConstants.Client.MetabaseClientId); var application = new OpenIdConnectApplication { - OwnerId = new Guid(LbnlInstitutionUuid) + OwnerId = new Guid(DataConstants.LbnlInstitutionUuid) }; - await manager.PopulateAsync(application, descriptor); - await manager.CreateAsync(application, appSettings.Igsdb.OpenIdConnectClientSecret); + await manager.PopulateAsync(application, descriptor, cancellationToken); + await manager.CreateAsync(application, appSettings.Igsdb.OpenIdConnectClientSecret, cancellationToken); } } } diff --git a/backend/src/Data/Entity.cs b/backend/src/Data/Entity.cs index 7be5f6be8..d9bc8b56f 100644 --- a/backend/src/Data/Entity.cs +++ b/backend/src/Data/Entity.cs @@ -1,7 +1,5 @@ using System; -// using System.ComponentModel.DataAnnotations.Schema; - namespace Metabase.Data; public abstract class Entity @@ -16,7 +14,7 @@ public Entity(Guid id) Id = id; } - public Guid Id { get; private set; } + public Guid Id { get; init; } // [NotMapped] // public Guid Uuid { get => Id; } diff --git a/backend/src/Data/GnuPgKeyFingerprint.cs b/backend/src/Data/GnuPgKeyFingerprint.cs index 351786454..57cef02be 100644 --- a/backend/src/Data/GnuPgKeyFingerprint.cs +++ b/backend/src/Data/GnuPgKeyFingerprint.cs @@ -11,8 +11,8 @@ namespace Metabase.Data; [Index(nameof(Fingerprint), IsUnique = true)] public sealed partial class GnuPgKeyFingerprint( string fingerprint - ) - : Entity +) +: AuditableEntity { [GeneratedRegex("[^A-F0-9]")] private static partial Regex HexadecimalRegex(); @@ -27,7 +27,6 @@ public static string Normalize(string dirtyFingerprint) [Required][MinLength(1)] public string Fingerprint { get; private set; } = Normalize(fingerprint); - [Required] public OffsetDateTime CreatedAt { get; private set; } = OffsetDateTime.UtcNow; public OffsetDateTime? AllowedAt { get; private set; } public OffsetDateTime? ForbiddenAt { get; private set; } @@ -39,19 +38,19 @@ public static string Normalize(string dirtyFingerprint) [InverseProperty(nameof(Institution.GnuPgKeyFingerprints))] public Institution? Institution { get; set; } - public void Allow() + public void Allow(IClock clock) { - AllowedAt ??= OffsetDateTime.UtcNow; + AllowedAt ??= clock.GetUtcNow(); } - public void Forbid() + public void Forbid(IClock clock) { // If this fingerprint has not been allowed for approval yet before it // shall be forbidden now, we set `AllowedAt` and `ForbiddenAt` to // the present moment making its total validity range the half closed // interval `[AllowedAt, ForbiddenAt)` empty. This makes sure that // whenever `ForbiddenAt` is set, `AllowedAt` is also set. - var now = OffsetDateTime.UtcNow; + var now = clock.GetUtcNow(); AllowedAt ??= now; ForbiddenAt ??= now; } diff --git a/backend/src/Data/IAssociation.cs b/backend/src/Data/IAssociation.cs new file mode 100644 index 000000000..d57b7a2d8 --- /dev/null +++ b/backend/src/Data/IAssociation.cs @@ -0,0 +1,7 @@ +namespace Metabase.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 000000000..e87fd4650 --- /dev/null +++ b/backend/src/Data/IAuditable.cs @@ -0,0 +1,13 @@ +using System; + +namespace Metabase.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/IMethodDeveloper.cs b/backend/src/Data/IMethodDeveloper.cs index d5553f614..562cc24b9 100644 --- a/backend/src/Data/IMethodDeveloper.cs +++ b/backend/src/Data/IMethodDeveloper.cs @@ -8,6 +8,7 @@ namespace Metabase.Data; [JsonDerivedType(typeof(UserMethodDeveloper), typeDiscriminator: nameof(UserMethodDeveloper))] [JsonDerivedType(typeof(InstitutionMethodDeveloper), typeDiscriminator: nameof(InstitutionMethodDeveloper))] public interface IMethodDeveloper +: IAuditable, IAssociation { public Guid MethodId { get; } public Method Method { get; } diff --git a/backend/src/Data/INamed.cs b/backend/src/Data/INamed.cs new file mode 100644 index 000000000..2427f2a47 --- /dev/null +++ b/backend/src/Data/INamed.cs @@ -0,0 +1,6 @@ +namespace Metabase.Data; + +public interface INamed +{ + public string Name { get; } +} \ No newline at end of file diff --git a/backend/src/Data/Institution.cs b/backend/src/Data/Institution.cs index 1d2134158..84862d026 100644 --- a/backend/src/Data/Institution.cs +++ b/backend/src/Data/Institution.cs @@ -9,7 +9,9 @@ namespace Metabase.Data; public sealed class Institution -: Entity, IStakeholder +: AuditableEntity, + IStakeholder, + INamed { // #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. // public Institution() diff --git a/backend/src/Data/InstitutionMethodDeveloper.cs b/backend/src/Data/InstitutionMethodDeveloper.cs index 3bd55ab9c..53580295c 100644 --- a/backend/src/Data/InstitutionMethodDeveloper.cs +++ b/backend/src/Data/InstitutionMethodDeveloper.cs @@ -3,10 +3,11 @@ namespace Metabase.Data; public sealed class InstitutionMethodDeveloper - : IMethodDeveloper +: AuditableAssociation, IMethodDeveloper, IAssociation { public Guid InstitutionId { get; set; } public Institution Institution { get; set; } = default!; + public Guid MethodId { get; set; } public Method Method { get; set; } = default!; diff --git a/backend/src/Data/InstitutionRepresentative.cs b/backend/src/Data/InstitutionRepresentative.cs index 32c81d931..e6ab400eb 100644 --- a/backend/src/Data/InstitutionRepresentative.cs +++ b/backend/src/Data/InstitutionRepresentative.cs @@ -5,6 +5,7 @@ namespace Metabase.Data; public sealed class InstitutionRepresentative +: AuditableAssociation, IAssociation { public Guid InstitutionId { get; set; } public Institution Institution { get; set; } = default!; diff --git a/backend/src/Data/Method.cs b/backend/src/Data/Method.cs index fcc72eafa..539ac6535 100644 --- a/backend/src/Data/Method.cs +++ b/backend/src/Data/Method.cs @@ -10,7 +10,8 @@ namespace Metabase.Data; public sealed class Method - : Entity +: AuditableEntity, + INamed { #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. public Method() diff --git a/backend/src/Data/OpenIdConnect/IOpenIdConnectSubject.cs b/backend/src/Data/OpenIdConnect/IOpenIdConnectSubject.cs new file mode 100644 index 000000000..457825c5a --- /dev/null +++ b/backend/src/Data/OpenIdConnect/IOpenIdConnectSubject.cs @@ -0,0 +1,43 @@ +using System; +using System.Globalization; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Metabase.GraphQl; + +namespace Metabase.Data.OpenIdConnect; + +[JsonPolymorphic(TypeDiscriminatorPropertyName = GraphQlConstants.TypeDiscriminatorPropertyName)] +[JsonDerivedType(typeof(User), typeDiscriminator: nameof(User))] +[JsonDerivedType(typeof(OpenIdConnectApplication), typeDiscriminator: nameof(OpenIdConnectApplication))] +public interface IOpenIdConnectSubject +{ + private const string ClientSubjectPrefix = "client:"; + + public static string BuildUserSubject(Guid userId) => + userId.ToString("D"); + + public static string BuildClientSubject(string clientId) => + $"{ClientSubjectPrefix}{clientId}"; + + public static Task SwitchSubjectAsync( + string? subject, + Func> handleUserId, + Func> handleClientId, + Func> handleUnknownSubject + ) + { + if (subject is null) + { + return handleUnknownSubject(); + } + if (subject.StartsWith(ClientSubjectPrefix, ignoreCase: false, culture: CultureInfo.InvariantCulture)) + { + return handleClientId(subject[ClientSubjectPrefix.Length..]); + } + if (Guid.TryParse(subject, out Guid userId)) + { + return handleUserId(userId); + } + return handleUnknownSubject(); + } +} \ No newline at end of file diff --git a/backend/src/Data/OpenIdConnect/OpenIdConnectApplication.cs b/backend/src/Data/OpenIdConnect/OpenIdConnectApplication.cs index 3323d8d77..6f39fbafd 100644 --- a/backend/src/Data/OpenIdConnect/OpenIdConnectApplication.cs +++ b/backend/src/Data/OpenIdConnect/OpenIdConnectApplication.cs @@ -1,19 +1,24 @@ using System; -using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using OpenIddict.EntityFrameworkCore.Models; namespace Metabase.Data.OpenIdConnect; public sealed class OpenIdConnectApplication - : OpenIddictEntityFrameworkCoreApplication, - IEntity +: OpenIddictEntityFrameworkCoreApplication, + IEntity, + IAuditable, + IOpenIdConnectSubject { public Guid OwnerId { get; set; } [InverseProperty(nameof(Institution.OpenIdConnectApplications))] public Institution Owner { get; set; } = null!; - [Timestamp] + public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset UpdatedAt { get; set; } + + // 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/OpenIdConnect/OpenIdConnectAuthorization.cs b/backend/src/Data/OpenIdConnect/OpenIdConnectAuthorization.cs index c9c87c533..704cccc9a 100644 --- a/backend/src/Data/OpenIdConnect/OpenIdConnectAuthorization.cs +++ b/backend/src/Data/OpenIdConnect/OpenIdConnectAuthorization.cs @@ -1,13 +1,18 @@ using System; -using System.ComponentModel.DataAnnotations; using OpenIddict.EntityFrameworkCore.Models; namespace Metabase.Data.OpenIdConnect; public sealed class OpenIdConnectAuthorization : OpenIddictEntityFrameworkCoreAuthorization, - IEntity + IEntity, + IAuditable { - [Timestamp] + // `createdAt` could be an alias of `creationDate` + public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset UpdatedAt { get; set; } + + // 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/OpenIdConnect/OpenIdConnectScope.cs b/backend/src/Data/OpenIdConnect/OpenIdConnectScope.cs index 2c93717d2..4cc0b6c5a 100644 --- a/backend/src/Data/OpenIdConnect/OpenIdConnectScope.cs +++ b/backend/src/Data/OpenIdConnect/OpenIdConnectScope.cs @@ -1,5 +1,4 @@ using System; -using System.ComponentModel.DataAnnotations; using OpenIddict.Abstractions; using OpenIddict.EntityFrameworkCore.Models; @@ -7,7 +6,8 @@ namespace Metabase.Data.OpenIdConnect; public sealed class OpenIdConnectScope : OpenIddictEntityFrameworkCoreScope, - IEntity + IEntity, + IAuditable { private const string ScopeSeparator = ":"; @@ -20,6 +20,8 @@ public sealed class OpenIdConnectScope public const string AdministrateApiScope = ApiScopePrefix + ScopeSeparator + "administrate"; // Allow elevated privileges for users with role "Verifier" public const string VerifyApiScope = ApiScopePrefix + ScopeSeparator + "verify"; + // Allow elevated privileges for users with role "Supporter" + public const string SupportApiScope = ApiScopePrefix + ScopeSeparator + "support"; public const string ManageDatabaseApiScope = ApiScopePrefix + ScopeSeparator + "database" + ScopeSeparator + "manage"; public const string ManageGnuPgApiScope = ApiScopePrefix + ScopeSeparator + "gnu_pg" + ScopeSeparator + "manage"; public const string ManageInstitutionRepresentativeApiScope = ApiScopePrefix + ScopeSeparator + "institution_representative" + ScopeSeparator + "manage"; @@ -38,6 +40,7 @@ public sealed class OpenIdConnectScope WriteApiScope, AdministrateApiScope, VerifyApiScope, + SupportApiScope, ManageDatabaseApiScope, ManageGnuPgApiScope, ManageInstitutionRepresentativeApiScope, @@ -45,6 +48,10 @@ public sealed class OpenIdConnectScope ManageUserApiScope, ]; - [Timestamp] + public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset UpdatedAt { get; set; } + + // 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/OpenIdConnect/OpenIdConnectToken.cs b/backend/src/Data/OpenIdConnect/OpenIdConnectToken.cs index b62ba3a28..d9c7b7d54 100644 --- a/backend/src/Data/OpenIdConnect/OpenIdConnectToken.cs +++ b/backend/src/Data/OpenIdConnect/OpenIdConnectToken.cs @@ -1,13 +1,18 @@ using System; -using System.ComponentModel.DataAnnotations; using OpenIddict.EntityFrameworkCore.Models; namespace Metabase.Data.OpenIdConnect; public sealed class OpenIdConnectToken : OpenIddictEntityFrameworkCoreToken, - IEntity + IEntity, + IAuditable { - [Timestamp] + // `createdAt` could be an alias of `creationDate` + public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset UpdatedAt { get; set; } + + // 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/Publication.cs b/backend/src/Data/Publication.cs index 886c9d430..be6a0de5d 100644 --- a/backend/src/Data/Publication.cs +++ b/backend/src/Data/Publication.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel.DataAnnotations; using HotChocolate; +using Metabase.GraphQl.Scalars; using Microsoft.EntityFrameworkCore; namespace Metabase.Data; @@ -23,20 +24,23 @@ public sealed class Publication( [MinLength(1)] [RegularExpression("^(10[.][0-z/.]*)$")] [GraphQLDescription("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.")] + [GraphQLType] public string? Doi { get; private set; } = doi; [MinLength(1)] [RegularExpression("^(arXiv:)[0-z./]*$")] [GraphQLDescription("The website arXiv.org is a free and open-access archive for publications. The arXiv identifier can be used to define a publication.")] + [GraphQLType] public string? ArXiv { get; private set; } = arXiv; [MinLength(1)] [RegularExpression("^(urn:)[0-z:./-]*$")] - [GraphQLDescription("A Uniform Resource Name (URN) can be used to define a publication. TODO: Improve the regex pattern to further restrict the string.")] + [GraphQLDescription("A Uniform Resource Name (URN) can be used to define a publication.")] + [GraphQLType] public string? Urn { get; private set; } = urn; [Url] - [GraphQLDescription("If a persistent identifier 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.")] + [GraphQLDescription("If a persistent identifier like DOI is defined above, this web address 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.")] public Uri? WebAddress { get; private set; } = webAddress; [MinLength(1)] public string? Title { get; private set; } = title; diff --git a/backend/src/Data/Role.cs b/backend/src/Data/Role.cs index 920dde358..9e907a3b2 100644 --- a/backend/src/Data/Role.cs +++ b/backend/src/Data/Role.cs @@ -1,7 +1,6 @@ using System; using System.Collections.ObjectModel; using Microsoft.AspNetCore.Identity; -using Guid = System.Guid; namespace Metabase.Data; @@ -9,12 +8,14 @@ public sealed class Role : IdentityRole { public const string Administrator = "Administrator"; private const string Verifier = "Verifier"; + private const string Supporter = "Supporter"; public static readonly ReadOnlyCollection AllEnum = Array.AsReadOnly( [ Enumerations.UserRole.ADMINISTRATOR, - Enumerations.UserRole.VERIFIER + Enumerations.UserRole.VERIFIER, + Enumerations.UserRole.SUPPORTER ]); // public ICollection UserRoles { get; } = new List(); @@ -39,6 +40,7 @@ public static string EnumToName(Enumerations.UserRole role) { Enumerations.UserRole.ADMINISTRATOR => Administrator, Enumerations.UserRole.VERIFIER => Verifier, + Enumerations.UserRole.SUPPORTER => Supporter, _ => throw new ArgumentOutOfRangeException(nameof(role), $"Unknown role `{role}.`") }; } @@ -49,6 +51,7 @@ public static Enumerations.UserRole EnumFromName(string name) { Administrator => Enumerations.UserRole.ADMINISTRATOR, Verifier => Enumerations.UserRole.VERIFIER, + Supporter => Enumerations.UserRole.SUPPORTER, _ => throw new ArgumentOutOfRangeException(nameof(name), $"Unknown name `{name}.`") }; } diff --git a/backend/src/Data/User.cs b/backend/src/Data/User.cs index 0bf8130ec..49decc61b 100644 --- a/backend/src/Data/User.cs +++ b/backend/src/Data/User.cs @@ -4,15 +4,18 @@ using System.ComponentModel.DataAnnotations.Schema; using HotChocolate; using Microsoft.AspNetCore.Identity; -using Guid = System.Guid; +using Metabase.Data.OpenIdConnect; // TODO Make `User`, `Role`, ... subtype `Entity` and use `Version` to catch update conflicts. Add interface `IEntity`. namespace Metabase.Data; public sealed class User - : IdentityUser, - IEntity, - IStakeholder +: IdentityUser, + IEntity, + IAuditable, + INamed, + IStakeholder, + IOpenIdConnectSubject { #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. public User() @@ -65,5 +68,8 @@ public User( [InverseProperty(nameof(GnuPgKeyFingerprint.User))] public ICollection GnuPgKeyFingerprints { get; } = []; + public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset UpdatedAt { get; set; } + 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/UserMethodDeveloper.cs b/backend/src/Data/UserMethodDeveloper.cs index aeda7885d..b81ff2275 100644 --- a/backend/src/Data/UserMethodDeveloper.cs +++ b/backend/src/Data/UserMethodDeveloper.cs @@ -3,10 +3,11 @@ namespace Metabase.Data; public sealed class UserMethodDeveloper - : IMethodDeveloper +: AuditableAssociation, IMethodDeveloper, IAssociation { public Guid UserId { get; set; } public User User { get; set; } = default!; + public Guid MethodId { get; set; } public Method Method { get; set; } = default!; diff --git a/backend/src/Enumerations/UserRole.cs b/backend/src/Enumerations/UserRole.cs index e4147b47d..c483604c5 100644 --- a/backend/src/Enumerations/UserRole.cs +++ b/backend/src/Enumerations/UserRole.cs @@ -4,4 +4,5 @@ public enum UserRole { ADMINISTRATOR, VERIFIER, + SUPPORTER } \ No newline at end of file diff --git a/backend/src/Extensions/EnumExtensions.cs b/backend/src/Extensions/EnumExtensions.cs new file mode 100644 index 000000000..c568bb1ab --- /dev/null +++ b/backend/src/Extensions/EnumExtensions.cs @@ -0,0 +1,20 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Reflection; + +namespace Metabase.Extensions; + +public static class EnumExtensions +{ + [Pure] + public static string GetDescription(this Enum value) + { + return value.GetType() + .GetMember(value.ToString()) + .FirstOrDefault() + ?.GetCustomAttribute() + ?.Description ?? value.ToString(); + } +} \ No newline at end of file diff --git a/backend/src/Extensions/HttpContextExtensions.cs b/backend/src/Extensions/HttpContextExtensions.cs index 19806daa4..a44280384 100644 --- a/backend/src/Extensions/HttpContextExtensions.cs +++ b/backend/src/Extensions/HttpContextExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Diagnostics.Contracts; using Metabase.Authentication; using Microsoft.AspNetCore.Http; @@ -10,13 +11,14 @@ public static class HttpContextExtensions public static void SetBearerToken(this HttpContext httpContext, string accessToken) => httpContext.Request.Headers.Authorization = $"{OpenIdConnectConstants.AuthorizationHeaderBearer} {accessToken}"; + [Pure] public static string? ExtractBearerToken(this HttpContext httpContext) { var bearerTokenPrefix = $"{OpenIdConnectConstants.AuthorizationHeaderBearer} "; return httpContext.Request?.Headers?.Authorization .FirstOrDefault( - x => x is not null - && x.TrimStart().StartsWith(bearerTokenPrefix, StringComparison.Ordinal) + _ => _ is not null + && _.TrimStart().StartsWith(bearerTokenPrefix, StringComparison.Ordinal) ) ?.TrimStart() ?[bearerTokenPrefix.Length..] diff --git a/backend/src/Extensions/LinqExtensions.cs b/backend/src/Extensions/LinqExtensions.cs new file mode 100644 index 000000000..69ca73f94 --- /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 Metabase.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 fea856524..6fae7cb24 100644 --- a/backend/src/Extensions/NodaTimeExtensions.cs +++ b/backend/src/Extensions/NodaTimeExtensions.cs @@ -1,21 +1,31 @@ +using System.Diagnostics.Contracts; using NodaTime; namespace Metabase.Extensions; public static class NodaTimeExtensions { - extension(OffsetDateTime) + [Pure] + public static OffsetDateTime GetUtcNow(this IClock clock) { - public static OffsetDateTime UtcNow => - SystemClock.Instance - .GetCurrentInstant() - .WithOffset(Offset.Zero); + return clock.GetCurrentInstant().WithOffset(Offset.Zero); + } + [Pure] + public static int CompareTo(this OffsetDateTime current, OffsetDateTime other) + { + return OffsetDateTime.Comparer.Instant.Compare(current, other); + } + + extension(OffsetDateTime) + { + [Pure] public static bool operator >(OffsetDateTime x, OffsetDateTime y) { return OffsetDateTime.Comparer.Instant.Compare(x, y) > 0; } + [Pure] 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 62aeeb098..736499685 100644 --- a/backend/src/Extensions/StringExtensions.cs +++ b/backend/src/Extensions/StringExtensions.cs @@ -1,13 +1,48 @@ +using System; +using System.Text; +using System.Diagnostics.Contracts; + namespace Metabase.Extensions; public static class StringExtensions { - public static string FirstCharToLower(this string str) + [Pure] + 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..]; + } + + [Pure] + public static string? NullIfEmpty(this string value) + => string.IsNullOrEmpty(value) ? null : value; + + [Pure] + public static string? NullIfWhitespace(this string value) + => string.IsNullOrWhiteSpace(value) ? null : value; + + [Pure] + public static string Base64Encode(this string plainText) + { + return Convert.ToBase64String( + Encoding.UTF8.GetBytes(plainText) + ); + } + + [Pure] + public static string Base64Decode(this string base64EncodedData) + { + return Encoding.UTF8.GetString( + Convert.FromBase64String(base64EncodedData) + ); + } + + [Pure] + public static string Enquote(this string str) { - return string.IsNullOrEmpty(str) - || !char.IsLetter(str, 0) - || char.IsLower(str, 0) - ? str - : char.ToLowerInvariant(str[0]) + str[1..]; + return "\"" + str + "\""; } } \ No newline at end of file diff --git a/backend/src/GraphQl/Associations/AssociationType.cs b/backend/src/GraphQl/Associations/AssociationType.cs new file mode 100644 index 000000000..dc28a194e --- /dev/null +++ b/backend/src/GraphQl/Associations/AssociationType.cs @@ -0,0 +1,23 @@ +using HotChocolate.Types; +using Metabase.Data; +using Metabase.GraphQl.Scalars; + +namespace Metabase.GraphQl.Associations; + +public abstract class AssociationType + : ObjectType + where TAssociation : IAssociation +{ + protected override void Configure( + IObjectTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + // TODO Do we want to expose this, require it as input, and use it to discover concurrent writes? + descriptor + .Field(t => t.Version) + .Type>() + .Name(GraphQlConstants.VersionFieldName) + .Ignore(); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Associations/AuditableAssociationFilterType.cs b/backend/src/GraphQl/Associations/AuditableAssociationFilterType.cs new file mode 100644 index 000000000..943a82343 --- /dev/null +++ b/backend/src/GraphQl/Associations/AuditableAssociationFilterType.cs @@ -0,0 +1,19 @@ +using HotChocolate.Data.Filters; +using Metabase.Data; + +namespace Metabase.GraphQl.Associations; + +public abstract class AuditableAssociationFilterType + : FilterInputType + where TAssociation : IAssociation, IAuditable +{ + protected override void Configure( + IFilterInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.BindFieldsExplicitly(); + descriptor.Field(_ => _.CreatedAt); + descriptor.Field(_ => _.UpdatedAt); + } +} diff --git a/backend/src/GraphQl/Associations/AuditableAssociationSortType.cs b/backend/src/GraphQl/Associations/AuditableAssociationSortType.cs new file mode 100644 index 000000000..79972bfd0 --- /dev/null +++ b/backend/src/GraphQl/Associations/AuditableAssociationSortType.cs @@ -0,0 +1,19 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data; + +namespace Metabase.GraphQl.Associations; + +public abstract class AuditableAssociationSortType + : SortInputType + where TAssociation : IAssociation, IAuditable +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.BindFieldsExplicitly(); + descriptor.Field(_ => _.CreatedAt); + descriptor.Field(_ => _.UpdatedAt); + } +} diff --git a/backend/src/GraphQl/AuthorizedConnection.cs b/backend/src/GraphQl/AuthorizedConnection.cs index 4b057e95c..bc6ba6b6b 100644 --- a/backend/src/GraphQl/AuthorizedConnection.cs +++ b/backend/src/GraphQl/AuthorizedConnection.cs @@ -6,29 +6,29 @@ using System.Threading.Tasks; using GreenDonut; using GreenDonut.Data; +using HotChocolate.CostAnalysis.Types; using Metabase.Data; namespace Metabase.GraphQl; -public abstract class AuthorizedConnection( +public abstract class AuthorizedConnection( TSubject subject, Func createEdge, Func> isAuthorized, QueryContext queryContext -) : Connection(subject, createEdge, queryContext) +) : Connection(subject, createEdge, queryContext) where TSubject : IEntity - where TAssociationsByAssociateIdDataLoader : IDataLoader + where TAssociationsByOneIdDataLoader : IDataLoader { - private readonly Func> _isAuthorized = isAuthorized; - + [Cost(0)] public async IAsyncEnumerable GetEdgesAsync( ClaimsPrincipal claimsPrincipal, TAuthorization authorization, - TAssociationsByAssociateIdDataLoader dataLoader, + TAssociationsByOneIdDataLoader dataLoader, [EnumeratorCancellation] CancellationToken cancellationToken ) { - if (!await _isAuthorized(claimsPrincipal, Subject, authorization, cancellationToken)) + if (!await isAuthorized(claimsPrincipal, Subject, authorization, cancellationToken)) { yield break; } diff --git a/backend/src/GraphQl/AuthorizedPaginatedConnection.cs b/backend/src/GraphQl/AuthorizedPaginatedConnection.cs new file mode 100644 index 000000000..e7434ef1f --- /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 Metabase.Data; + +namespace Metabase.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/CalorimetricData.cs b/backend/src/GraphQl/CalorimetricDataX/CalorimetricData.cs new file mode 100644 index 000000000..65baf2dee --- /dev/null +++ b/backend/src/GraphQl/CalorimetricDataX/CalorimetricData.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate.Resolvers; +using HotChocolate.Types.Relay; +using Metabase.Data; +using Metabase.GraphQl.DataX; +using Metabase.GraphQl.Requests; +using NodaTime; + +namespace Metabase.GraphQl.CalorimetricDataX; + +[Node(IdField = nameof(Id))] +public sealed record CalorimetricData( + string Id, + Guid Uuid, + OffsetDateTime Timestamp, + string Locale, + Guid DatabaseId, + Guid ComponentId, + string? Name, + string? Description, + IReadOnlyList Warnings, + Guid CreatorId, + OffsetDateTime CreatedAt, + AppliedMethod AppliedMethod, + IReadOnlyList Resources, + GetHttpsResourceTree ResourceTree, + IReadOnlyList Approvals, + // ResponseApproval Approval + IReadOnlyList GValues, + IReadOnlyList uValues +) +: DataX.Data( + Id, + Uuid, + Timestamp, + Locale, + DatabaseId, + ComponentId, + Name, + Description, + Warnings, + CreatorId, + CreatedAt, + AppliedMethod, + Resources, + ResourceTree, + Approvals +) +{ + public override DataKind Kind { get => DataKind.CALORIMETRIC_DATA; } + + [NodeResolver] + public static Task GetAsync( + string id, + DataQueries dataQueries, + ApplicationDbContext databaseContext, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + return DataX.Data.FetchNodeAsync( + id, + (database, uuid, locale) => dataQueries.GetCalorimetricDataAsync( + database, + uuid, + locale, + resolverContext, + cancellationToken + ), + databaseContext, + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/CalorimetricDataConnection.cs b/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataConnection.cs similarity index 51% rename from backend/src/GraphQl/DataX/CalorimetricDataConnection.cs rename to backend/src/GraphQl/CalorimetricDataX/CalorimetricDataConnection.cs index def1f6f26..fb95f4a14 100644 --- a/backend/src/GraphQl/DataX/CalorimetricDataConnection.cs +++ b/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataConnection.cs @@ -1,14 +1,14 @@ -using System; using System.Collections.Generic; using HotChocolate.Types.Pagination; +using Metabase.GraphQl.DataX; -namespace Metabase.GraphQl.DataX; +namespace Metabase.GraphQl.CalorimetricDataX; public sealed record CalorimetricDataConnection( - IReadOnlyList Edges, - uint TotalCount, + IReadOnlyList? Edges, + int TotalCount, ConnectionPageInfo PageInfo -) : DataConnectionBase( +) : DataConnection( Edges, TotalCount, PageInfo diff --git a/backend/src/GraphQl/DataX/CalorimetricDataEdge.cs b/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataEdge.cs similarity index 50% rename from backend/src/GraphQl/DataX/CalorimetricDataEdge.cs rename to backend/src/GraphQl/CalorimetricDataX/CalorimetricDataEdge.cs index 53d5448b7..7e4b8b454 100644 --- a/backend/src/GraphQl/DataX/CalorimetricDataEdge.cs +++ b/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataEdge.cs @@ -1,9 +1,11 @@ -namespace Metabase.GraphQl.DataX; +using Metabase.GraphQl.DataX; + +namespace Metabase.GraphQl.CalorimetricDataX; public sealed record CalorimetricDataEdge( string Cursor, CalorimetricData Node -) : DataEdgeBase( +) : DataEdge( Cursor, Node ); \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/CalorimetricDataPropositionInput.cs b/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataPropositionInput.cs similarity index 66% rename from backend/src/GraphQl/DataX/CalorimetricDataPropositionInput.cs rename to backend/src/GraphQl/CalorimetricDataX/CalorimetricDataPropositionInput.cs index 53e04afcb..45d9b24f8 100644 --- a/backend/src/GraphQl/DataX/CalorimetricDataPropositionInput.cs +++ b/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataPropositionInput.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; +using Metabase.GraphQl.DataX; -namespace Metabase.GraphQl.DataX; +namespace Metabase.GraphQl.CalorimetricDataX; public sealed record CalorimetricDataPropositionInput( UuidPropositionInput? ComponentId, @@ -10,4 +11,8 @@ public sealed record CalorimetricDataPropositionInput( GetHttpsResourcesPropositionInput? Resources, FloatsPropositionInput? GValues, FloatsPropositionInput? UValues -); \ No newline at end of file +) +{ + public static readonly CalorimetricDataPropositionInput Empty = + new(null, null, null, null, null, null, null); +}; \ No newline at end of file diff --git a/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataQueries.cs b/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataQueries.cs new file mode 100644 index 000000000..3eb4904de --- /dev/null +++ b/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataQueries.cs @@ -0,0 +1,105 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.CostAnalysis.Types; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using Metabase.Data; +using Metabase.GraphQl.Requests; +using Metabase.GraphQl.Scalars; +using Microsoft.EntityFrameworkCore; + +namespace Metabase.GraphQl.CalorimetricDataX; + +[ExtendObjectType(nameof(Query))] +public sealed class CalorimetricDataQueries +{ + public async Task GetCalorimetricDataAsync( + Guid databaseId, + Guid id, + [GraphQLType] string? locale, + DataQueries dataQueries, + ApplicationDbContext databaseContext, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + var database = await databaseContext.Databases.AsNoTracking() + .Where(_ => _.Id == databaseId) + .SingleOrDefaultAsync(cancellationToken); + if (database is null) + { + return null; + } + return await dataQueries.GetCalorimetricDataAsync( + database, + id, + locale, + resolverContext, + cancellationToken + ); + } + + [ListSize( + AssumedSize = (int)GraphQlConstants.MaximumPageSize - 1, + SlicingArguments = ["first", "last"], + SlicingArgumentDefaultValue = (int)GraphQlConstants.MaximumPageSize - 1, + SizedFields = ["edges", "nodes"], + RequireOneSlicingArgument = false + )] + public Task GetAllCalorimetricDataAsync( + CalorimetricDataPropositionInput? where, + [GraphQLType] string? locale, + int? first, + string? after, + int? last, + string? before, + DataQueries dataQueries, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + return dataQueries.GetAllDataAsync( + first, + after, + last, + before, + (edges, totalCount, pageInfo) => new CalorimetricDataConnection(edges, totalCount, pageInfo), + (node, cursor) => new CalorimetricDataEdge(cursor, node), + (database, first, after, last, before) => dataQueries.GetAllCalorimetricDataAsync( + database, + where, + locale, + first, + after, + last, + before, + resolverContext, + cancellationToken + ), + cancellationToken + ); + } + + public Task HasCalorimetricDataAsync( + CalorimetricDataPropositionInput? where, + [GraphQLType] string? locale, + DataQueries dataQueries, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + return dataQueries.HasDataAsync( + (database) => dataQueries.HasCalorimetricDataAsync( + database, + where, + locale, + resolverContext, + cancellationToken + ), + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Common/OpenEndedDateTimeRangeType.cs b/backend/src/GraphQl/Common/OpenEndedDateTimeRangeType.cs index 25f45a9d7..c2a31c0f7 100644 --- a/backend/src/GraphQl/Common/OpenEndedDateTimeRangeType.cs +++ b/backend/src/GraphQl/Common/OpenEndedDateTimeRangeType.cs @@ -1,6 +1,8 @@ using HotChocolate.Types; +using HotChocolate.Types.NodaTime; using NodaTime; using NpgsqlTypes; +using DateTimeType = HotChocolate.Types.NodaTime.DateTimeType; namespace Metabase.GraphQl.Common; @@ -19,6 +21,7 @@ IObjectTypeDescriptor> descriptor descriptor .Field("from") .Type() + .Cost(0) .Resolve(context => { var range = context.Parent>(); @@ -31,6 +34,7 @@ IObjectTypeDescriptor> descriptor descriptor .Field("to") .Type() + .Cost(0) .Resolve(context => { var range = context.Parent>(); diff --git a/backend/src/GraphQl/ComponentAssemblies/ComponentAssemblyFilterType.cs b/backend/src/GraphQl/ComponentAssemblies/ComponentAssemblyFilterType.cs index 44ac2bbb2..394a3ad97 100644 --- a/backend/src/GraphQl/ComponentAssemblies/ComponentAssemblyFilterType.cs +++ b/backend/src/GraphQl/ComponentAssemblies/ComponentAssemblyFilterType.cs @@ -1,19 +1,23 @@ using HotChocolate.Data.Filters; using Metabase.Data; +using Metabase.GraphQl.Associations; namespace Metabase.GraphQl.ComponentAssemblies; public abstract class ComponentAssemblyFilterType - : FilterInputType + : AuditableAssociationFilterType { protected override void Configure( IFilterInputTypeDescriptor descriptor ) { - descriptor.BindFieldsExplicitly(); - descriptor.Field(x => x.AssembledComponent); - descriptor.Field(x => x.PartComponent); - descriptor.Field(x => x.Index); - descriptor.Field(x => x.PrimeSurface); + base.Configure(descriptor); + // TODO Remove CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(_ => _.CreatedAt); + descriptor.Field(_ => _.UpdatedAt); + descriptor.Field(_ => _.AssembledComponent); + descriptor.Field(_ => _.PartComponent); + descriptor.Field(_ => _.Index); + descriptor.Field(_ => _.PrimeSurface); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/ComponentAssemblies/ComponentAssemblySortType.cs b/backend/src/GraphQl/ComponentAssemblies/ComponentAssemblySortType.cs index 043174587..28afee34e 100644 --- a/backend/src/GraphQl/ComponentAssemblies/ComponentAssemblySortType.cs +++ b/backend/src/GraphQl/ComponentAssemblies/ComponentAssemblySortType.cs @@ -1,19 +1,18 @@ using HotChocolate.Data.Sorting; using Metabase.Data; +using Metabase.GraphQl.Associations; namespace Metabase.GraphQl.ComponentAssemblies; -public sealed class ComponentAssemblySortType - : SortInputType +public abstract class ComponentAssemblySortType + : AuditableAssociationSortType { protected override void Configure( ISortInputTypeDescriptor descriptor ) { - descriptor.BindFieldsExplicitly(); - descriptor.Field(x => x.AssembledComponent); - descriptor.Field(x => x.PartComponent); - descriptor.Field(x => x.Index); - descriptor.Field(x => x.PrimeSurface); + base.Configure(descriptor); + descriptor.Field(_ => _.Index); + descriptor.Field(_ => _.PrimeSurface); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/ComponentAssemblies/RemoveComponentAssemblyPayload.cs b/backend/src/GraphQl/ComponentAssemblies/RemoveComponentAssemblyPayload.cs index 1a266b63a..625de0df9 100644 --- a/backend/src/GraphQl/ComponentAssemblies/RemoveComponentAssemblyPayload.cs +++ b/backend/src/GraphQl/ComponentAssemblies/RemoveComponentAssemblyPayload.cs @@ -34,7 +34,7 @@ RemoveComponentAssemblyError error public IReadOnlyCollection? Errors { get; } public async Task GetAssembledComponentAsync( - ComponentByIdDataLoader byId, + IComponentByIdDataLoader byId, CancellationToken cancellationToken ) { @@ -46,7 +46,7 @@ CancellationToken cancellationToken } public async Task GetPartComponentAsync( - ComponentByIdDataLoader byId, + IComponentByIdDataLoader byId, CancellationToken cancellationToken ) { diff --git a/backend/src/GraphQl/ComponentGeneralizations/ComponentConcretizationAndGeneralizationFilterType.cs b/backend/src/GraphQl/ComponentGeneralizations/ComponentConcretizationAndGeneralizationFilterType.cs index d73906a32..9346588dc 100644 --- a/backend/src/GraphQl/ComponentGeneralizations/ComponentConcretizationAndGeneralizationFilterType.cs +++ b/backend/src/GraphQl/ComponentGeneralizations/ComponentConcretizationAndGeneralizationFilterType.cs @@ -1,17 +1,21 @@ using HotChocolate.Data.Filters; using Metabase.Data; +using Metabase.GraphQl.Associations; namespace Metabase.GraphQl.ComponentGeneralizations; public abstract class ComponentConcretizationAndGeneralizationFilterType - : FilterInputType + : AuditableAssociationFilterType { protected override void Configure( IFilterInputTypeDescriptor descriptor ) { - descriptor.BindFieldsExplicitly(); - descriptor.Field(x => x.ConcreteComponent); - descriptor.Field(x => x.GeneralComponent); + base.Configure(descriptor); + // TODO Remove CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(_ => _.CreatedAt); + descriptor.Field(_ => _.UpdatedAt); + descriptor.Field(_ => _.ConcreteComponent); + descriptor.Field(_ => _.GeneralComponent); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/ComponentGeneralizations/ComponentConcretizationAndGeneralizationSortType.cs b/backend/src/GraphQl/ComponentGeneralizations/ComponentConcretizationAndGeneralizationSortType.cs new file mode 100644 index 000000000..051170ab2 --- /dev/null +++ b/backend/src/GraphQl/ComponentGeneralizations/ComponentConcretizationAndGeneralizationSortType.cs @@ -0,0 +1,16 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data; +using Metabase.GraphQl.Associations; + +namespace Metabase.GraphQl.ComponentGeneralizations; + +public abstract class ComponentConcretizationAndGeneralizationSortType + : AuditableAssociationSortType +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/ComponentGeneralizations/RemoveComponentGeneralizationPayload.cs b/backend/src/GraphQl/ComponentGeneralizations/RemoveComponentGeneralizationPayload.cs index 360ef877e..89170d30f 100644 --- a/backend/src/GraphQl/ComponentGeneralizations/RemoveComponentGeneralizationPayload.cs +++ b/backend/src/GraphQl/ComponentGeneralizations/RemoveComponentGeneralizationPayload.cs @@ -34,7 +34,7 @@ RemoveComponentGeneralizationError error public IReadOnlyCollection? Errors { get; } public async Task GetGeneralComponentAsync( - ComponentByIdDataLoader byId, + IComponentByIdDataLoader byId, CancellationToken cancellationToken ) { @@ -46,7 +46,7 @@ CancellationToken cancellationToken } public async Task GetConcreteComponentAsync( - ComponentByIdDataLoader byId, + IComponentByIdDataLoader byId, CancellationToken cancellationToken ) { diff --git a/backend/src/GraphQl/ComponentManufacturers/AddComponentManufacturerPayload.cs b/backend/src/GraphQl/ComponentManufacturers/AddComponentManufacturerPayload.cs index 81b5b2aa0..bd9f8f7eb 100644 --- a/backend/src/GraphQl/ComponentManufacturers/AddComponentManufacturerPayload.cs +++ b/backend/src/GraphQl/ComponentManufacturers/AddComponentManufacturerPayload.cs @@ -11,7 +11,10 @@ public AddComponentManufacturerPayload( ComponentManufacturer componentManufacturer ) { - ManufacturedComponentEdge = new InstitutionManufacturedComponentEdge(componentManufacturer); + ManufacturedComponentEdge = new InstitutionManufacturedComponentEdge( + componentManufacturer, + PaginationHelpers.ConstructCursor(componentManufacturer.InstitutionId, componentManufacturer.ComponentId) + ); ComponentManufacturerEdge = new ComponentManufacturerEdge(componentManufacturer); } diff --git a/backend/src/GraphQl/ComponentManufacturers/ComponentManufacturerFilterType.cs b/backend/src/GraphQl/ComponentManufacturers/ComponentManufacturerFilterType.cs index a8decbe87..aeccc8065 100644 --- a/backend/src/GraphQl/ComponentManufacturers/ComponentManufacturerFilterType.cs +++ b/backend/src/GraphQl/ComponentManufacturers/ComponentManufacturerFilterType.cs @@ -1,17 +1,21 @@ using HotChocolate.Data.Filters; using Metabase.Data; +using Metabase.GraphQl.Associations; namespace Metabase.GraphQl.ComponentManufacturers; public abstract class ComponentManufacturerFilterType - : FilterInputType + : AuditableAssociationFilterType { protected override void Configure( IFilterInputTypeDescriptor descriptor ) { - descriptor.BindFieldsExplicitly(); - descriptor.Field(x => x.Component); - descriptor.Field(x => x.Institution); + base.Configure(descriptor); + // TODO Remove CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(_ => _.CreatedAt); + descriptor.Field(_ => _.UpdatedAt); + descriptor.Field(_ => _.Component); + descriptor.Field(_ => _.Institution); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/ComponentManufacturers/ComponentManufacturerMutations.cs b/backend/src/GraphQl/ComponentManufacturers/ComponentManufacturerMutations.cs index 8c4eca552..337895967 100644 --- a/backend/src/GraphQl/ComponentManufacturers/ComponentManufacturerMutations.cs +++ b/backend/src/GraphQl/ComponentManufacturers/ComponentManufacturerMutations.cs @@ -9,7 +9,6 @@ using Metabase.Data; using Metabase.Extensions; using Metabase.GraphQl.Users; -using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; namespace Metabase.GraphQl.ComponentManufacturers; @@ -59,9 +58,9 @@ CancellationToken cancellationToken } if (!await context.Institutions.AsQueryable() - .Where(c => c.Id == input.InstitutionId) - .AnyAsync(cancellationToken) - ) + .Where(_ => _.Id == input.InstitutionId) + .AnyAsync(cancellationToken) + ) { errors.Add( new AddComponentManufacturerError( diff --git a/backend/src/GraphQl/ComponentManufacturers/ComponentManufacturerSortType.cs b/backend/src/GraphQl/ComponentManufacturers/ComponentManufacturerSortType.cs index 09cb4e334..b17d28d40 100644 --- a/backend/src/GraphQl/ComponentManufacturers/ComponentManufacturerSortType.cs +++ b/backend/src/GraphQl/ComponentManufacturers/ComponentManufacturerSortType.cs @@ -1,17 +1,16 @@ using HotChocolate.Data.Sorting; using Metabase.Data; +using Metabase.GraphQl.Associations; namespace Metabase.GraphQl.ComponentManufacturers; -public sealed class ComponentManufacturerSortType - : SortInputType +public abstract class ComponentManufacturerSortType + : AuditableAssociationSortType { protected override void Configure( ISortInputTypeDescriptor descriptor ) { - descriptor.BindFieldsExplicitly(); - descriptor.Field(x => x.Component); - descriptor.Field(x => x.Institution); + base.Configure(descriptor); } } \ No newline at end of file diff --git a/backend/src/GraphQl/ComponentManufacturers/ConfirmComponentManufacturerPayload.cs b/backend/src/GraphQl/ComponentManufacturers/ConfirmComponentManufacturerPayload.cs index cfffe9836..0384dd1e3 100644 --- a/backend/src/GraphQl/ComponentManufacturers/ConfirmComponentManufacturerPayload.cs +++ b/backend/src/GraphQl/ComponentManufacturers/ConfirmComponentManufacturerPayload.cs @@ -11,7 +11,10 @@ public ConfirmComponentManufacturerPayload( ComponentManufacturer componentManufacturer ) { - ManufacturedComponentEdge = new InstitutionManufacturedComponentEdge(componentManufacturer); + ManufacturedComponentEdge = new InstitutionManufacturedComponentEdge( + componentManufacturer, + PaginationHelpers.ConstructCursor(componentManufacturer.InstitutionId, componentManufacturer.ComponentId) + ); ComponentManufacturerEdge = new ComponentManufacturerEdge(componentManufacturer); } diff --git a/backend/src/GraphQl/ComponentManufacturers/RemoveComponentManufacturerPayload.cs b/backend/src/GraphQl/ComponentManufacturers/RemoveComponentManufacturerPayload.cs index c03d53fd7..4d35ca435 100644 --- a/backend/src/GraphQl/ComponentManufacturers/RemoveComponentManufacturerPayload.cs +++ b/backend/src/GraphQl/ComponentManufacturers/RemoveComponentManufacturerPayload.cs @@ -35,7 +35,7 @@ RemoveComponentManufacturerError error public IReadOnlyCollection? Errors { get; } public async Task GetComponentAsync( - ComponentByIdDataLoader byId, + IComponentByIdDataLoader byId, CancellationToken cancellationToken ) { @@ -47,7 +47,7 @@ CancellationToken cancellationToken } public async Task GetInstitutionAsync( - InstitutionByIdDataLoader byId, + IInstitutionByIdDataLoader byId, CancellationToken cancellationToken ) { diff --git a/backend/src/GraphQl/ComponentVariants/ComponentVariantFilterType.cs b/backend/src/GraphQl/ComponentVariants/ComponentVariantFilterType.cs index 49f359370..d5d102a36 100644 --- a/backend/src/GraphQl/ComponentVariants/ComponentVariantFilterType.cs +++ b/backend/src/GraphQl/ComponentVariants/ComponentVariantFilterType.cs @@ -1,17 +1,21 @@ using HotChocolate.Data.Filters; using Metabase.Data; +using Metabase.GraphQl.Associations; namespace Metabase.GraphQl.ComponentVariants; public abstract class ComponentVariantFilterType - : FilterInputType + : AuditableAssociationFilterType { protected override void Configure( IFilterInputTypeDescriptor descriptor ) { - descriptor.BindFieldsExplicitly(); - descriptor.Field(x => x.OfComponent); - descriptor.Field(x => x.ToComponent); + base.Configure(descriptor); + // TODO Remove CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(_ => _.CreatedAt); + descriptor.Field(_ => _.UpdatedAt); + descriptor.Field(_ => _.OfComponent); + descriptor.Field(_ => _.ToComponent); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/ComponentVariants/ComponentVariantSortType.cs b/backend/src/GraphQl/ComponentVariants/ComponentVariantSortType.cs new file mode 100644 index 000000000..e6414fbd9 --- /dev/null +++ b/backend/src/GraphQl/ComponentVariants/ComponentVariantSortType.cs @@ -0,0 +1,16 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data; +using Metabase.GraphQl.Associations; + +namespace Metabase.GraphQl.ComponentVariants; + +public abstract class ComponentVariantSortType + : AuditableAssociationSortType +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/ComponentVariants/RemoveComponentVariantPayload.cs b/backend/src/GraphQl/ComponentVariants/RemoveComponentVariantPayload.cs index 72a4840dd..539244a1f 100644 --- a/backend/src/GraphQl/ComponentVariants/RemoveComponentVariantPayload.cs +++ b/backend/src/GraphQl/ComponentVariants/RemoveComponentVariantPayload.cs @@ -38,7 +38,7 @@ RemoveComponentVariantError error public IReadOnlyCollection? Errors { get; } public async Task GetOneComponentAsync( - ComponentByIdDataLoader byId, + IComponentByIdDataLoader byId, CancellationToken cancellationToken ) { @@ -50,7 +50,7 @@ CancellationToken cancellationToken } public async Task GetOtherComponentAsync( - ComponentByIdDataLoader byId, + IComponentByIdDataLoader byId, CancellationToken cancellationToken ) { diff --git a/backend/src/GraphQl/Components/ComponentAssembledOfConnection.cs b/backend/src/GraphQl/Components/ComponentAssembledOfConnection.cs index 4eee58a86..300099542 100644 --- a/backend/src/GraphQl/Components/ComponentAssembledOfConnection.cs +++ b/backend/src/GraphQl/Components/ComponentAssembledOfConnection.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using GreenDonut.Data; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Users; @@ -11,15 +12,15 @@ namespace Metabase.GraphQl.Components; public sealed class ComponentAssembledOfConnection( Component subject, QueryContext queryContext - ) - : Connection( - subject, - x => new ComponentAssembledOfEdge(x), - queryContext - ) +) +: Connection( + subject, + x => new ComponentAssembledOfEdge(x), + queryContext +) { [UseUserManager] + [Cost(1)] public Task IsAuthorizedToAddEdgeAsync( ClaimsPrincipal claimsPrincipal, ComponentAssemblyAuthorization authorization, diff --git a/backend/src/GraphQl/Components/ComponentAssembledOfEdge.cs b/backend/src/GraphQl/Components/ComponentAssembledOfEdge.cs index e142770d1..82662655f 100644 --- a/backend/src/GraphQl/Components/ComponentAssembledOfEdge.cs +++ b/backend/src/GraphQl/Components/ComponentAssembledOfEdge.cs @@ -1,26 +1,25 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.Enumerations; using Metabase.GraphQl.Users; -using Microsoft.AspNetCore.Identity; namespace Metabase.GraphQl.Components; public sealed class ComponentAssembledOfEdge( ComponentAssembly association - ) - : Edge(association.PartComponentId) +) +: Edge(association.PartComponentId) { - private readonly ComponentAssembly _association = association; - - public byte? Index => _association.Index; + public byte? Index => association.Index; - public PrimeSurface? PrimeSurface => _association.PrimeSurface; + public PrimeSurface? PrimeSurface => association.PrimeSurface; [UseUserManager] + [Cost(1)] public Task IsAuthorizedToUpdateEdgeAsync( ClaimsPrincipal claimsPrincipal, ComponentAssemblyAuthorization authorization, @@ -29,13 +28,14 @@ CancellationToken cancellationToken { return authorization.IsAuthorizedToManage( claimsPrincipal, - _association.AssembledComponentId, - _association.PartComponentId, + association.AssembledComponentId, + association.PartComponentId, cancellationToken ); } [UseUserManager] + [Cost(1)] public Task IsAuthorizedToRemoveEdgeAsync( ClaimsPrincipal claimsPrincipal, ComponentAssemblyAuthorization authorization, @@ -44,8 +44,8 @@ CancellationToken cancellationToken { return authorization.IsAuthorizedToManage( claimsPrincipal, - _association.AssembledComponentId, - _association.PartComponentId, + association.AssembledComponentId, + association.PartComponentId, cancellationToken ); } diff --git a/backend/src/GraphQl/Components/ComponentAssembledOfFilterType.cs b/backend/src/GraphQl/Components/ComponentAssembledOfFilterType.cs index d6c499537..18176f4a3 100644 --- a/backend/src/GraphQl/Components/ComponentAssembledOfFilterType.cs +++ b/backend/src/GraphQl/Components/ComponentAssembledOfFilterType.cs @@ -1,5 +1,4 @@ using HotChocolate.Data.Filters; -using Metabase.Configuration; using Metabase.Data; namespace Metabase.GraphQl.Components; @@ -13,6 +12,6 @@ IFilterInputTypeDescriptor descriptor { base.Configure(descriptor); descriptor.Name(nameof(ComponentAssembledOfFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); - descriptor.Field(x => x.AssembledComponent).Ignore(); + descriptor.Field(_ => _.AssembledComponent).Ignore(); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Components/ComponentAssembledOfSortType.cs b/backend/src/GraphQl/Components/ComponentAssembledOfSortType.cs new file mode 100644 index 000000000..6f03dddf1 --- /dev/null +++ b/backend/src/GraphQl/Components/ComponentAssembledOfSortType.cs @@ -0,0 +1,16 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data; + +namespace Metabase.GraphQl.Components; + +public sealed class ComponentAssembledOfSortType + : ComponentAssemblies.ComponentAssemblySortType +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.Name(nameof(ComponentAssembledOfSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Components/ComponentByIdDataLoader.cs b/backend/src/GraphQl/Components/ComponentByIdDataLoader.cs deleted file mode 100644 index e4425f177..000000000 --- a/backend/src/GraphQl/Components/ComponentByIdDataLoader.cs +++ /dev/null @@ -1,20 +0,0 @@ -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Components; - -public sealed class ComponentByIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : EntityByIdDataLoader( - batchScheduler, - options, - dbContextFactory, - dbContext => dbContext.Components - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Components/ComponentConcretizationOfConnection.cs b/backend/src/GraphQl/Components/ComponentConcretizationOfConnection.cs index 4e61c7ceb..3d40a7880 100644 --- a/backend/src/GraphQl/Components/ComponentConcretizationOfConnection.cs +++ b/backend/src/GraphQl/Components/ComponentConcretizationOfConnection.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using GreenDonut.Data; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Users; @@ -12,14 +13,14 @@ public sealed class ComponentConcretizationOfConnection( Component subject, QueryContext queryContext ) - : Connection( + : Connection( subject, x => new ComponentConcretizationOfEdge(x), queryContext ) { [UseUserManager] + [Cost(1)] public Task IsAuthorizedToAddEdgeAsync( ClaimsPrincipal claimsPrincipal, ComponentGeneralizationAuthorization authorization, diff --git a/backend/src/GraphQl/Components/ComponentConcretizationOfEdge.cs b/backend/src/GraphQl/Components/ComponentConcretizationOfEdge.cs index c7e56d366..3527216f1 100644 --- a/backend/src/GraphQl/Components/ComponentConcretizationOfEdge.cs +++ b/backend/src/GraphQl/Components/ComponentConcretizationOfEdge.cs @@ -1,21 +1,20 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Users; -using Microsoft.AspNetCore.Identity; namespace Metabase.GraphQl.Components; public sealed class ComponentConcretizationOfEdge( ComponentConcretizationAndGeneralization association ) - : Edge(association.GeneralComponentId) + : Edge(association.GeneralComponentId) { - private readonly ComponentConcretizationAndGeneralization _association = association; - [UseUserManager] + [Cost(1)] public Task IsAuthorizedToRemoveEdgeAsync( ClaimsPrincipal claimsPrincipal, ComponentGeneralizationAuthorization authorization, @@ -24,8 +23,8 @@ CancellationToken cancellationToken { return authorization.IsAuthorizedToManage( claimsPrincipal, - _association.GeneralComponentId, - _association.ConcreteComponentId, + association.GeneralComponentId, + association.ConcreteComponentId, cancellationToken ); } diff --git a/backend/src/GraphQl/Components/ComponentConcretizationOfFilterType.cs b/backend/src/GraphQl/Components/ComponentConcretizationOfFilterType.cs index 9d11f3f65..9cfe2444a 100644 --- a/backend/src/GraphQl/Components/ComponentConcretizationOfFilterType.cs +++ b/backend/src/GraphQl/Components/ComponentConcretizationOfFilterType.cs @@ -1,5 +1,4 @@ using HotChocolate.Data.Filters; -using Metabase.Configuration; using Metabase.Data; namespace Metabase.GraphQl.Components; @@ -13,6 +12,6 @@ IFilterInputTypeDescriptor descriptor { base.Configure(descriptor); descriptor.Name(nameof(ComponentConcretizationOfFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); - descriptor.Field(x => x.ConcreteComponent).Ignore(); + descriptor.Field(_ => _.ConcreteComponent).Ignore(); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Components/ComponentConcretizationOfSortType.cs b/backend/src/GraphQl/Components/ComponentConcretizationOfSortType.cs new file mode 100644 index 000000000..8b9ce68fc --- /dev/null +++ b/backend/src/GraphQl/Components/ComponentConcretizationOfSortType.cs @@ -0,0 +1,16 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data; + +namespace Metabase.GraphQl.Components; + +public sealed class ComponentConcretizationOfSortType + : ComponentGeneralizations.ComponentConcretizationAndGeneralizationSortType +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.Name(nameof(ComponentConcretizationOfSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Components/ComponentConcretizationsByComponentIdDataLoader.cs b/backend/src/GraphQl/Components/ComponentConcretizationsByComponentIdDataLoader.cs deleted file mode 100644 index 27f7ab270..000000000 --- a/backend/src/GraphQl/Components/ComponentConcretizationsByComponentIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Components; - -public sealed class ComponentConcretizationsByComponentIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.ComponentConcretizationAndGeneralizations.AsNoTracking().Where(x => - ids.Contains(x.GeneralComponentId) - ).With(queryContext), - x => x.GeneralComponentId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Components/ComponentDataLoaders.cs b/backend/src/GraphQl/Components/ComponentDataLoaders.cs new file mode 100644 index 000000000..c37a169c1 --- /dev/null +++ b/backend/src/GraphQl/Components/ComponentDataLoaders.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut; +using GreenDonut.Data; +using Metabase.Data; +using Microsoft.EntityFrameworkCore; + +namespace Metabase.GraphQl.Components; + +public sealed class ComponentDataLoaders +: DataLoaders +{ + [DataLoader] + public static ValueTask> GetComponentByIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetEntityByIdAsync( + ids, + databaseContext => databaseContext.Components, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask> GetComponentManufacturersByComponentIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetAssociationsByOneIdAsync( + ids, + (databaseContext) => databaseContext.ComponentManufacturers.Where(_ => !_.Pending), + _ => _.ComponentId, + _ => _.InstitutionId, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask> GetPendingComponentManufacturersByComponentIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetAssociationsByOneIdAsync( + ids, + (databaseContext) => databaseContext.ComponentManufacturers.Where(_ => _.Pending), + _ => _.ComponentId, + _ => _.InstitutionId, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask> GetComponentPartOfByComponentIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetAssociationsByOneIdAsync( + ids, + (databaseContext) => databaseContext.ComponentAssemblies, + _ => _.PartComponentId, + _ => _.AssembledComponentId, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask> GetComponentAssembledOfByComponentIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetAssociationsByOneIdAsync( + ids, + (databaseContext) => databaseContext.ComponentAssemblies, + _ => _.AssembledComponentId, + _ => _.PartComponentId, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask> GetComponentVariantOfByComponentIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetAssociationsByOneIdAsync( + ids, + (databaseContext) => databaseContext.ComponentVariants, + _ => _.ToComponentId, + _ => _.OfComponentId, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask> GetComponentConcretizationsByComponentIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetAssociationsByOneIdAsync( + ids, + (databaseContext) => databaseContext.ComponentConcretizationAndGeneralizations, + _ => _.GeneralComponentId, + _ => _.ConcreteComponentId, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask> GetComponentGeneralizationsByComponentIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetAssociationsByOneIdAsync( + ids, + (databaseContext) => databaseContext.ComponentConcretizationAndGeneralizations, + _ => _.ConcreteComponentId, + _ => _.GeneralComponentId, + queryContext, + databaseContextFactory, + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Components/ComponentFilterType.cs b/backend/src/GraphQl/Components/ComponentFilterType.cs index 793e58f58..66ed839a4 100644 --- a/backend/src/GraphQl/Components/ComponentFilterType.cs +++ b/backend/src/GraphQl/Components/ComponentFilterType.cs @@ -5,28 +5,32 @@ namespace Metabase.GraphQl.Components; public class ComponentFilterType - : EntityFilterType + : AuditableEntityFilterType { protected override void Configure( IFilterInputTypeDescriptor descriptor ) { base.Configure(descriptor); - descriptor.Field(x => x.Name); - descriptor.Field(x => x.Abbreviation); - descriptor.Field(x => x.Description); - descriptor.Field(x => x.Categories); - descriptor.Field(x => x.Extras); - descriptor.Field(x => x.PartOf); - descriptor.Field(x => x.Parts); - descriptor.Field(x => x.PartOfEdges); - descriptor.Field(x => x.PartEdges); - descriptor.Field(x => x.Concretizations); - descriptor.Field(x => x.Generalizations); - descriptor.Field(x => x.Variants); - descriptor.Field(x => x.Manager); - descriptor.Field(x => x.Manufacturers); - descriptor.Field(x => x.ManufacturerEdges); + // TODO Remove Id, CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(_ => _.Id); + descriptor.Field(_ => _.CreatedAt); + descriptor.Field(_ => _.UpdatedAt); + descriptor.Field(_ => _.Name); + descriptor.Field(_ => _.Abbreviation); + descriptor.Field(_ => _.Description); + descriptor.Field(_ => _.Categories); + descriptor.Field(_ => _.Extras); + descriptor.Field(_ => _.PartOf); + descriptor.Field(_ => _.Parts); + descriptor.Field(_ => _.PartOfEdges); + descriptor.Field(_ => _.PartEdges); + descriptor.Field(_ => _.Concretizations); + descriptor.Field(_ => _.Generalizations); + descriptor.Field(_ => _.Variants); + descriptor.Field(_ => _.Manager); + descriptor.Field(_ => _.Manufacturers); + descriptor.Field(_ => _.ManufacturerEdges); // TODO Allow filtering by Availability. How? See https://chillicream.com/docs/hotchocolate/fetching-data/filtering/#customization } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Components/ComponentGeneralizationOfConnection.cs b/backend/src/GraphQl/Components/ComponentGeneralizationOfConnection.cs index df51c1ed5..daecb5558 100644 --- a/backend/src/GraphQl/Components/ComponentGeneralizationOfConnection.cs +++ b/backend/src/GraphQl/Components/ComponentGeneralizationOfConnection.cs @@ -2,10 +2,10 @@ using System.Threading; using System.Threading.Tasks; using GreenDonut.Data; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Users; -using Microsoft.AspNetCore.Identity; namespace Metabase.GraphQl.Components; @@ -13,14 +13,14 @@ public sealed class ComponentGeneralizationOfConnection( Component subject, QueryContext queryContext ) - : Connection( + : Connection( subject, x => new ComponentGeneralizationOfEdge(x), queryContext ) { [UseUserManager] + [Cost(1)] public Task IsAuthorizedToAddEdgeAsync( ClaimsPrincipal claimsPrincipal, ComponentGeneralizationAuthorization authorization, diff --git a/backend/src/GraphQl/Components/ComponentGeneralizationOfEdge.cs b/backend/src/GraphQl/Components/ComponentGeneralizationOfEdge.cs index 569951c2c..b05c756c5 100644 --- a/backend/src/GraphQl/Components/ComponentGeneralizationOfEdge.cs +++ b/backend/src/GraphQl/Components/ComponentGeneralizationOfEdge.cs @@ -1,21 +1,20 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Users; -using Microsoft.AspNetCore.Identity; namespace Metabase.GraphQl.Components; public sealed class ComponentGeneralizationOfEdge( ComponentConcretizationAndGeneralization association - ) - : Edge(association.ConcreteComponentId) +) +: Edge(association.ConcreteComponentId) { - private readonly ComponentConcretizationAndGeneralization _association = association; - [UseUserManager] + [Cost(1)] public Task IsAuthorizedToRemoveEdgeAsync( ClaimsPrincipal claimsPrincipal, ComponentGeneralizationAuthorization authorization, @@ -24,8 +23,8 @@ CancellationToken cancellationToken { return authorization.IsAuthorizedToManage( claimsPrincipal, - _association.GeneralComponentId, - _association.ConcreteComponentId, + association.GeneralComponentId, + association.ConcreteComponentId, cancellationToken ); } diff --git a/backend/src/GraphQl/Components/ComponentGeneralizationOfFilterType.cs b/backend/src/GraphQl/Components/ComponentGeneralizationOfFilterType.cs index 4a3b1d5a3..e71b9e3b1 100644 --- a/backend/src/GraphQl/Components/ComponentGeneralizationOfFilterType.cs +++ b/backend/src/GraphQl/Components/ComponentGeneralizationOfFilterType.cs @@ -1,5 +1,4 @@ using HotChocolate.Data.Filters; -using Metabase.Configuration; using Metabase.Data; namespace Metabase.GraphQl.Components; @@ -13,6 +12,6 @@ IFilterInputTypeDescriptor descriptor { base.Configure(descriptor); descriptor.Name(nameof(ComponentGeneralizationOfFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); - descriptor.Field(x => x.GeneralComponent).Ignore(); + descriptor.Field(_ => _.GeneralComponent).Ignore(); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Components/ComponentGeneralizationOfSortType.cs b/backend/src/GraphQl/Components/ComponentGeneralizationOfSortType.cs new file mode 100644 index 000000000..c59f58ede --- /dev/null +++ b/backend/src/GraphQl/Components/ComponentGeneralizationOfSortType.cs @@ -0,0 +1,16 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data; + +namespace Metabase.GraphQl.Components; + +public sealed class ComponentGeneralizationOfSortType + : ComponentGeneralizations.ComponentConcretizationAndGeneralizationSortType +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.Name(nameof(ComponentGeneralizationOfSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Components/ComponentGeneralizationsByComponentIdDataLoader.cs b/backend/src/GraphQl/Components/ComponentGeneralizationsByComponentIdDataLoader.cs deleted file mode 100644 index 5352d6b63..000000000 --- a/backend/src/GraphQl/Components/ComponentGeneralizationsByComponentIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Components; - -public sealed class ComponentGeneralizationsByComponentIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.ComponentConcretizationAndGeneralizations.AsNoTracking().Where(x => - ids.Contains(x.ConcreteComponentId) - ).With(queryContext), - x => x.ConcreteComponentId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Components/ComponentManagerEdge.cs b/backend/src/GraphQl/Components/ComponentManagerEdge.cs index 4210013a5..7d7d4d059 100644 --- a/backend/src/GraphQl/Components/ComponentManagerEdge.cs +++ b/backend/src/GraphQl/Components/ComponentManagerEdge.cs @@ -5,7 +5,7 @@ namespace Metabase.GraphQl.Components; public sealed class ComponentManagerEdge( Component association - ) - : Edge(association.ManagerId) +) +: Edge(association.ManagerId) { -} +} \ No newline at end of file diff --git a/backend/src/GraphQl/Components/ComponentManufacturerConnection.cs b/backend/src/GraphQl/Components/ComponentManufacturerConnection.cs index e3ba6710c..eb43a9649 100644 --- a/backend/src/GraphQl/Components/ComponentManufacturerConnection.cs +++ b/backend/src/GraphQl/Components/ComponentManufacturerConnection.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using GreenDonut.Data; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Users; @@ -11,14 +12,15 @@ namespace Metabase.GraphQl.Components; public sealed class ComponentManufacturerConnection( Component subject, QueryContext queryContext - ) - : Connection( - subject, - x => new ComponentManufacturerEdge(x), - queryContext - ) +) +: Connection( + subject, + association => new ComponentManufacturerEdge(association), + queryContext +) { [UseUserManager] + [Cost(1)] public Task IsAuthorizedToAddEdgeAsync( ClaimsPrincipal claimsPrincipal, ComponentManufacturerAuthorization authorization, @@ -36,16 +38,15 @@ CancellationToken cancellationToken public sealed class PendingComponentManufacturerConnection( Component subject, QueryContext queryContext - ) - : AuthorizedConnection( - subject, - x => new ComponentManufacturerEdge(x), - (claimsPrincipal, component, authorization, cancellationToken) => - authorization.IsAuthorizedToAdd(claimsPrincipal, component.Id, cancellationToken), - queryContext - ) +) +: Connection( + subject, + association => new ComponentManufacturerEdge(association), + queryContext +) { [UseUserManager] + [Cost(1)] public Task IsAuthorizedToAddEdgeAsync( ClaimsPrincipal claimsPrincipal, ComponentManufacturerAuthorization authorization, diff --git a/backend/src/GraphQl/Components/ComponentManufacturerEdge.cs b/backend/src/GraphQl/Components/ComponentManufacturerEdge.cs index 136e94ffd..915feec1f 100644 --- a/backend/src/GraphQl/Components/ComponentManufacturerEdge.cs +++ b/backend/src/GraphQl/Components/ComponentManufacturerEdge.cs @@ -1,22 +1,23 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Institutions; using Metabase.GraphQl.Users; -using Microsoft.AspNetCore.Identity; namespace Metabase.GraphQl.Components; public sealed class ComponentManufacturerEdge( ComponentManufacturer association - ) - : Edge(association.InstitutionId) +) +: Edge( + association.InstitutionId +) { - private readonly ComponentManufacturer _association = association; - [UseUserManager] + [Cost(1)] public Task IsAuthorizedToConfirmEdgeAsync( ClaimsPrincipal claimsPrincipal, ComponentManufacturerAuthorization authorization, @@ -25,12 +26,13 @@ CancellationToken cancellationToken { return authorization.IsAuthorizedToConfirm( claimsPrincipal, - _association.InstitutionId, + association.InstitutionId, cancellationToken ); } [UseUserManager] + [Cost(1)] public Task IsAuthorizedToRemoveEdgeAsync( ClaimsPrincipal claimsPrincipal, ComponentManufacturerAuthorization authorization, @@ -39,7 +41,7 @@ CancellationToken cancellationToken { return authorization.IsAuthorizedToRemove( claimsPrincipal, - _association.ComponentId, + association.ComponentId, cancellationToken ); } diff --git a/backend/src/GraphQl/Components/ComponentManufacturerFilterType.cs b/backend/src/GraphQl/Components/ComponentManufacturerFilterType.cs index 5c0ae6585..accaee89b 100644 --- a/backend/src/GraphQl/Components/ComponentManufacturerFilterType.cs +++ b/backend/src/GraphQl/Components/ComponentManufacturerFilterType.cs @@ -1,5 +1,4 @@ using HotChocolate.Data.Filters; -using Metabase.Configuration; using Metabase.Data; namespace Metabase.GraphQl.Components; @@ -13,6 +12,6 @@ IFilterInputTypeDescriptor descriptor { base.Configure(descriptor); descriptor.Name(nameof(ComponentManufacturerFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); - descriptor.Field(x => x.Component).Ignore(); + descriptor.Field(_ => _.Component).Ignore(); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Components/ComponentManufacturerSortType.cs b/backend/src/GraphQl/Components/ComponentManufacturerSortType.cs new file mode 100644 index 000000000..7533d1459 --- /dev/null +++ b/backend/src/GraphQl/Components/ComponentManufacturerSortType.cs @@ -0,0 +1,16 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data; + +namespace Metabase.GraphQl.Components; + +public sealed class ComponentManufacturerSortType + : ComponentManufacturers.ComponentManufacturerSortType +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.Name(nameof(ComponentManufacturerSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Components/ComponentManufacturersByComponentIdDataLoader.cs b/backend/src/GraphQl/Components/ComponentManufacturersByComponentIdDataLoader.cs deleted file mode 100644 index 74d8c254c..000000000 --- a/backend/src/GraphQl/Components/ComponentManufacturersByComponentIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Components; - -public sealed class ComponentManufacturersByComponentIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.ComponentManufacturers.AsNoTracking().Where(x => - !x.Pending && ids.Contains(x.ComponentId) - ).With(queryContext), - x => x.ComponentId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Components/ComponentMutations.cs b/backend/src/GraphQl/Components/ComponentMutations.cs index 47079a263..ddc2dcc83 100644 --- a/backend/src/GraphQl/Components/ComponentMutations.cs +++ b/backend/src/GraphQl/Components/ComponentMutations.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading; @@ -27,7 +28,6 @@ public async Task CreateComponentAsync( CancellationToken cancellationToken ) { - // TODO Make CreateComponentInput.ManagerId required and remove fallback in mutation var applicationOwnerId = await authorization.SwitchUserOrApplicationAsync( claimsPrincipal, _ => Task.FromResult(null), @@ -51,15 +51,16 @@ CancellationToken cancellationToken ); } + var errors = new List(); if (input.ComponentId is not null && await context.Components.AsQueryable() .AnyAsync( - x => x.Id == input.ComponentId, + _ => _.Id == input.ComponentId, cancellationToken ) ) { - return new CreateComponentPayload( + errors.Add( new CreateComponentError( CreateComponentErrorCode.DUPLICATE_COMPONENT_ID, "The component ID is already in use.", @@ -67,16 +68,15 @@ CancellationToken cancellationToken ) ); } - if (input.ManagerId is not null && !await context.Institutions.AsQueryable() .AnyAsync( - x => x.Id == input.ManagerId, + _ => _.Id == input.ManagerId, cancellationToken ) ) { - return new CreateComponentPayload( + errors.Add( new CreateComponentError( CreateComponentErrorCode.UNKNOWN_MANAGER, "Unknown manager.", @@ -84,15 +84,14 @@ CancellationToken cancellationToken ) ); } - if (!await context.Institutions.AsQueryable() .AnyAsync( - x => x.Id == input.ManufacturerId, + _ => _.Id == input.ManufacturerId, cancellationToken ) ) { - return new CreateComponentPayload( + errors.Add( new CreateComponentError( CreateComponentErrorCode.UNKNOWN_MANUFACTURER, "Unknown manufacturer.", @@ -100,11 +99,10 @@ CancellationToken cancellationToken ) ); } - if (input.PrimeSurface?.Reference?.Standard is not null && input.PrimeSurface?.Reference?.Publication is not null) { - return new CreateComponentPayload( + errors.Add( new CreateComponentError( CreateComponentErrorCode.AMBIGUOUS_REFERENCE, "Both standard and publication are non-null.", @@ -112,11 +110,10 @@ CancellationToken cancellationToken ) ); } - if (input.PrimeDirection?.Reference?.Standard is not null && input.PrimeDirection?.Reference?.Publication is not null) { - return new CreateComponentPayload( + errors.Add( new CreateComponentError( CreateComponentErrorCode.AMBIGUOUS_REFERENCE, "Both standard and publication are non-null.", @@ -124,11 +121,10 @@ CancellationToken cancellationToken ) ); } - if (input.SwitchableLayers?.Reference?.Standard is not null && input.SwitchableLayers?.Reference?.Publication is not null) { - return new CreateComponentPayload( + errors.Add( new CreateComponentError( CreateComponentErrorCode.AMBIGUOUS_REFERENCE, "Both standard and publication are non-null.", @@ -136,6 +132,10 @@ CancellationToken cancellationToken ) ); } + if (errors.Count > 0) + { + return new CreateComponentPayload(errors); + } var availability = input.Availability?.ToDomainModel(); // Note that above we make sure that, for each reference, standard and publication are *not* both non-null. @@ -263,15 +263,16 @@ await context.Components.AsQueryable() ); } + var errors = new List(); if (input.ManufacturerId is not null && !await context.Institutions.AsQueryable() .AnyAsync( - x => x.Id == input.ManufacturerId, + _ => _.Id == input.ManufacturerId, cancellationToken ) ) { - return new UpdateComponentPayload( + errors.Add( new UpdateComponentError( UpdateComponentErrorCode.UNKNOWN_MANUFACTURER, "Unknown manufacturer", @@ -279,11 +280,10 @@ await context.Components.AsQueryable() ) ); } - if (input.PrimeSurface?.Reference?.Standard is not null && input.PrimeSurface?.Reference?.Publication is not null) { - return new UpdateComponentPayload( + errors.Add( new UpdateComponentError( UpdateComponentErrorCode.AMBIGUOUS_REFERENCE, "Both standard and publication are non-null.", @@ -291,11 +291,10 @@ await context.Components.AsQueryable() ) ); } - if (input.PrimeDirection?.Reference?.Standard is not null && input.PrimeDirection?.Reference?.Publication is not null) { - return new UpdateComponentPayload( + errors.Add( new UpdateComponentError( UpdateComponentErrorCode.AMBIGUOUS_REFERENCE, "Both standard and publication are non-null.", @@ -303,11 +302,10 @@ await context.Components.AsQueryable() ) ); } - if (input.SwitchableLayers?.Reference?.Standard is not null && input.SwitchableLayers?.Reference?.Publication is not null) { - return new UpdateComponentPayload( + errors.Add( new UpdateComponentError( UpdateComponentErrorCode.AMBIGUOUS_REFERENCE, "Both standard and publication are non-null.", @@ -315,6 +313,10 @@ await context.Components.AsQueryable() ) ); } + if (errors.Count > 0) + { + return new UpdateComponentPayload(errors); + } component.Update( input.Name, diff --git a/backend/src/GraphQl/Components/ComponentPartOfByComponentIdDataLoader.cs b/backend/src/GraphQl/Components/ComponentPartOfByComponentIdDataLoader.cs deleted file mode 100644 index fd4d811c8..000000000 --- a/backend/src/GraphQl/Components/ComponentPartOfByComponentIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Components; - -public sealed class ComponentPartOfByComponentIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.ComponentAssemblies.AsNoTracking().Where(x => - ids.Contains(x.PartComponentId) - ).With(queryContext), - x => x.PartComponentId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Components/ComponentPartOfConnection.cs b/backend/src/GraphQl/Components/ComponentPartOfConnection.cs index d60f209ac..c62984788 100644 --- a/backend/src/GraphQl/Components/ComponentPartOfConnection.cs +++ b/backend/src/GraphQl/Components/ComponentPartOfConnection.cs @@ -2,10 +2,10 @@ using System.Threading; using System.Threading.Tasks; using GreenDonut.Data; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Users; -using Microsoft.AspNetCore.Identity; namespace Metabase.GraphQl.Components; @@ -13,14 +13,14 @@ public sealed class ComponentPartOfConnection( Component subject, QueryContext queryContext ) - : Connection( + : Connection( subject, x => new ComponentPartOfEdge(x), queryContext ) { [UseUserManager] + [Cost(1)] public Task IsAuthorizedToAddEdgeAsync( ClaimsPrincipal claimsPrincipal, ComponentAssemblyAuthorization authorization, diff --git a/backend/src/GraphQl/Components/ComponentPartOfEdge.cs b/backend/src/GraphQl/Components/ComponentPartOfEdge.cs index 587c9d5bf..e704a44ae 100644 --- a/backend/src/GraphQl/Components/ComponentPartOfEdge.cs +++ b/backend/src/GraphQl/Components/ComponentPartOfEdge.cs @@ -1,26 +1,25 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.Enumerations; using Metabase.GraphQl.Users; -using Microsoft.AspNetCore.Identity; namespace Metabase.GraphQl.Components; public sealed class ComponentPartOfEdge( ComponentAssembly association ) - : Edge(association.AssembledComponentId) + : Edge(association.AssembledComponentId) { - private readonly ComponentAssembly _association = association; + public byte? Index => association.Index; - public byte? Index => _association.Index; - - public PrimeSurface? PrimeSurface => _association.PrimeSurface; + public PrimeSurface? PrimeSurface => association.PrimeSurface; [UseUserManager] + [Cost(1)] public Task IsAuthorizedToUpdateEdgeAsync( ClaimsPrincipal claimsPrincipal, ComponentAssemblyAuthorization authorization, @@ -29,13 +28,14 @@ CancellationToken cancellationToken { return authorization.IsAuthorizedToManage( claimsPrincipal, - _association.AssembledComponentId, - _association.PartComponentId, + association.AssembledComponentId, + association.PartComponentId, cancellationToken ); } [UseUserManager] + [Cost(1)] public Task IsAuthorizedToRemoveEdgeAsync( ClaimsPrincipal claimsPrincipal, ComponentAssemblyAuthorization authorization, @@ -44,8 +44,8 @@ CancellationToken cancellationToken { return authorization.IsAuthorizedToManage( claimsPrincipal, - _association.AssembledComponentId, - _association.PartComponentId, + association.AssembledComponentId, + association.PartComponentId, cancellationToken ); } diff --git a/backend/src/GraphQl/Components/ComponentPartOfFilterType.cs b/backend/src/GraphQl/Components/ComponentPartOfFilterType.cs index 8559308b6..e2819010a 100644 --- a/backend/src/GraphQl/Components/ComponentPartOfFilterType.cs +++ b/backend/src/GraphQl/Components/ComponentPartOfFilterType.cs @@ -1,5 +1,4 @@ using HotChocolate.Data.Filters; -using Metabase.Configuration; using Metabase.Data; namespace Metabase.GraphQl.Components; @@ -13,6 +12,6 @@ IFilterInputTypeDescriptor descriptor { base.Configure(descriptor); descriptor.Name(nameof(ComponentPartOfFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); - descriptor.Field(x => x.PartComponent).Ignore(); + descriptor.Field(_ => _.PartComponent).Ignore(); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Components/ComponentPartOfSortType.cs b/backend/src/GraphQl/Components/ComponentPartOfSortType.cs new file mode 100644 index 000000000..331a0d9da --- /dev/null +++ b/backend/src/GraphQl/Components/ComponentPartOfSortType.cs @@ -0,0 +1,16 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data; + +namespace Metabase.GraphQl.Components; + +public sealed class ComponentPartOfSortType + : ComponentAssemblies.ComponentAssemblySortType +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.Name(nameof(ComponentPartOfSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Components/ComponentPartsByComponentIdDataLoader.cs b/backend/src/GraphQl/Components/ComponentPartsByComponentIdDataLoader.cs deleted file mode 100644 index 430757729..000000000 --- a/backend/src/GraphQl/Components/ComponentPartsByComponentIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Components; - -public sealed class ComponentPartsByComponentIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.ComponentAssemblies.AsNoTracking().Where(x => - ids.Contains(x.AssembledComponentId) - ).OrderBy(x => x.Index).With(queryContext), - x => x.AssembledComponentId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Components/ComponentQueries.cs b/backend/src/GraphQl/Components/ComponentQueries.cs index 1d16784c0..0d6da95d1 100644 --- a/backend/src/GraphQl/Components/ComponentQueries.cs +++ b/backend/src/GraphQl/Components/ComponentQueries.cs @@ -2,8 +2,9 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using GreenDonut.Data; using HotChocolate.Data; -using HotChocolate.Data.Sorting; +using HotChocolate.Resolvers; using HotChocolate.Types; using Metabase.Data; using Metabase.GraphQl.Extensions; @@ -15,27 +16,32 @@ namespace Metabase.GraphQl.Components; public sealed class ComponentQueries { [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 GetComponents( - ApplicationDbContext context, - ISortingContext sorting + public ValueTask> GetComponentsAsync( + IResolverContext resolverContext, + ApplicationDbContext databaseContext, + // PagingArguments pagingArguments, // results in the parameter `pagingArguments: PagingArgumentsInput` in the GraphQL schema + // QueryContext queryContext, // starts up the projection engine producing many problems + CancellationToken cancellationToken ) { - sorting.StabilizeOrder(); - return context.Components.AsNoTracking(); + return databaseContext.Components + .AsNoTracking() + .With(resolverContext.GetQueryContext(), Sorting.DefaultEntityOrder) + .ToPageAsync(resolverContext.GetPagingArguments(), cancellationToken) + .ToConnectionAsync(); } public Task GetComponentAsync( Guid id, - ComponentByIdDataLoader componentById, + IComponentByIdDataLoader byId, + // QueryContext queryContext, // starts up the projection engine producing many problems CancellationToken cancellationToken ) { - return componentById.LoadAsync( - id, - cancellationToken - ); + return byId + // .With(queryContext) + .LoadAsync(id, cancellationToken); } } \ No newline at end of file diff --git a/backend/src/GraphQl/Components/ComponentSortType.cs b/backend/src/GraphQl/Components/ComponentSortType.cs index 7121b4a63..9eb88247b 100644 --- a/backend/src/GraphQl/Components/ComponentSortType.cs +++ b/backend/src/GraphQl/Components/ComponentSortType.cs @@ -4,17 +4,17 @@ namespace Metabase.GraphQl.Components; -public sealed class ComponentSortType - : EntitySortType +public class ComponentSortType + : AuditableEntitySortType { protected override void Configure( ISortInputTypeDescriptor descriptor ) { base.Configure(descriptor); - descriptor.Field(x => x.Name); - descriptor.Field(x => x.Abbreviation); - descriptor.Field(x => x.Description); + descriptor.Field(_ => _.Name); + descriptor.Field(_ => _.Abbreviation); + descriptor.Field(_ => _.Description); // TODO Allow sorting by Availability. How? See https://chillicream.com/docs/hotchocolate/fetching-data/sorting/#customization } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Components/ComponentType.cs b/backend/src/GraphQl/Components/ComponentType.cs index e62961e66..9e8146f6e 100644 --- a/backend/src/GraphQl/Components/ComponentType.cs +++ b/backend/src/GraphQl/Components/ComponentType.cs @@ -1,19 +1,29 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using GreenDonut.Data; using HotChocolate; +using HotChocolate.Resolvers; using HotChocolate.Types; using Metabase.Authorization; using Metabase.Data; -using Metabase.Extensions; +using Metabase.GraphQl.CalorimetricDataX; +using Metabase.GraphQl.GeometricDataX; +using Metabase.GraphQl.HygrothermalDataX; +using Metabase.GraphQl.LifeCycleDataX; +using Metabase.GraphQl.OpticalDataX; +using Metabase.GraphQl.PhotovoltaicDataX; using Metabase.GraphQl.Entities; using Metabase.GraphQl.Extensions; +using Metabase.GraphQl.Scalars; using Metabase.GraphQl.Users; +using Metabase.GraphQl.Requests; +using HotChocolate.CostAnalysis.Types; namespace Metabase.GraphQl.Components; public sealed class ComponentType - : EntityType + : EntityType { protected override void Configure( IObjectTypeDescriptor descriptor @@ -29,6 +39,7 @@ IObjectTypeDescriptor descriptor descriptor .Field("prime") .Type>() + .Cost(0) .Resolve(context => { var component = context.Parent(); @@ -54,6 +65,7 @@ IObjectTypeDescriptor descriptor .Field(t => t.Manufacturers) .Type>>() .UseFiltering() + .UseSorting() .Resolve(context => new ComponentManufacturerConnection( context.Parent(), @@ -65,6 +77,7 @@ IObjectTypeDescriptor descriptor .Type>() .Authorize(AuthorizationPolicies.WriteScopePolicy) .UseFiltering() + .UseSorting() .Resolve(context => new PendingComponentManufacturerConnection( context.Parent(), @@ -78,6 +91,7 @@ IObjectTypeDescriptor descriptor .Name("assembledOf") .Type>>() .UseFiltering() + .UseSorting() .Resolve(context => new ComponentAssembledOfConnection( context.Parent(), @@ -90,6 +104,7 @@ IObjectTypeDescriptor descriptor .Field(t => t.PartOf) .Type>>() .UseFiltering() + .UseSorting() .Resolve(context => new ComponentPartOfConnection( context.Parent(), @@ -103,6 +118,7 @@ IObjectTypeDescriptor descriptor .Name("concretizationOf") .Type>>() .UseFiltering() + .UseSorting() .Resolve(context => new ComponentConcretizationOfConnection( context.Parent(), @@ -117,6 +133,7 @@ IObjectTypeDescriptor descriptor .Name("generalizationOf") .Type>>() .UseFiltering() + .UseSorting() .Resolve(context => new ComponentGeneralizationOfConnection( context.Parent(), @@ -135,6 +152,7 @@ IObjectTypeDescriptor descriptor .Field(t => t.VariantOf) .Type>>() .UseFiltering() + .UseSorting() .Resolve(context => new ComponentVariantOfConnection( context.Parent(), @@ -143,16 +161,355 @@ IObjectTypeDescriptor descriptor ); descriptor .Field(t => t.VariantOfEdges).Ignore(); + descriptor + .Field("allCalorimetricData") + .ResolveWith(_ => _.GetAllCalorimetricDataAsync(default!, default, default, default, default, default, default!, default!, default)); + descriptor + .Field("allGeometricData") + .ResolveWith(_ => _.GetAllGeometricDataAsync(default!, default, default, default, default, default, default!, default!, default)); + descriptor + .Field("allHygrothermalData") + .ResolveWith(_ => _.GetAllHygrothermalDataAsync(default!, default, default, default, default, default, default!, default!, default)); + descriptor + .Field("allLifeCycleData") + .ResolveWith(_ => _.GetAllLifeCycleDataAsync(default!, default, default, default, default, default, default!, default!, default)); + descriptor + .Field("allOpticalData") + .ResolveWith(_ => _.GetAllOpticalDataAsync(default!, default, default, default, default, default, default!, default!, default)); + descriptor + .Field("allPhotovoltaicData") + .ResolveWith(_ => _.GetAllPhotovoltaicDataAsync(default!, default, default, default, default, default, default!, default!, default)); descriptor .Field("isAuthorizedToUpdateNode") + .Cost(1) .ResolveWith(x => ComponentResolvers.IsAuthorizedToUpdateNodeAsync(default!, default!, default!, default!)) .UseUserManager(); } - private sealed class ComponentResolvers + private sealed class ComponentResolvers( + DataQueries dataQueries + ) { - public static Task IsAuthorizedToUpdateNodeAsync( + [ListSize( + AssumedSize = (int)GraphQlConstants.MaximumPageSize - 1, + SlicingArguments = ["first", "last"], + SlicingArgumentDefaultValue = (int)GraphQlConstants.MaximumPageSize - 1, + SizedFields = ["edges", "nodes"], + RequireOneSlicingArgument = false + )] + public Task GetAllCalorimetricDataAsync( + [Parent] Component component, + CalorimetricDataPropositionInput? where, + [GraphQLType] string? locale, + int? first, + string? after, + int? last, + string? before, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + var componentIdPropositionInput = CalorimetricDataPropositionInput.Empty with + { + ComponentId = DataX.UuidPropositionInput.Empty with + { + EqualTo = component.Id + } + }; + return dataQueries.GetAllDataAsync( + first, + after, + last, + before, + (edges, totalCount, pageInfo) => new CalorimetricDataConnection(edges, totalCount, pageInfo), + (node, cursor) => new CalorimetricDataEdge(cursor, node), + (database, first, after, last, before) => dataQueries.GetAllCalorimetricDataAsync( + database, + CalorimetricDataPropositionInput.Empty with + { + And = where is null + ? [componentIdPropositionInput] + : [componentIdPropositionInput, where] + }, + locale, + first, + after, + last, + before, + resolverContext, + cancellationToken + ), + cancellationToken + ); + } + + [ListSize( + AssumedSize = (int)GraphQlConstants.MaximumPageSize - 1, + SlicingArguments = ["first", "last"], + SlicingArgumentDefaultValue = (int)GraphQlConstants.MaximumPageSize - 1, + SizedFields = ["edges", "nodes"], + RequireOneSlicingArgument = false + )] + public Task GetAllGeometricDataAsync( + [Parent] Component component, + GeometricDataPropositionInput? where, + [GraphQLType] string? locale, + int? first, + string? after, + int? last, + string? before, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + var componentIdPropositionInput = GeometricDataPropositionInput.Empty with + { + ComponentId = DataX.UuidPropositionInput.Empty with + { + EqualTo = component.Id + } + }; + return dataQueries.GetAllDataAsync( + first, + after, + last, + before, + (edges, totalCount, pageInfo) => new GeometricDataConnection(edges, totalCount, pageInfo), + (node, cursor) => new GeometricDataEdge(cursor, node), + (database, first, after, last, before) => dataQueries.GetAllGeometricDataAsync( + database, + GeometricDataPropositionInput.Empty with + { + And = where is null + ? [componentIdPropositionInput] + : [componentIdPropositionInput, where] + }, + locale, + first, + after, + last, + before, + resolverContext, + cancellationToken + ), + cancellationToken + ); + } + + [ListSize( + AssumedSize = (int)GraphQlConstants.MaximumPageSize - 1, + SlicingArguments = ["first", "last"], + SlicingArgumentDefaultValue = (int)GraphQlConstants.MaximumPageSize - 1, + SizedFields = ["edges", "nodes"], + RequireOneSlicingArgument = false + )] + public Task GetAllHygrothermalDataAsync( + [Parent] Component component, + HygrothermalDataPropositionInput? where, + [GraphQLType] string? locale, + int? first, + string? after, + int? last, + string? before, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + var componentIdPropositionInput = HygrothermalDataPropositionInput.Empty with + { + ComponentId = DataX.UuidPropositionInput.Empty with + { + EqualTo = component.Id + } + }; + return dataQueries.GetAllDataAsync( + first, + after, + last, + before, + (edges, totalCount, pageInfo) => new HygrothermalDataConnection(edges, totalCount, pageInfo), + (node, cursor) => new HygrothermalDataEdge(cursor, node), + (database, first, after, last, before) => dataQueries.GetAllHygrothermalDataAsync( + database, + HygrothermalDataPropositionInput.Empty with + { + And = where is null + ? [componentIdPropositionInput] + : [componentIdPropositionInput, where] + }, + locale, + first, + after, + last, + before, + resolverContext, + cancellationToken + ), + cancellationToken + ); + } + + [ListSize( + AssumedSize = (int)GraphQlConstants.MaximumPageSize - 1, + SlicingArguments = ["first", "last"], + SlicingArgumentDefaultValue = (int)GraphQlConstants.MaximumPageSize - 1, + SizedFields = ["edges", "nodes"], + RequireOneSlicingArgument = false + )] + public Task GetAllLifeCycleDataAsync( + [Parent] Component component, + LifeCycleDataPropositionInput? where, + [GraphQLType] string? locale, + int? first, + string? after, + int? last, + string? before, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + var componentIdPropositionInput = LifeCycleDataPropositionInput.Empty with + { + ComponentId = DataX.UuidPropositionInput.Empty with + { + EqualTo = component.Id + } + }; + return dataQueries.GetAllDataAsync( + first, + after, + last, + before, + (edges, totalCount, pageInfo) => new LifeCycleDataConnection(edges, totalCount, pageInfo), + (node, cursor) => new LifeCycleDataEdge(cursor, node), + (database, first, after, last, before) => dataQueries.GetAllLifeCycleDataAsync( + database, + LifeCycleDataPropositionInput.Empty with + { + And = where is null + ? [componentIdPropositionInput] + : [componentIdPropositionInput, where] + }, + locale, + first, + after, + last, + before, + resolverContext, + cancellationToken + ), + cancellationToken + ); + } + + [ListSize( + AssumedSize = (int)GraphQlConstants.MaximumPageSize - 1, + SlicingArguments = ["first", "last"], + SlicingArgumentDefaultValue = (int)GraphQlConstants.MaximumPageSize - 1, + SizedFields = ["edges", "nodes"], + RequireOneSlicingArgument = false + )] + public Task GetAllOpticalDataAsync( + [Parent] Component component, + OpticalDataPropositionInput? where, + [GraphQLType] string? locale, + int? first, + string? after, + int? last, + string? before, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + var componentIdPropositionInput = OpticalDataPropositionInput.Empty with + { + ComponentId = DataX.UuidPropositionInput.Empty with + { + EqualTo = component.Id + } + }; + return dataQueries.GetAllDataAsync( + first, + after, + last, + before, + (edges, totalCount, pageInfo) => new OpticalDataConnection(edges, totalCount, pageInfo), + (node, cursor) => new OpticalDataEdge(cursor, node), + (database, first, after, last, before) => dataQueries.GetAllOpticalDataAsync( + database, + OpticalDataPropositionInput.Empty with + { + And = where is null + ? [componentIdPropositionInput] + : [componentIdPropositionInput, where] + }, + locale, + first, + after, + last, + before, + resolverContext, + cancellationToken + ), + cancellationToken + ); + } + + [ListSize( + AssumedSize = (int)GraphQlConstants.MaximumPageSize - 1, + SlicingArguments = ["first", "last"], + SlicingArgumentDefaultValue = (int)GraphQlConstants.MaximumPageSize - 1, + SizedFields = ["edges", "nodes"], + RequireOneSlicingArgument = false + )] + public Task GetAllPhotovoltaicDataAsync( + [Parent] Component component, + PhotovoltaicDataPropositionInput? where, + [GraphQLType] string? locale, + int? first, + string? after, + int? last, + string? before, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + var componentIdPropositionInput = PhotovoltaicDataPropositionInput.Empty with + { + ComponentId = DataX.UuidPropositionInput.Empty with + { + EqualTo = component.Id + } + }; + return dataQueries.GetAllDataAsync( + first, + after, + last, + before, + (edges, totalCount, pageInfo) => new PhotovoltaicDataConnection(edges, totalCount, pageInfo), + (node, cursor) => new PhotovoltaicDataEdge(cursor, node), + (database, first, after, last, before) => dataQueries.GetAllPhotovoltaicDataAsync( + database, + PhotovoltaicDataPropositionInput.Empty with + { + And = where is null + ? [componentIdPropositionInput] + : [componentIdPropositionInput, where] + }, + locale, + first, + after, + last, + before, + resolverContext, + cancellationToken + ), + cancellationToken + ); + } + + internal static Task IsAuthorizedToUpdateNodeAsync( [Parent] Component component, ClaimsPrincipal claimsPrincipal, ComponentAuthorization authorization, diff --git a/backend/src/GraphQl/Components/ComponentVariantOfByComponentIdDataLoader.cs b/backend/src/GraphQl/Components/ComponentVariantOfByComponentIdDataLoader.cs deleted file mode 100644 index 61d455ea5..000000000 --- a/backend/src/GraphQl/Components/ComponentVariantOfByComponentIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Components; - -public sealed class ComponentVariantOfByComponentIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.ComponentVariants.AsNoTracking().Where(x => - ids.Contains(x.ToComponentId) - ).With(queryContext), - x => x.ToComponentId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Components/ComponentVariantOfConnection.cs b/backend/src/GraphQl/Components/ComponentVariantOfConnection.cs index fe99344f6..0df26a946 100644 --- a/backend/src/GraphQl/Components/ComponentVariantOfConnection.cs +++ b/backend/src/GraphQl/Components/ComponentVariantOfConnection.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using GreenDonut.Data; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Users; @@ -12,14 +13,14 @@ public sealed class ComponentVariantOfConnection( Component subject, QueryContext queryContext ) - : Connection( + : Connection( subject, x => new ComponentVariantOfEdge(x), queryContext ) { [UseUserManager] + [Cost(1)] public Task IsAuthorizedToAddEdgeAsync( ClaimsPrincipal claimsPrincipal, ComponentVariantAuthorization authorization, diff --git a/backend/src/GraphQl/Components/ComponentVariantOfEdge.cs b/backend/src/GraphQl/Components/ComponentVariantOfEdge.cs index 57b55101b..b0facc961 100644 --- a/backend/src/GraphQl/Components/ComponentVariantOfEdge.cs +++ b/backend/src/GraphQl/Components/ComponentVariantOfEdge.cs @@ -1,21 +1,20 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Users; -using Microsoft.AspNetCore.Identity; namespace Metabase.GraphQl.Components; public sealed class ComponentVariantOfEdge( ComponentVariant association ) - : Edge(association.OfComponentId) + : Edge(association.OfComponentId) { - private readonly ComponentVariant _association = association; - [UseUserManager] + [Cost(1)] public Task IsAuthorizedToRemoveEdgeAsync( ClaimsPrincipal claimsPrincipal, ComponentAssemblyAuthorization authorization, @@ -24,8 +23,8 @@ CancellationToken cancellationToken { return authorization.IsAuthorizedToManage( claimsPrincipal, - _association.OfComponentId, - _association.ToComponentId, + association.OfComponentId, + association.ToComponentId, cancellationToken ); } diff --git a/backend/src/GraphQl/Components/ComponentVariantOfFilterType.cs b/backend/src/GraphQl/Components/ComponentVariantOfFilterType.cs index 09cbd214d..1db5f2d46 100644 --- a/backend/src/GraphQl/Components/ComponentVariantOfFilterType.cs +++ b/backend/src/GraphQl/Components/ComponentVariantOfFilterType.cs @@ -1,5 +1,4 @@ using HotChocolate.Data.Filters; -using Metabase.Configuration; using Metabase.Data; namespace Metabase.GraphQl.Components; @@ -13,6 +12,6 @@ IFilterInputTypeDescriptor descriptor { base.Configure(descriptor); descriptor.Name(nameof(ComponentVariantOfFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); - descriptor.Field(x => x.ToComponent).Ignore(); + descriptor.Field(_ => _.ToComponent).Ignore(); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Components/ComponentVariantOfSortType.cs b/backend/src/GraphQl/Components/ComponentVariantOfSortType.cs new file mode 100644 index 000000000..4f8201c08 --- /dev/null +++ b/backend/src/GraphQl/Components/ComponentVariantOfSortType.cs @@ -0,0 +1,16 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data; + +namespace Metabase.GraphQl.Components; + +public sealed class ComponentVariantOfSortType + : ComponentVariants.ComponentVariantSortType +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.Name(nameof(ComponentVariantOfSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Components/PendingComponentManufacturersByComponentIdDataLoader.cs b/backend/src/GraphQl/Components/PendingComponentManufacturersByComponentIdDataLoader.cs deleted file mode 100644 index 78672c3e0..000000000 --- a/backend/src/GraphQl/Components/PendingComponentManufacturersByComponentIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Components; - -public sealed class PendingComponentManufacturersByComponentIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.ComponentManufacturers.AsNoTracking().Where(x => - x.Pending && ids.Contains(x.ComponentId) - ).With(queryContext), - x => x.ComponentId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Connection.cs b/backend/src/GraphQl/Connection.cs index df5578991..e0380963c 100644 --- a/backend/src/GraphQl/Connection.cs +++ b/backend/src/GraphQl/Connection.cs @@ -6,34 +6,42 @@ using System.Threading.Tasks; using GreenDonut; using GreenDonut.Data; +using HotChocolate.CostAnalysis.Types; using Metabase.Data; namespace Metabase.GraphQl; -public abstract class Connection( +public abstract class Connection( TSubject subject, Func createEdge, QueryContext queryContext - ) +) where TSubject : IEntity - where TAssociationsByAssociateIdDataLoader : IDataLoader + where TAssociationsByOneIdDataLoader : IDataLoader { protected TSubject Subject { get; } = subject; - public async Task GetTotalCountAsync( - TAssociationsByAssociateIdDataLoader dataLoader, + [Cost(0)] + public async Task GetTotalCountAsync( + TAssociationsByOneIdDataLoader dataLoader, CancellationToken cancellationToken ) { - return (uint)(await dataLoader.With(queryContext).LoadRequiredAsync(Subject.Id, cancellationToken)).Length; + return ( + 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); } diff --git a/backend/src/GraphQl/ContactInformations/ContactInformationFilterType.cs b/backend/src/GraphQl/ContactInformations/ContactInformationFilterType.cs index 38e164b9f..1252e6c95 100644 --- a/backend/src/GraphQl/ContactInformations/ContactInformationFilterType.cs +++ b/backend/src/GraphQl/ContactInformations/ContactInformationFilterType.cs @@ -10,6 +10,8 @@ protected override void Configure( IFilterInputTypeDescriptor descriptor ) { + base.Configure(descriptor); + descriptor.Name(nameof(ContactInformationFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); descriptor.BindFieldsExplicitly(); descriptor.Field(_ => _.PhoneNumber); descriptor.Field(_ => _.IsPhoneNumberConfirmed); diff --git a/backend/src/GraphQl/ContactInformations/ContactInformationInput.cs b/backend/src/GraphQl/ContactInformations/ContactInformationInput.cs index e69fa62d3..0ff9ae37a 100644 --- a/backend/src/GraphQl/ContactInformations/ContactInformationInput.cs +++ b/backend/src/GraphQl/ContactInformations/ContactInformationInput.cs @@ -1,12 +1,14 @@ using System; +using HotChocolate; +using HotChocolate.Types; using Metabase.Data; namespace Metabase.GraphQl.ContactInformations; public sealed record ContactInformationInput( - string? PhoneNumber, + [property: GraphQLType] string? PhoneNumber, string? PostalAddress, - string? EmailAddress, + [property: GraphQLType] string? EmailAddress, Uri? WebsiteLocator ) { diff --git a/backend/src/GraphQl/ContactInformations/ContactInformationSortType.cs b/backend/src/GraphQl/ContactInformations/ContactInformationSortType.cs index 439a6150e..2897b2cc5 100644 --- a/backend/src/GraphQl/ContactInformations/ContactInformationSortType.cs +++ b/backend/src/GraphQl/ContactInformations/ContactInformationSortType.cs @@ -10,12 +10,12 @@ protected override void Configure( ISortInputTypeDescriptor descriptor ) { + base.Configure(descriptor); descriptor.BindFieldsExplicitly(); + descriptor.Name(nameof(ContactInformationSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); descriptor.Field(_ => _.PhoneNumber); - descriptor.Field(_ => _.IsPhoneNumberConfirmed); descriptor.Field(_ => _.PostalAddress); descriptor.Field(_ => _.EmailAddress); - descriptor.Field(_ => _.IsEmailAddressConfirmed); descriptor.Field(_ => _.WebsiteLocator); } } \ No newline at end of file diff --git a/backend/src/GraphQl/ContactInformations/ContactInformationType.cs b/backend/src/GraphQl/ContactInformations/ContactInformationType.cs index 3ac53b46b..ac3472c6d 100644 --- a/backend/src/GraphQl/ContactInformations/ContactInformationType.cs +++ b/backend/src/GraphQl/ContactInformations/ContactInformationType.cs @@ -10,5 +10,11 @@ protected override void Configure( IObjectTypeDescriptor descriptor ) { + descriptor + .Field(_ => _.EmailAddress) + .Type(); + descriptor + .Field(_ => _.PhoneNumber) + .Type(); } } \ No newline at end of file diff --git a/backend/src/GraphQl/DataFormats/DataFormatByIdDataLoader.cs b/backend/src/GraphQl/DataFormats/DataFormatByIdDataLoader.cs deleted file mode 100644 index 0f1bb3b9c..000000000 --- a/backend/src/GraphQl/DataFormats/DataFormatByIdDataLoader.cs +++ /dev/null @@ -1,20 +0,0 @@ -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.DataFormats; - -public sealed class DataFormatByIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : EntityByIdDataLoader( - batchScheduler, - options, - dbContextFactory, - dbContext => dbContext.DataFormats - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/DataFormats/DataFormatDataLoaders.cs b/backend/src/GraphQl/DataFormats/DataFormatDataLoaders.cs new file mode 100644 index 000000000..2501972a5 --- /dev/null +++ b/backend/src/GraphQl/DataFormats/DataFormatDataLoaders.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut; +using GreenDonut.Data; +using Metabase.Data; +using Microsoft.EntityFrameworkCore; + +namespace Metabase.GraphQl.DataFormats; + +public sealed class DataFormatDataLoaders +: DataLoaders +{ + [DataLoader] + public static ValueTask> GetDataFormatByIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetEntityByIdAsync( + ids, + databaseContext => databaseContext.DataFormats, + queryContext, + databaseContextFactory, + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/DataFormats/DataFormatFilterType.cs b/backend/src/GraphQl/DataFormats/DataFormatFilterType.cs index 043830b21..c62905026 100644 --- a/backend/src/GraphQl/DataFormats/DataFormatFilterType.cs +++ b/backend/src/GraphQl/DataFormats/DataFormatFilterType.cs @@ -5,18 +5,22 @@ namespace Metabase.GraphQl.DataFormats; public class DataFormatFilterType - : EntityFilterType + : AuditableEntityFilterType { protected override void Configure( IFilterInputTypeDescriptor descriptor ) { base.Configure(descriptor); - descriptor.Field(x => x.Name); - descriptor.Field(x => x.Extension); - descriptor.Field(x => x.Description); - descriptor.Field(x => x.MediaType); - descriptor.Field(x => x.SchemaLocator); - descriptor.Field(x => x.Manager); + // TODO Remove Id, CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(_ => _.Id); + descriptor.Field(_ => _.CreatedAt); + descriptor.Field(_ => _.UpdatedAt); + descriptor.Field(_ => _.Name); + descriptor.Field(_ => _.Extension); + descriptor.Field(_ => _.Description); + descriptor.Field(_ => _.MediaType); + descriptor.Field(_ => _.SchemaLocator); + descriptor.Field(_ => _.Manager); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/DataFormats/DataFormatManagerEdge.cs b/backend/src/GraphQl/DataFormats/DataFormatManagerEdge.cs index 45f1d3b47..b7eb5fbe5 100644 --- a/backend/src/GraphQl/DataFormats/DataFormatManagerEdge.cs +++ b/backend/src/GraphQl/DataFormats/DataFormatManagerEdge.cs @@ -6,6 +6,6 @@ namespace Metabase.GraphQl.DataFormats; public sealed class DataFormatManagerEdge( DataFormat association ) - : Edge(association.ManagerId) + : Edge(association.ManagerId) { } \ No newline at end of file diff --git a/backend/src/GraphQl/DataFormats/DataFormatMutations.cs b/backend/src/GraphQl/DataFormats/DataFormatMutations.cs index cef89ac35..06071e0a1 100644 --- a/backend/src/GraphQl/DataFormats/DataFormatMutations.cs +++ b/backend/src/GraphQl/DataFormats/DataFormatMutations.cs @@ -43,7 +43,7 @@ CancellationToken cancellationToken if (!await context.Institutions.AsQueryable() .AnyAsync( - x => x.Id == input.ManagerId, + _ => _.Id == input.ManagerId, cancellationToken ) ) @@ -151,4 +151,4 @@ await context.DataFormats.AsQueryable() await context.SaveChangesAsync(cancellationToken); return new UpdateDataFormatPayload(dataFormat); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/DataFormats/DataFormatQueries.cs b/backend/src/GraphQl/DataFormats/DataFormatQueries.cs index 1cd76764c..24553041c 100644 --- a/backend/src/GraphQl/DataFormats/DataFormatQueries.cs +++ b/backend/src/GraphQl/DataFormats/DataFormatQueries.cs @@ -2,8 +2,10 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using GreenDonut.Data; using HotChocolate.Data; using HotChocolate.Data.Sorting; +using HotChocolate.Resolvers; using HotChocolate.Types; using Metabase.Data; using Metabase.GraphQl.Extensions; @@ -15,27 +17,27 @@ namespace Metabase.GraphQl.DataFormats; public sealed class DataFormatQueries { [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 GetDataFormats( - ApplicationDbContext context, - ISortingContext sorting + public ValueTask> GetDataFormatsAsync( + IResolverContext resolverContext, + ApplicationDbContext databaseContext, + CancellationToken cancellationToken ) { - sorting.StabilizeOrder(); - return context.DataFormats.AsNoTracking(); + return databaseContext.DataFormats + .AsNoTracking() + .With(resolverContext.GetQueryContext(), Sorting.DefaultEntityOrder) + .ToPageAsync(resolverContext.GetPagingArguments(), cancellationToken) + .ToConnectionAsync(); } public Task GetDataFormatAsync( Guid id, - DataFormatByIdDataLoader dataFormatById, + IDataFormatByIdDataLoader byId, CancellationToken cancellationToken ) { - return dataFormatById.LoadAsync( - id, - cancellationToken - ); + return byId.LoadAsync(id, cancellationToken); } } \ No newline at end of file diff --git a/backend/src/GraphQl/DataFormats/DataFormatSortType.cs b/backend/src/GraphQl/DataFormats/DataFormatSortType.cs index 59fc22fd4..71723cdb8 100644 --- a/backend/src/GraphQl/DataFormats/DataFormatSortType.cs +++ b/backend/src/GraphQl/DataFormats/DataFormatSortType.cs @@ -4,19 +4,18 @@ namespace Metabase.GraphQl.DataFormats; -public sealed class DataFormatSortType - : EntitySortType +public class DataFormatSortType + : AuditableEntitySortType { protected override void Configure( ISortInputTypeDescriptor descriptor ) { base.Configure(descriptor); - descriptor.Field(x => x.Name); - descriptor.Field(x => x.Extension); - descriptor.Field(x => x.Description); - descriptor.Field(x => x.MediaType); - descriptor.Field(x => x.SchemaLocator); - descriptor.Field(x => x.Manager); + descriptor.Field(_ => _.Name); + descriptor.Field(_ => _.Extension); + descriptor.Field(_ => _.Description); + descriptor.Field(_ => _.MediaType); + descriptor.Field(_ => _.SchemaLocator); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/DataFormats/DataFormatType.cs b/backend/src/GraphQl/DataFormats/DataFormatType.cs index 51c77cf0b..baab33be5 100644 --- a/backend/src/GraphQl/DataFormats/DataFormatType.cs +++ b/backend/src/GraphQl/DataFormats/DataFormatType.cs @@ -8,12 +8,11 @@ using Metabase.GraphQl.Entities; using Metabase.GraphQl.References; using Metabase.GraphQl.Users; -using Microsoft.AspNetCore.Identity; namespace Metabase.GraphQl.DataFormats; public sealed class DataFormatType - : EntityType + : EntityType { protected override void Configure( IObjectTypeDescriptor descriptor @@ -23,6 +22,7 @@ IObjectTypeDescriptor descriptor descriptor .Field(t => t.Reference) .Type() + .Cost(0) .Resolve(context => context .Parent() .Reference? @@ -41,6 +41,7 @@ IObjectTypeDescriptor descriptor .Ignore(); descriptor .Field("isAuthorizedToUpdateNode") + .Cost(1) .ResolveWith(x => DataFormatResolvers.IsAuthorizedToUpdateNodeAsync(default!, default!, default!, default!)) .UseUserManager(); diff --git a/backend/src/GraphQl/DataLoaders.cs b/backend/src/GraphQl/DataLoaders.cs new file mode 100644 index 000000000..cece1343a --- /dev/null +++ b/backend/src/GraphQl/DataLoaders.cs @@ -0,0 +1,121 @@ +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 Metabase.Data; +using Microsoft.EntityFrameworkCore; + +namespace Metabase.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 = await databaseContextFactory.CreateDbContextAsync(cancellationToken); + 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 = await databaseContextFactory.CreateDbContextAsync(cancellationToken); + 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 = await databaseContextFactory.CreateDbContextAsync(cancellationToken); + 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 = await databaseContextFactory.CreateDbContextAsync(cancellationToken); + 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 = await databaseContextFactory.CreateDbContextAsync(cancellationToken); + 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/AppliedMethod.cs b/backend/src/GraphQl/DataX/AppliedMethod.cs index 6c17d7881..3040de45f 100644 --- a/backend/src/GraphQl/DataX/AppliedMethod.cs +++ b/backend/src/GraphQl/DataX/AppliedMethod.cs @@ -7,16 +7,12 @@ namespace Metabase.GraphQl.DataX; -public sealed class AppliedMethod( - Guid methodId, - IReadOnlyList arguments, - IReadOnlyList sources +public sealed record AppliedMethod( + Guid MethodId, + IReadOnlyList Arguments, + IReadOnlyList Sources ) { - public Guid MethodId { get; } = methodId; - public IReadOnlyList Arguments { get; } = arguments; - public IReadOnlyList Sources { get; } = sources; - public Task GetMethodAsync( MethodByIdDataLoader methodById, CancellationToken cancellationToken diff --git a/backend/src/GraphQl/DataX/CalorimetricData.cs b/backend/src/GraphQl/DataX/CalorimetricData.cs deleted file mode 100644 index 240f253e2..000000000 --- a/backend/src/GraphQl/DataX/CalorimetricData.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections.Generic; -using NodaTime; - -namespace Metabase.GraphQl.DataX; - -public sealed class CalorimetricData( - string id, - Guid uuid, - OffsetDateTime timestamp, - string locale, - Guid databaseId, - Guid componentId, - string? name, - string? description, - IReadOnlyList warnings, - Guid creatorId, - OffsetDateTime createdAt, - AppliedMethod appliedMethod, - IReadOnlyList resources, - GetHttpsResourceTree resourceTree, - IReadOnlyList approvals, - // ResponseApproval approval - IReadOnlyList gValues, - IReadOnlyList uValues - ) - : Data( - id, - uuid, - timestamp, - locale, - databaseId, - componentId, - name, - description, - warnings, - creatorId, - createdAt, - appliedMethod, - resources, - resourceTree, - approvals - ) -{ - public override DataKind Kind { get => DataKind.CALORIMETRIC_DATA; } - public IReadOnlyList GValues { get; } = gValues; - public IReadOnlyList UValues { get; } = uValues; -} \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/CrossDatabaseDataReference.cs b/backend/src/GraphQl/DataX/CrossDatabaseDataReference.cs index f0435fb64..01d3d3e0d 100644 --- a/backend/src/GraphQl/DataX/CrossDatabaseDataReference.cs +++ b/backend/src/GraphQl/DataX/CrossDatabaseDataReference.cs @@ -2,25 +2,20 @@ using System.Threading; using System.Threading.Tasks; using Metabase.Data; -using Metabase.GraphQl.Institutions; +using Metabase.GraphQl.Databases; using NodaTime; namespace Metabase.GraphQl.DataX; -public sealed class CrossDatabaseDataReference( - Guid dataId, - OffsetDateTime dataTimestamp, - DataKind dataKind, - Guid databaseId - ) +public sealed record CrossDatabaseDataReference( + Guid DataId, + OffsetDateTime DataTimestamp, + DataKind DataKind, + Guid DatabaseId +) { - public Guid DataId { get; } = dataId; - public OffsetDateTime DataTimestamp { get; } = dataTimestamp; - public DataKind DataKind { get; } = dataKind; - public Guid DatabaseId { get; } = databaseId; - - public Task GetDatabaseAsync( - InstitutionByIdDataLoader databaseById, + public Task GetDatabaseAsync( + DatabaseByIdDataLoader databaseById, CancellationToken cancellationToken ) { diff --git a/backend/src/GraphQl/DataX/Data.cs b/backend/src/GraphQl/DataX/Data.cs index 13050f62b..fe0b9f8c1 100644 --- a/backend/src/GraphQl/DataX/Data.cs +++ b/backend/src/GraphQl/DataX/Data.cs @@ -1,63 +1,81 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using HotChocolate; using HotChocolate.Types; using HotChocolate.Types.Relay; using Metabase.Data; +using Metabase.Extensions; using Metabase.GraphQl.Components; using Metabase.GraphQl.Databases; using Metabase.GraphQl.Institutions; +using Metabase.GraphQl.Scalars; +using Microsoft.EntityFrameworkCore; using NodaTime; namespace Metabase.GraphQl.DataX; -public abstract class Data( - string id, - Guid uuid, - OffsetDateTime timestamp, - string locale, - Guid databaseId, - Guid componentId, - string? name, - string? description, - IReadOnlyList warnings, - Guid creatorId, - OffsetDateTime createdAt, - AppliedMethod appliedMethod, - IReadOnlyList resources, - GetHttpsResourceTree resourceTree, - IReadOnlyList approvals - // ResponseApproval approval - ) - : IData +public abstract partial record Data( + [property: GraphQLIgnore] string DataId, + Guid Uuid, + OffsetDateTime Timestamp, + [property: GraphQLType>] string Locale, + Guid DatabaseId, + Guid ComponentId, + string? Name, + string? Description, + IReadOnlyList Warnings, + Guid CreatorId, + OffsetDateTime CreatedAt, + AppliedMethod AppliedMethod, + IReadOnlyList Resources, + GetHttpsResourceTree ResourceTree, + IReadOnlyList Approvals +// ResponseApproval approval +) +: IData { - [ID] - public string Id { get; } = id; + [GeneratedRegex("^(?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}):(?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}):(?[^:]*):(?.+)$", RegexOptions.IgnoreCase)] + private static partial Regex IdRegex(); - public abstract DataKind Kind { get; } + public static async Task FetchNodeAsync( + [ID] string id, + Func> getDataAsync, + ApplicationDbContext databaseContext, + CancellationToken cancellationToken + ) + where TData : class, IData + { + var match = IdRegex().Match(id); + if (match is null || !match.Success) + { + return null; + } + var databaseId = new Guid(match.Groups["databaseId"].Value); + var uuid = new Guid(match.Groups["uuid"].Value); + var locale = match.Groups["locale"].Value.NullIfWhitespace(); + // TODO use only the following dataId instead of uuid and locale and use the product-data database's query `node(id: ID)` instead of `*Data(id: Uuid)`: var dataId = match.Groups["dataId"].Value; + var database = await databaseContext.Databases.AsNoTracking() + .Where(_ => _.Id == databaseId) + .SingleOrDefaultAsync(cancellationToken); + if (database is null) + { + return null; + } + return await getDataAsync(database, uuid, locale); + } - [GraphQLType>] - public string Locale { get; } = locale; + [ID] + public string Id => $"{DatabaseId}:{Uuid}:{Locale ?? ""}:{DataId}".Base64Encode(); - public IReadOnlyList Warnings { get; } = warnings; - public Guid CreatorId { get; } = creatorId; - public OffsetDateTime CreatedAt { get; } = createdAt; - public IReadOnlyList Resources { get; } = resources; - public Guid Uuid { get; } = uuid; - public OffsetDateTime Timestamp { get; } = timestamp; - public Guid DatabaseId { get; } = databaseId; - public Guid ComponentId { get; } = componentId; - public string? Name { get; } = name; - public string? Description { get; } = description; - public AppliedMethod AppliedMethod { get; } = appliedMethod; - public GetHttpsResourceTree ResourceTree { get; } = resourceTree; - public IReadOnlyList Approvals { get; } = approvals; + public abstract DataKind Kind { get; } public Task GetDatabaseAsync( - DatabaseByIdDataLoader databaseById, - CancellationToken cancellationToken + IDatabaseByIdDataLoader databaseById, + CancellationToken cancellationToken ) { return databaseById.LoadAsync( @@ -67,7 +85,7 @@ CancellationToken cancellationToken } public Task GetComponentAsync( - ComponentByIdDataLoader componentById, + IComponentByIdDataLoader componentById, CancellationToken cancellationToken ) { @@ -78,7 +96,7 @@ CancellationToken cancellationToken } public Task GetCreatorAsync( - InstitutionByIdDataLoader institutionById, + IInstitutionByIdDataLoader institutionById, CancellationToken cancellationToken ) { diff --git a/backend/src/GraphQl/DataX/DataApproval.cs b/backend/src/GraphQl/DataX/DataApproval.cs index 915f5d5fe..9e8be5735 100644 --- a/backend/src/GraphQl/DataX/DataApproval.cs +++ b/backend/src/GraphQl/DataX/DataApproval.cs @@ -2,35 +2,29 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.Types; using Metabase.Data; using Metabase.GraphQl.Institutions; +using Metabase.GraphQl.Scalars; using NodaTime; namespace Metabase.GraphQl.DataX; -public sealed class DataApproval( - OffsetDateTime timestamp, - string signature, - string keyFingerprint, - string query, - JsonElement variables, - string message, - Guid approverId, - IReference statement - ) - : IApproval +public sealed record DataApproval( + OffsetDateTime Timestamp, + string Signature, + string KeyFingerprint, + [property: GraphQLType>] string Query, + JsonElement Variables, + string Message, + Guid ApproverId, + IReference Statement +) +: IApproval { - public Guid ApproverId { get; } = approverId; - public OffsetDateTime Timestamp { get; } = timestamp; - public string Signature { get; } = signature; - public string KeyFingerprint { get; } = keyFingerprint; - public string Query { get; } = query; - public JsonElement Variables { get; } = variables; - public string Message { get; } = message; - public IReference Statement { get; private set; } = statement; - public Task GetApproverAsync( - InstitutionByIdDataLoader institutionById, + IInstitutionByIdDataLoader institutionById, CancellationToken cancellationToken ) { diff --git a/backend/src/GraphQl/DataX/DataConnection.cs b/backend/src/GraphQl/DataX/DataConnection.cs index 95bca54a8..952d276fd 100644 --- a/backend/src/GraphQl/DataX/DataConnection.cs +++ b/backend/src/GraphQl/DataX/DataConnection.cs @@ -1,16 +1,11 @@ -using System; using System.Collections.Generic; -using System.Linq; +using HotChocolate.CostAnalysis.Types; using HotChocolate.Types.Pagination; namespace Metabase.GraphQl.DataX; -public sealed record DataConnection( - IReadOnlyList Edges, - uint TotalCount, - ConnectionPageInfo PageInfo -) : DataConnectionBase( - Edges, - TotalCount, - PageInfo +public abstract record DataConnection( + [property: Cost(0)] IReadOnlyList? Edges, + [property: Cost(0)] int TotalCount, + [property: Cost(0)] ConnectionPageInfo PageInfo ); \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/DataConnectionBase.cs b/backend/src/GraphQl/DataX/DataConnectionBase.cs deleted file mode 100644 index 30cc4612e..000000000 --- a/backend/src/GraphQl/DataX/DataConnectionBase.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; -using HotChocolate.Types.Pagination; - -namespace Metabase.GraphQl.DataX; - -public abstract record DataConnectionBase( - IReadOnlyList Edges, - uint TotalCount, - ConnectionPageInfo PageInfo -); \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/DataEdge.cs b/backend/src/GraphQl/DataX/DataEdge.cs index de2709cc6..33ecf20d3 100644 --- a/backend/src/GraphQl/DataX/DataEdge.cs +++ b/backend/src/GraphQl/DataX/DataEdge.cs @@ -1,9 +1,8 @@ +using HotChocolate.CostAnalysis.Types; + namespace Metabase.GraphQl.DataX; -public sealed record DataEdge( - string Cursor, - IData Node -) : DataEdgeBase( - Cursor, - Node +public abstract record DataEdge( + [property: Cost(0)] string Cursor, + [property: Cost(0)] TData Node ); \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/DataEdgeBase.cs b/backend/src/GraphQl/DataX/DataEdgeBase.cs deleted file mode 100644 index 78da81a18..000000000 --- a/backend/src/GraphQl/DataX/DataEdgeBase.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Metabase.GraphQl.DataX; - -public abstract record DataEdgeBase( - string Cursor, - TData Node -); \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/DataPropositionInput.cs b/backend/src/GraphQl/DataX/DataPropositionInput.cs index 3eb30e29a..f086d3a16 100644 --- a/backend/src/GraphQl/DataX/DataPropositionInput.cs +++ b/backend/src/GraphQl/DataX/DataPropositionInput.cs @@ -1,5 +1,11 @@ using System.Collections.Generic; using System.Linq; +using Metabase.GraphQl.CalorimetricDataX; +using Metabase.GraphQl.GeometricDataX; +using Metabase.GraphQl.HygrothermalDataX; +using Metabase.GraphQl.LifeCycleDataX; +using Metabase.GraphQl.OpticalDataX; +using Metabase.GraphQl.PhotovoltaicDataX; namespace Metabase.GraphQl.DataX; diff --git a/backend/src/GraphQl/DataX/FileMetaInformation.cs b/backend/src/GraphQl/DataX/FileMetaInformation.cs index 782db6b45..1bdc7af6d 100644 --- a/backend/src/GraphQl/DataX/FileMetaInformation.cs +++ b/backend/src/GraphQl/DataX/FileMetaInformation.cs @@ -7,14 +7,11 @@ namespace Metabase.GraphQl.DataX; -public sealed class FileMetaInformation( - IReadOnlyList path, - Guid dataFormatId - ) +public sealed record FileMetaInformation( + IReadOnlyList Path, + Guid DataFormatId +) { - public IReadOnlyList Path { get; } = path; - public Guid DataFormatId { get; } = dataFormatId; - public Task GetDataFormatAsync( DataFormatByIdDataLoader dataFormatById, CancellationToken cancellationToken diff --git a/backend/src/GraphQl/DataX/GeometricData.cs b/backend/src/GraphQl/DataX/GeometricData.cs deleted file mode 100644 index 95da1579c..000000000 --- a/backend/src/GraphQl/DataX/GeometricData.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using NodaTime; - -namespace Metabase.GraphQl.DataX; - -public sealed class GeometricData( - string id, - Guid uuid, - OffsetDateTime timestamp, - string locale, - Guid databaseId, - Guid componentId, - string? name, - string? description, - IReadOnlyList warnings, - Guid creatorId, - OffsetDateTime createdAt, - AppliedMethod appliedMethod, - IReadOnlyList resources, - GetHttpsResourceTree resourceTree, - IReadOnlyList approvals, - // ResponseApproval approval, - IReadOnlyList thicknesses - ) - : Data( - id, - uuid, - timestamp, - locale, - databaseId, - componentId, - name, - description, - warnings, - creatorId, - createdAt, - appliedMethod, - resources, - resourceTree, - approvals - // approval - ) -{ - public override DataKind Kind { get => DataKind.GEOMETRIC_DATA; } - public IReadOnlyList Thicknesses { get; } = thicknesses; -} \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/GetHttpsResource.cs b/backend/src/GraphQl/DataX/GetHttpsResource.cs index 707987977..b5e872bc8 100644 --- a/backend/src/GraphQl/DataX/GetHttpsResource.cs +++ b/backend/src/GraphQl/DataX/GetHttpsResource.cs @@ -7,20 +7,14 @@ namespace Metabase.GraphQl.DataX; -public sealed class GetHttpsResource( - string? description, - string hashValue, - Uri locator, - Guid dataFormatId, - IReadOnlyList archivedFilesMetaInformation - ) +public sealed record GetHttpsResource( + string? Description, + string HashValue, + Uri Locator, + Guid DataFormatId, + IReadOnlyList ArchivedFilesMetaInformation +) { - public string? Description { get; } = description; - public string HashValue { get; } = hashValue; - public Uri Locator { get; } = locator; - public Guid DataFormatId { get; } = dataFormatId; - public IReadOnlyList ArchivedFilesMetaInformation { get; } = archivedFilesMetaInformation; - public Task GetDataFormatAsync( DataFormatByIdDataLoader dataFormatById, CancellationToken cancellationToken diff --git a/backend/src/GraphQl/DataX/GetHttpsResourceTreeNonRootVertex.cs b/backend/src/GraphQl/DataX/GetHttpsResourceTreeNonRootVertex.cs index 6dd6b3c65..5a50e1948 100644 --- a/backend/src/GraphQl/DataX/GetHttpsResourceTreeNonRootVertex.cs +++ b/backend/src/GraphQl/DataX/GetHttpsResourceTreeNonRootVertex.cs @@ -1,9 +1,12 @@ +using HotChocolate; +using HotChocolate.Types; + namespace Metabase.GraphQl.DataX; public sealed record GetHttpsResourceTreeNonRootVertex( - string VertexId, + [property: GraphQLType>] string VertexId, GetHttpsResource Value, - string ParentId, + [property: GraphQLType>] string ParentId, ToTreeVertexAppliedConversionMethod AppliedConversionMethod ) : IGetHttpsResourceTreeVertex; \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/GetHttpsResourceTreeRoot.cs b/backend/src/GraphQl/DataX/GetHttpsResourceTreeRoot.cs index 402dfbb4f..fe6b6887d 100644 --- a/backend/src/GraphQl/DataX/GetHttpsResourceTreeRoot.cs +++ b/backend/src/GraphQl/DataX/GetHttpsResourceTreeRoot.cs @@ -1,8 +1,9 @@ -using System; +using HotChocolate; +using HotChocolate.Types; namespace Metabase.GraphQl.DataX; public sealed record GetHttpsResourceTreeRoot( - string VertexId, + [property: GraphQLType>] string VertexId, GetHttpsResource Value ) : IGetHttpsResourceTreeVertex; \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/HygrothermalData.cs b/backend/src/GraphQl/DataX/HygrothermalData.cs deleted file mode 100644 index 53834b0e2..000000000 --- a/backend/src/GraphQl/DataX/HygrothermalData.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using NodaTime; - -namespace Metabase.GraphQl.DataX; - -public sealed class HygrothermalData( - string id, - Guid uuid, - OffsetDateTime timestamp, - string locale, - Guid databaseId, - Guid componentId, - string? name, - string? description, - IReadOnlyList warnings, - Guid creatorId, - OffsetDateTime createdAt, - AppliedMethod appliedMethod, - IReadOnlyList resources, - GetHttpsResourceTree resourceTree, - IReadOnlyList approvals - // ResponseApproval approval - ) - : Data( - id, - uuid, - timestamp, - locale, - databaseId, - componentId, - name, - description, - warnings, - creatorId, - createdAt, - appliedMethod, - resources, - resourceTree, - approvals - ) -{ - public override DataKind Kind { get => DataKind.HYGROTHERMAL_DATA; } -} \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/IApproval.cs b/backend/src/GraphQl/DataX/IApproval.cs index 4121a6f3d..897dfafb6 100644 --- a/backend/src/GraphQl/DataX/IApproval.cs +++ b/backend/src/GraphQl/DataX/IApproval.cs @@ -1,8 +1,8 @@ using System; using System.Text.Json; -using System.Text.Json.Serialization; +using HotChocolate; using HotChocolate.Types; -using Metabase.Configuration; +using Metabase.GraphQl.Scalars; using NodaTime; namespace Metabase.GraphQl.DataX; @@ -13,7 +13,7 @@ public interface IApproval OffsetDateTime Timestamp { get; } string Signature { get; } string KeyFingerprint { get; } - string Query { get; } + [GraphQLType>] string Query { get; } JsonElement Variables { get; } string Message { get; } Guid ApproverId { get; } diff --git a/backend/src/GraphQl/DataX/IData.cs b/backend/src/GraphQl/DataX/IData.cs index 552da97a3..af407609a 100644 --- a/backend/src/GraphQl/DataX/IData.cs +++ b/backend/src/GraphQl/DataX/IData.cs @@ -1,8 +1,21 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; using HotChocolate; using HotChocolate.Types; +using Metabase.Data; +using Metabase.GraphQl.CalorimetricDataX; +using Metabase.GraphQl.Components; +using Metabase.GraphQl.Databases; +using Metabase.GraphQl.GeometricDataX; +using Metabase.GraphQl.HygrothermalDataX; +using Metabase.GraphQl.Institutions; +using Metabase.GraphQl.LifeCycleDataX; +using Metabase.GraphQl.OpticalDataX; +using Metabase.GraphQl.PhotovoltaicDataX; +using Metabase.GraphQl.Scalars; using NodaTime; namespace Metabase.GraphQl.DataX; @@ -17,6 +30,9 @@ namespace Metabase.GraphQl.DataX; [JsonDerivedType(typeof(PhotovoltaicData), typeDiscriminator: nameof(PhotovoltaicData))] public interface IData { + [GraphQLType>] + string Id { get; } + Guid Uuid { get; } DataKind Kind { get; } OffsetDateTime Timestamp { get; } @@ -35,4 +51,19 @@ public interface IData [GraphQLType>] string Locale { get; } + + public Task GetDatabaseAsync( + IDatabaseByIdDataLoader databaseById, + CancellationToken cancellationToken + ); + + public Task GetComponentAsync( + IComponentByIdDataLoader componentById, + CancellationToken cancellationToken + ); + + public Task GetCreatorAsync( + IInstitutionByIdDataLoader institutionById, + CancellationToken cancellationToken + ); } \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/IGetHttpsResourceTreeVertex.cs b/backend/src/GraphQl/DataX/IGetHttpsResourceTreeVertex.cs index 7304ed573..677ab8dfa 100644 --- a/backend/src/GraphQl/DataX/IGetHttpsResourceTreeVertex.cs +++ b/backend/src/GraphQl/DataX/IGetHttpsResourceTreeVertex.cs @@ -1,3 +1,4 @@ +using HotChocolate; using HotChocolate.Types; namespace Metabase.GraphQl.DataX; @@ -5,6 +6,6 @@ namespace Metabase.GraphQl.DataX; [InterfaceType("GetHttpsResourceTreeVertex")] public interface IGetHttpsResourceTreeVertex { - // string VertexId { get; } + [property: GraphQLType>] string VertexId { get; } GetHttpsResource Value { get; } } \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/LifeCycleData.cs b/backend/src/GraphQl/DataX/LifeCycleData.cs deleted file mode 100644 index 02cf21d69..000000000 --- a/backend/src/GraphQl/DataX/LifeCycleData.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using NodaTime; - -namespace Metabase.GraphQl.DataX; - -public sealed class LifeCycleData( - string id, - Guid uuid, - OffsetDateTime timestamp, - string locale, - Guid databaseId, - Guid componentId, - string? name, - string? description, - IReadOnlyList warnings, - Guid creatorId, - OffsetDateTime createdAt, - AppliedMethod appliedMethod, - IReadOnlyList resources, - GetHttpsResourceTree resourceTree, - IReadOnlyList approvals - // ResponseApproval approval - ) - : Data( - id, - uuid, - timestamp, - locale, - databaseId, - componentId, - name, - description, - warnings, - creatorId, - createdAt, - appliedMethod, - resources, - resourceTree, - approvals - ) -{ - public override DataKind Kind { get => DataKind.LIFE_CYCLE_DATA; } -} \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/OpenEndedDateTimeRange.cs b/backend/src/GraphQl/DataX/OpenEndedDateTimeRange.cs index fbdd28397..675ff14c1 100644 --- a/backend/src/GraphQl/DataX/OpenEndedDateTimeRange.cs +++ b/backend/src/GraphQl/DataX/OpenEndedDateTimeRange.cs @@ -1,4 +1,3 @@ -using System; using NodaTime; namespace Metabase.GraphQl.DataX; diff --git a/backend/src/GraphQl/DataX/OpticalData.cs b/backend/src/GraphQl/DataX/OpticalData.cs deleted file mode 100644 index fe5e1b40b..000000000 --- a/backend/src/GraphQl/DataX/OpticalData.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Collections.Generic; -using NodaTime; - -namespace Metabase.GraphQl.DataX; - -public sealed class OpticalData( - string id, - Guid uuid, - OffsetDateTime timestamp, - string locale, - Guid databaseId, - Guid componentId, - string? name, - string? description, - IReadOnlyList warnings, - Guid creatorId, - OffsetDateTime createdAt, - OpticalComponentType? type, - OpticalComponentSubtype? subtype, - CoatedSide? coatedSide, - AppliedMethod appliedMethod, - IReadOnlyList resources, - GetHttpsResourceTree resourceTree, - IReadOnlyList approvals, - // ResponseApproval approval - IReadOnlyList nearnormalHemisphericalVisibleTransmittances, - IReadOnlyList nearnormalHemisphericalVisibleReflectances, - IReadOnlyList nearnormalHemisphericalSolarTransmittances, - IReadOnlyList nearnormalHemisphericalSolarReflectances, - IReadOnlyList infraredEmittances, - IReadOnlyList colorRenderingIndices, - IReadOnlyList cielabColors - ) - : Data( - id, - uuid, - timestamp, - locale, - databaseId, - componentId, - name, - description, - warnings, - creatorId, - createdAt, - appliedMethod, - resources, - resourceTree, - approvals - ) -{ - public override DataKind Kind { get => DataKind.OPTICAL_DATA; } - public OpticalComponentType? Type { get; } = type; - public OpticalComponentSubtype? Subtype { get; } = subtype; - public CoatedSide? CoatedSide { get; } = coatedSide; - public IReadOnlyList NearnormalHemisphericalVisibleTransmittances { get; } = nearnormalHemisphericalVisibleTransmittances; - public IReadOnlyList NearnormalHemisphericalVisibleReflectances { get; } = nearnormalHemisphericalVisibleReflectances; - public IReadOnlyList NearnormalHemisphericalSolarTransmittances { get; } = nearnormalHemisphericalSolarTransmittances; - public IReadOnlyList NearnormalHemisphericalSolarReflectances { get; } = nearnormalHemisphericalSolarReflectances; - - public IReadOnlyList InfraredEmittances { get; } = infraredEmittances; - - public IReadOnlyList ColorRenderingIndices { get; } = colorRenderingIndices; - public IReadOnlyList CielabColors { get; } = cielabColors; -} \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/PhotovoltaicData.cs b/backend/src/GraphQl/DataX/PhotovoltaicData.cs deleted file mode 100644 index ac1e93a0b..000000000 --- a/backend/src/GraphQl/DataX/PhotovoltaicData.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using NodaTime; - -namespace Metabase.GraphQl.DataX; - -public sealed class PhotovoltaicData( - string id, - Guid uuid, - OffsetDateTime timestamp, - string locale, - Guid databaseId, - Guid componentId, - string? name, - string? description, - IReadOnlyList warnings, - Guid creatorId, - OffsetDateTime createdAt, - AppliedMethod appliedMethod, - IReadOnlyList resources, - GetHttpsResourceTree resourceTree, - IReadOnlyList approvals - // ResponseApproval approval - ) - : Data( - id, - uuid, - timestamp, - locale, - databaseId, - componentId, - name, - description, - warnings, - creatorId, - createdAt, - appliedMethod, - resources, - resourceTree, - approvals - ) -{ - public override DataKind Kind { get => DataKind.PHOTOVOLTAIC_DATA; } -} \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/ResponseApproval.cs b/backend/src/GraphQl/DataX/ResponseApproval.cs index f1059e5fa..1436ab314 100644 --- a/backend/src/GraphQl/DataX/ResponseApproval.cs +++ b/backend/src/GraphQl/DataX/ResponseApproval.cs @@ -1,5 +1,8 @@ using System; using System.Text.Json; +using HotChocolate; +using HotChocolate.Types; +using Metabase.GraphQl.Scalars; using NodaTime; namespace Metabase.GraphQl.DataX; @@ -8,7 +11,7 @@ public sealed record ResponseApproval( OffsetDateTime Timestamp, string Signature, string KeyFingerprint, - string Query, + [property: GraphQLType>] string Query, JsonElement Variables, string Message, Guid ApproverId diff --git a/backend/src/GraphQl/DataX/ToTreeVertexAppliedConversionMethod.cs b/backend/src/GraphQl/DataX/ToTreeVertexAppliedConversionMethod.cs index 4594ff7af..08ca4cc0c 100644 --- a/backend/src/GraphQl/DataX/ToTreeVertexAppliedConversionMethod.cs +++ b/backend/src/GraphQl/DataX/ToTreeVertexAppliedConversionMethod.cs @@ -7,16 +7,12 @@ namespace Metabase.GraphQl.DataX; -public sealed class ToTreeVertexAppliedConversionMethod( - Guid methodId, - IReadOnlyList arguments, - string sourceName - ) +public sealed record ToTreeVertexAppliedConversionMethod( + Guid MethodId, + IReadOnlyList Arguments, + string SourceName +) { - public Guid MethodId { get; } = methodId; - public IReadOnlyList Arguments { get; } = arguments; - public string SourceName { get; } = sourceName; - public Task GetMethodAsync( MethodByIdDataLoader methodById, CancellationToken cancellationToken diff --git a/backend/src/GraphQl/DataX/UuidPropositionInput.cs b/backend/src/GraphQl/DataX/UuidPropositionInput.cs index 1d0c294c8..b5996089e 100644 --- a/backend/src/GraphQl/DataX/UuidPropositionInput.cs +++ b/backend/src/GraphQl/DataX/UuidPropositionInput.cs @@ -4,4 +4,7 @@ namespace Metabase.GraphQl.DataX; public sealed record UuidPropositionInput( Guid? EqualTo -); \ No newline at end of file +) +{ + public static readonly UuidPropositionInput Empty = new(EqualTo: null); +}; \ No newline at end of file diff --git a/backend/src/GraphQl/Databases/DatabaseByIdDataLoader.cs b/backend/src/GraphQl/Databases/DatabaseByIdDataLoader.cs deleted file mode 100644 index d473a9abe..000000000 --- a/backend/src/GraphQl/Databases/DatabaseByIdDataLoader.cs +++ /dev/null @@ -1,20 +0,0 @@ -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Databases; - -public sealed class DatabaseByIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : EntityByIdDataLoader( - batchScheduler, - options, - dbContextFactory, - dbContext => dbContext.Databases - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Databases/DatabaseDataLoaders.cs b/backend/src/GraphQl/Databases/DatabaseDataLoaders.cs new file mode 100644 index 000000000..15512bff7 --- /dev/null +++ b/backend/src/GraphQl/Databases/DatabaseDataLoaders.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut; +using GreenDonut.Data; +using Metabase.Data; +using Microsoft.EntityFrameworkCore; + +namespace Metabase.GraphQl.Databases; + +public sealed class DatabaseDataLoaders +: DataLoaders +{ + [DataLoader] + public static ValueTask> GetDatabaseByIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetEntityByIdAsync( + ids, + databaseContext => databaseContext.Databases, + queryContext, + databaseContextFactory, + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Databases/DatabaseFilterType.cs b/backend/src/GraphQl/Databases/DatabaseFilterType.cs index bb0cf3e94..3869f2924 100644 --- a/backend/src/GraphQl/Databases/DatabaseFilterType.cs +++ b/backend/src/GraphQl/Databases/DatabaseFilterType.cs @@ -5,16 +5,20 @@ namespace Metabase.GraphQl.Databases; public class DatabaseFilterType - : EntityFilterType + : AuditableEntityFilterType { protected override void Configure( IFilterInputTypeDescriptor descriptor ) { base.Configure(descriptor); - descriptor.Field(x => x.Name); - descriptor.Field(x => x.Description); - descriptor.Field(x => x.Locator); - descriptor.Field(x => x.Operator); + // TODO Remove Id, CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(_ => _.Id); + descriptor.Field(_ => _.CreatedAt); + descriptor.Field(_ => _.UpdatedAt); + descriptor.Field(_ => _.Name); + descriptor.Field(_ => _.Description); + descriptor.Field(_ => _.Locator); + descriptor.Field(_ => _.Operator); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Databases/DatabaseMutations.cs b/backend/src/GraphQl/Databases/DatabaseMutations.cs index bf0b7c3e4..8ccef1ca1 100644 --- a/backend/src/GraphQl/Databases/DatabaseMutations.cs +++ b/backend/src/GraphQl/Databases/DatabaseMutations.cs @@ -10,8 +10,8 @@ using Metabase.Authorization; using Metabase.Data; using Metabase.Extensions; +using Metabase.GraphQl.Requests; using Metabase.GraphQl.Users; -using Metabase.Services; using Microsoft.EntityFrameworkCore; namespace Metabase.GraphQl.Databases; @@ -52,7 +52,7 @@ CancellationToken cancellationToken if (!await context.Institutions .AnyAsync( - x => x.Id == input.OperatorId, + _ => _.Id == input.OperatorId, cancellationToken ) ) @@ -237,4 +237,4 @@ await QueryingDatabases.ConstructQuery( } private sealed record VerificationCodeData(string VerificationCode); -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Databases/DatabaseOperatorEdge.cs b/backend/src/GraphQl/Databases/DatabaseOperatorEdge.cs index d7a0a70fd..35ea64ab7 100644 --- a/backend/src/GraphQl/Databases/DatabaseOperatorEdge.cs +++ b/backend/src/GraphQl/Databases/DatabaseOperatorEdge.cs @@ -6,6 +6,6 @@ namespace Metabase.GraphQl.Databases; public sealed class DatabaseOperatorEdge( Database association ) - : Edge(association.OperatorId) + : Edge(association.OperatorId) { } \ No newline at end of file diff --git a/backend/src/GraphQl/Databases/DatabaseQueries.cs b/backend/src/GraphQl/Databases/DatabaseQueries.cs index 1a999935e..37afad608 100644 --- a/backend/src/GraphQl/Databases/DatabaseQueries.cs +++ b/backend/src/GraphQl/Databases/DatabaseQueries.cs @@ -2,9 +2,10 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using GreenDonut.Data; using HotChocolate.Authorization; using HotChocolate.Data; -using HotChocolate.Data.Sorting; +using HotChocolate.Resolvers; using HotChocolate.Types; using Metabase.Authorization; using Metabase.Data; @@ -18,45 +19,62 @@ namespace Metabase.GraphQl.Databases; public sealed class DatabaseQueries { [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 GetDatabases( - ApplicationDbContext context, - ISortingContext sorting + public ValueTask> GetDatabasesAsync( + IResolverContext resolverContext, + ApplicationDbContext databaseContext, + CancellationToken cancellationToken ) { - sorting.StabilizeOrder(); - return - context.Databases.AsNoTracking() - .Where(d => d.VerificationState == DatabaseVerificationState.VERIFIED); + return databaseContext.Databases + .AsNoTracking() + .Where(d => d.VerificationState == DatabaseVerificationState.VERIFIED) + .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] [Authorize(Policy = AuthorizationPolicies.ManageDatabaseScopePolicy)] - public IQueryable GetPendingDatabases( - ApplicationDbContext context, - ISortingContext sorting + public ValueTask> GetPendingDatabasesAsync( + IResolverContext resolverContext, + ApplicationDbContext databaseContext, + CancellationToken cancellationToken + ) + { + return databaseContext.Databases + .AsNoTracking() + .Where(d => d.VerificationState == DatabaseVerificationState.PENDING) + .With(resolverContext.GetQueryContext(), Sorting.DefaultEntityOrder) + .ToPageAsync(resolverContext.GetPagingArguments(), cancellationToken) + .ToConnectionAsync(); + } + + [UsePaging] + [UseFiltering] + [UseSorting] + public ValueTask> GetAnyDatabasesAsync( + IResolverContext resolverContext, + ApplicationDbContext databaseContext, + CancellationToken cancellationToken ) { - sorting.StabilizeOrder(); - return - context.Databases.AsNoTracking() - .Where(d => d.VerificationState == DatabaseVerificationState.PENDING); + return databaseContext.Databases + .AsNoTracking() + .With(resolverContext.GetQueryContext(), Sorting.DefaultEntityOrder) + .ToPageAsync(resolverContext.GetPagingArguments(), cancellationToken) + .ToConnectionAsync(); } public Task GetDatabaseAsync( Guid id, - DatabaseByIdDataLoader databaseById, + IDatabaseByIdDataLoader byId, CancellationToken cancellationToken ) { - return databaseById.LoadAsync( - id, - cancellationToken - ); + return byId.LoadAsync(id, cancellationToken); } } \ No newline at end of file diff --git a/backend/src/GraphQl/Databases/DatabaseResolvers.cs b/backend/src/GraphQl/Databases/DatabaseResolvers.cs index 10ae82cda..a9e2e4288 100644 --- a/backend/src/GraphQl/Databases/DatabaseResolvers.cs +++ b/backend/src/GraphQl/Databases/DatabaseResolvers.cs @@ -1,206 +1,28 @@ using System; -using System.Linq; -using System.Net; -using System.Net.Http; using System.Security.Claims; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using GraphQL; using HotChocolate; +using HotChocolate.CostAnalysis.Types; using HotChocolate.Resolvers; using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.DataX; -using Metabase.Json; -using Metabase.Services; -using Microsoft.Extensions.Logging; +using Metabase.GraphQl.CalorimetricDataX; +using Metabase.GraphQl.GeometricDataX; +using Metabase.GraphQl.HygrothermalDataX; +using Metabase.GraphQl.LifeCycleDataX; +using Metabase.GraphQl.OpticalDataX; +using Metabase.GraphQl.PhotovoltaicDataX; +using Metabase.GraphQl.Requests; +using Metabase.GraphQl.Scalars; namespace Metabase.GraphQl.Databases; -public static partial class Log -{ - [LoggerMessage( - Level = LogLevel.Warning, - Message = "Failed with errors {Errors} to query the database {Locator} for {Request}.")] - public static partial void FailedWithErrors( - this ILogger logger, - string Errors, - Uri Locator, - string Request - ); - - [LoggerMessage( - Level = LogLevel.Error, - Message = "Failed with status code {StatusCode} to request {Locator} for {Request}.")] - public static partial void FailedWithStatusCode( - this ILogger logger, - Exception exception, - HttpStatusCode? StatusCode, - Uri Locator, - string Request - ); - - [LoggerMessage( - Level = LogLevel.Error, - Message = - "Failed to deserialize GraphQL response of request to {Locator} for {Request}. The details given are: Zero-based number of bytes read within the current line before the exception are {BytePositionInLine}, zero-based number of lines read before the exception are {LineNumber}, message that describes the current exception is '{Message}', path within the JSON where the exception was encountered is {Path}.")] - public static partial void FailedToDeserialize( - this ILogger logger, - Exception exception, - Uri Locator, - string Request, - long? BytePositionInLine, - long? LineNumber, - string Message, - string? Path - ); - - [LoggerMessage( - Level = LogLevel.Error, - Message = "Failed to request {Locator} for {Request} or failed to deserialize the response.")] - public static partial void FailedToRequestOrDeserialize( - this ILogger logger, - Exception exception, - Uri Locator, - string Request - ); -} - public sealed class DatabaseResolvers( - AppSettings appSettings, - ILogger logger + DataQueries dataQueries ) { - private const string IgsdbUrl = "https://igsdb-v2.herokuapp.com/graphql/"; - private const string IgsdbStagingUrl = "https://igsdb-v2-staging.herokuapp.com/graphql/"; - - private static readonly string[] s_calorimetricDataFileNames = - [ - "DataFields.graphql", - "CalorimetricDataFields.graphql", - "CalorimetricData.graphql" - ]; - - private static readonly string[] s_geometricDataFileNames = - [ - "DataFields.graphql", - "GeometricDataFields.graphql", - "GeometricData.graphql" - ]; - - private static readonly string[] s_hygrothermalDataFileNames = - [ - "DataFields.graphql", - "HygrothermalDataFields.graphql", - "HygrothermalData.graphql" - ]; - - private static readonly string[] s_lifeCycleDataFileNames = - [ - "DataFields.graphql", - "LifeCycleDataFields.graphql", - "LifeCycleData.graphql" - ]; - - private static readonly string[] s_opticalDataFileNames = - [ - "DataFields.graphql", - "OpticalDataFields.graphql", - "OpticalData.graphql" - ]; - - private static readonly string[] s_photovoltaicDataFileNames = - [ - "DataFields.graphql", - "PhotovoltaicDataFields.graphql", - "PhotovoltaicData.graphql" - ]; - - private static readonly string[] s_allCalorimetricDataFileNames = - [ - "DataFields.graphql", - "CalorimetricDataFields.graphql", - "PageInfoFields.graphql", - "AllCalorimetricData.graphql" - ]; - - private static readonly string[] s_allGeometricDataFileNames = - [ - "DataFields.graphql", - "GeometricDataFields.graphql", - "PageInfoFields.graphql", - "AllGeometricData.graphql" - ]; - - private static readonly string[] s_allHygrothermalDataFileNames = - [ - "DataFields.graphql", - "HygrothermalDataFields.graphql", - "PageInfoFields.graphql", - "AllHygrothermalData.graphql" - ]; - - private static readonly string[] s_allLifeCycleDataFileNames = - [ - "DataFields.graphql", - "LifeCycleDataFields.graphql", - "PageInfoFields.graphql", - "AllLifeCycleData.graphql" - ]; - - private static readonly string[] s_allOpticalDataFileNames = - [ - "DataFields.graphql", - "OpticalDataFields.graphql", - "PageInfoFields.graphql", - "AllOpticalData.graphql" - ]; - - private static readonly string[] s_allPhotovoltaicDataFileNames = - [ - "DataFields.graphql", - "PhotovoltaicDataFields.graphql", - "PageInfoFields.graphql", - "AllPhotovoltaicData.graphql" - ]; - - private static readonly string[] s_hasCalorimetricDataFileNames = - [ - "HasCalorimetricData.graphql" - ]; - - private static readonly string[] s_hasGeometricDataFileNames = - [ - "HasGeometricData.graphql" - ]; - - private static readonly string[] s_hasHygrothermalDataFileNames = - [ - "HasHygrothermalData.graphql" - ]; - - private static readonly string[] s_hasLifeCycleDataFileNames = - [ - "HasLifeCycleData.graphql" - ]; - - private static readonly string[] s_hasOpticalDataFileNames = - [ - "HasOpticalData.graphql" - ]; - - private static readonly string[] s_hasPhotovoltaicDataFileNames = - [ - "HasPhotovoltaicData.graphql" - ]; - - private static bool IsIgsdbDatabase(Database database) - { - return new[] { IgsdbUrl, IgsdbStagingUrl } - .Contains(database.Locator.AbsoluteUri); - } - public Task IsAuthorizedToUpdateNodeAsync( [Parent] Database database, ClaimsPrincipal claimsPrincipal, @@ -221,737 +43,291 @@ CancellationToken cancellationToken return authorization.IsAuthorizedToVerify(claimsPrincipal, database.Id, cancellationToken); } - public async Task GetDataAsync( + public Task GetDataAsync( [Parent] Database database, Guid id, DataKind kind, - string? locale, - QueryingDatabases queryingDatabases, + [GraphQLType] string? locale, IResolverContext resolverContext, CancellationToken cancellationToken ) { - return kind switch - { - DataKind.CALORIMETRIC_DATA => await GetCalorimetricDataAsync(database, id, locale, queryingDatabases, resolverContext, cancellationToken), - DataKind.GEOMETRIC_DATA => await GetGeometricDataAsync(database, id, locale, queryingDatabases, resolverContext, cancellationToken), - DataKind.HYGROTHERMAL_DATA => await GetHygrothermalDataAsync(database, id, locale, queryingDatabases, resolverContext, cancellationToken), - DataKind.LIFE_CYCLE_DATA => await GetLifeCycleDataAsync(database, id, locale, queryingDatabases, resolverContext, cancellationToken), - DataKind.OPTICAL_DATA => await GetOpticalDataAsync(database, id, locale, queryingDatabases, resolverContext, cancellationToken), - DataKind.PHOTOVOLTAIC_DATA => await GetPhotovoltaicDataAsync(database, id, locale, queryingDatabases, resolverContext, cancellationToken), - _ => throw new ArgumentOutOfRangeException($"The data kind {kind} is not supported.") - }; + return dataQueries.GetDataAsync(database, id, kind, locale, resolverContext, cancellationToken); } - public async Task HasDataAsync( + public Task HasDataAsync( [Parent] Database database, DataKind kind, - DataPropositionInput dataPropositionInput, - string? locale, - QueryingDatabases queryingDatabases, + DataPropositionInput? where, + [GraphQLType] string? locale, IResolverContext resolverContext, CancellationToken cancellationToken ) { - return kind switch - { - DataKind.CALORIMETRIC_DATA => await HasCalorimetricDataAsync(database, dataPropositionInput.ToCalorimetricInput(), locale, queryingDatabases, resolverContext, cancellationToken), - DataKind.GEOMETRIC_DATA => await HasGeometricDataAsync(database, dataPropositionInput.ToGeometricInput(), locale, queryingDatabases, resolverContext, cancellationToken), - DataKind.HYGROTHERMAL_DATA => await HasHygrothermalDataAsync(database, dataPropositionInput.ToHygrothermalInput(), locale, queryingDatabases, resolverContext, cancellationToken), - DataKind.LIFE_CYCLE_DATA => await HasLifeCycleDataAsync(database, dataPropositionInput.ToLifeCycleInput(), locale, queryingDatabases, resolverContext, cancellationToken), - DataKind.OPTICAL_DATA => await HasOpticalDataAsync(database, dataPropositionInput.ToOpticalInput(), locale, queryingDatabases, resolverContext, cancellationToken), - DataKind.PHOTOVOLTAIC_DATA => await HasPhotovoltaicDataAsync(database, dataPropositionInput.ToPhotovoltaiInput(), locale, queryingDatabases, resolverContext, cancellationToken), - _ => throw new ArgumentOutOfRangeException($"The data kind {kind} is not supported.") - }; + return dataQueries.HasDataAsync(database, kind, where, locale, resolverContext, cancellationToken); } - public async Task GetCalorimetricDataAsync( + public Task GetCalorimetricDataAsync( [Parent] Database database, Guid id, - string? locale, - QueryingDatabases queryingDatabases, + [GraphQLType] string? locale, IResolverContext resolverContext, CancellationToken cancellationToken ) { - return (await QueryDatabase( - database, - new GraphQLRequest( - await QueryingDatabases.ConstructQuery( - s_calorimetricDataFileNames - ), - new - { - id, - locale - }, - nameof(CalorimetricData) - ), - queryingDatabases, - resolverContext, - cancellationToken - ) - )?.CalorimetricData; + return dataQueries.GetCalorimetricDataAsync(database, id, locale, resolverContext, cancellationToken); } - public async Task GetGeometricDataAsync( + public Task GetGeometricDataAsync( [Parent] Database database, Guid id, - string? locale, - QueryingDatabases queryingDatabases, + [GraphQLType] string? locale, IResolverContext resolverContext, CancellationToken cancellationToken ) { - return (await QueryDatabase( - database, - new GraphQLRequest( - await QueryingDatabases.ConstructQuery( - s_geometricDataFileNames - ), - new - { - id, - locale - }, - nameof(GeometricData) - ), - queryingDatabases, - resolverContext, - cancellationToken - ) - )?.GeometricData; + return dataQueries.GetGeometricDataAsync(database, id, locale, resolverContext, cancellationToken); } - public async Task GetHygrothermalDataAsync( + public Task GetHygrothermalDataAsync( [Parent] Database database, Guid id, - string? locale, - QueryingDatabases queryingDatabases, + [GraphQLType] string? locale, IResolverContext resolverContext, CancellationToken cancellationToken ) { - return (await QueryDatabase( - database, - new GraphQLRequest( - await QueryingDatabases.ConstructQuery( - s_hygrothermalDataFileNames - ), - new - { - id, - locale - }, - nameof(HygrothermalData) - ), - queryingDatabases, - resolverContext, - cancellationToken - ) - )?.HygrothermalData; + return dataQueries.GetHygrothermalDataAsync(database, id, locale, resolverContext, cancellationToken); } - public async Task GetLifeCycleDataAsync( + public Task GetLifeCycleDataAsync( [Parent] Database database, Guid id, - string? locale, - QueryingDatabases queryingDatabases, + [GraphQLType] string? locale, IResolverContext resolverContext, CancellationToken cancellationToken ) { - return (await QueryDatabase( - database, - new GraphQLRequest( - await QueryingDatabases.ConstructQuery( - s_lifeCycleDataFileNames - ), - new - { - id, - locale - }, - nameof(LifeCycleData) - ), - queryingDatabases, - resolverContext, - cancellationToken - ) - )?.LifeCycleData; + return dataQueries.GetLifeCycleDataAsync(database, id, locale, resolverContext, cancellationToken); } - - public async Task GetOpticalDataAsync( + public Task GetOpticalDataAsync( [Parent] Database database, Guid id, - string? locale, - QueryingDatabases queryingDatabases, + [GraphQLType] string? locale, IResolverContext resolverContext, CancellationToken cancellationToken ) { - return (await QueryDatabase( - database, - new GraphQLRequest( - await QueryingDatabases.ConstructQuery( - s_opticalDataFileNames - ), - new - { - id, - locale - }, - nameof(OpticalData) - ), - queryingDatabases, - resolverContext, - cancellationToken - ) - )?.OpticalData; + return dataQueries.GetOpticalDataAsync(database, id, locale, resolverContext, cancellationToken); } - public async Task GetPhotovoltaicDataAsync( + public Task GetPhotovoltaicDataAsync( [Parent] Database database, Guid id, - string? locale, - QueryingDatabases queryingDatabases, + [GraphQLType] string? locale, IResolverContext resolverContext, CancellationToken cancellationToken ) { - return (await QueryDatabase( - database, - new GraphQLRequest( - await QueryingDatabases.ConstructQuery( - s_photovoltaicDataFileNames - ), - new - { - id, - locale - }, - nameof(PhotovoltaicData) - ), - queryingDatabases, - resolverContext, - cancellationToken - ) - )?.PhotovoltaicData; + return dataQueries.GetPhotovoltaicDataAsync(database, id, locale, resolverContext, cancellationToken); } - public async Task GetAllCalorimetricDataAsync( + [ListSize( + AssumedSize = (int)GraphQlConstants.MaximumPageSize, + SlicingArguments = ["first", "last"], + SlicingArgumentDefaultValue = (int)GraphQlConstants.MaximumPageSize, + SizedFields = ["edges", "nodes"], + RequireOneSlicingArgument = false + )] + public Task GetAllCalorimetricDataAsync( [Parent] Database database, CalorimetricDataPropositionInput? where, - string? locale, - uint? first, + [GraphQLType] string? locale, + int? first, string? after, - uint? last, + int? last, string? before, - QueryingDatabases queryingDatabases, IResolverContext resolverContext, CancellationToken cancellationToken ) { - return (await QueryDatabase( - database, - new GraphQLRequest( - await QueryingDatabases.ConstructQuery( - s_allCalorimetricDataFileNames - ), - new - { - where, - locale, - first, - after, - last, - before - }, - "AllCalorimetricData" - ), - queryingDatabases, - resolverContext, - cancellationToken - ) - )?.AllCalorimetricData; + return dataQueries.GetAllCalorimetricDataAsync(database, where, locale, first, after, last, before, resolverContext, cancellationToken); } - public async Task GetAllGeometricDataAsync( + [ListSize( + AssumedSize = (int)GraphQlConstants.MaximumPageSize, + SlicingArguments = ["first", "last"], + SlicingArgumentDefaultValue = (int)GraphQlConstants.MaximumPageSize, + SizedFields = ["edges", "nodes"], + RequireOneSlicingArgument = false + )] + public Task GetAllGeometricDataAsync( [Parent] Database database, GeometricDataPropositionInput? where, - string? locale, - uint? first, + [GraphQLType] string? locale, + int? first, string? after, - uint? last, + int? last, string? before, - QueryingDatabases queryingDatabases, IResolverContext resolverContext, CancellationToken cancellationToken ) { - return (await QueryDatabase( - database, - new GraphQLRequest( - await QueryingDatabases.ConstructQuery( - s_allGeometricDataFileNames), - new - { - where, - locale, - first, - after, - last, - before - }, - "AllGeometricData" - ), - queryingDatabases, - resolverContext, - cancellationToken - ) - )?.AllGeometricData; + return dataQueries.GetAllGeometricDataAsync(database, where, locale, first, after, last, before, resolverContext, cancellationToken); } - public async Task GetAllHygrothermalDataAsync( + [ListSize( + AssumedSize = (int)GraphQlConstants.MaximumPageSize, + SlicingArguments = ["first", "last"], + SlicingArgumentDefaultValue = (int)GraphQlConstants.MaximumPageSize, + SizedFields = ["edges", "nodes"], + RequireOneSlicingArgument = false + )] + public Task GetAllHygrothermalDataAsync( [Parent] Database database, HygrothermalDataPropositionInput? where, - string? locale, - uint? first, + [GraphQLType] string? locale, + int? first, string? after, - uint? last, + int? last, string? before, - QueryingDatabases queryingDatabases, IResolverContext resolverContext, CancellationToken cancellationToken ) { - return (await QueryDatabase( - database, - new GraphQLRequest( - await QueryingDatabases.ConstructQuery( - s_allHygrothermalDataFileNames - ), - new - { - where, - locale, - first, - after, - last, - before - }, - "AllHygrothermalData" - ), - queryingDatabases, - resolverContext, - cancellationToken - ) - )?.AllHygrothermalData; + return dataQueries.GetAllHygrothermalDataAsync(database, where, locale, first, after, last, before, resolverContext, cancellationToken); } - public async Task GetAllLifeCycleDataAsync( + [ListSize( + AssumedSize = (int)GraphQlConstants.MaximumPageSize, + SlicingArguments = ["first", "last"], + SlicingArgumentDefaultValue = (int)GraphQlConstants.MaximumPageSize, + SizedFields = ["edges", "nodes"], + RequireOneSlicingArgument = false + )] + public Task GetAllLifeCycleDataAsync( [Parent] Database database, LifeCycleDataPropositionInput? where, - string? locale, - uint? first, + [GraphQLType] string? locale, + int? first, string? after, - uint? last, + int? last, string? before, - QueryingDatabases queryingDatabases, IResolverContext resolverContext, CancellationToken cancellationToken ) { - return (await QueryDatabase( - database, - new GraphQLRequest( - await QueryingDatabases.ConstructQuery( - s_allLifeCycleDataFileNames - ), - new - { - where, - locale, - first, - after, - last, - before - }, - "AllLifeCycleData" - ), - queryingDatabases, - resolverContext, - cancellationToken - ) - )?.AllLifeCycleData; + return dataQueries.GetAllLifeCycleDataAsync(database, where, locale, first, after, last, before, resolverContext, cancellationToken); } - public async Task GetAllOpticalDataAsync( + [ListSize( + AssumedSize = (int)GraphQlConstants.MaximumPageSize, + SlicingArguments = ["first", "last"], + SlicingArgumentDefaultValue = (int)GraphQlConstants.MaximumPageSize, + SizedFields = ["edges", "nodes"], + RequireOneSlicingArgument = false + )] + public Task GetAllOpticalDataAsync( [Parent] Database database, OpticalDataPropositionInput? where, - string? locale, - uint? first, + [GraphQLType] string? locale, + int? first, string? after, - uint? last, + int? last, string? before, - QueryingDatabases queryingDatabases, IResolverContext resolverContext, CancellationToken cancellationToken ) { - return (await QueryDatabase( - database, - new GraphQLRequest( - await QueryingDatabases.ConstructQuery( - s_allOpticalDataFileNames), - new - { - where, - locale, - first, - after, - last, - before - }, - "AllOpticalData" - ), - queryingDatabases, - resolverContext, - cancellationToken - ) - )?.AllOpticalData; + return dataQueries.GetAllOpticalDataAsync(database, where, locale, first, after, last, before, resolverContext, cancellationToken); } - public async Task GetAllPhotovoltaicDataAsync( + [ListSize( + AssumedSize = (int)GraphQlConstants.MaximumPageSize, + SlicingArguments = ["first", "last"], + SlicingArgumentDefaultValue = (int)GraphQlConstants.MaximumPageSize, + SizedFields = ["edges", "nodes"], + RequireOneSlicingArgument = false + )] + public Task GetAllPhotovoltaicDataAsync( [Parent] Database database, PhotovoltaicDataPropositionInput? where, - string? locale, - uint? first, + [GraphQLType] string? locale, + int? first, string? after, - uint? last, + int? last, string? before, - QueryingDatabases queryingDatabases, IResolverContext resolverContext, CancellationToken cancellationToken ) { - return (await QueryDatabase( - database, - new GraphQLRequest( - await QueryingDatabases.ConstructQuery( - s_allPhotovoltaicDataFileNames - ), - new - { - where, - locale, - first, - after, - last, - before - }, - "AllPhotovoltaicData" - ), - queryingDatabases, - resolverContext, - cancellationToken - ) - )?.AllPhotovoltaicData; + return dataQueries.GetAllPhotovoltaicDataAsync(database, where, locale, first, after, last, before, resolverContext, cancellationToken); } - public async Task HasCalorimetricDataAsync( + public Task HasCalorimetricDataAsync( [Parent] Database database, CalorimetricDataPropositionInput? where, - string? locale, - QueryingDatabases queryingDatabases, + [GraphQLType] string? locale, IResolverContext resolverContext, CancellationToken cancellationToken ) { - return (await QueryDatabase( - database, - new GraphQLRequest( - await QueryingDatabases.ConstructQuery( - s_hasCalorimetricDataFileNames - ), - new - { - where, - locale - }, - "HasCalorimetricData" - ), - queryingDatabases, - resolverContext, - cancellationToken - ) - )?.HasCalorimetricData; + return dataQueries.HasCalorimetricDataAsync(database, where, locale, resolverContext, cancellationToken); } - public async Task HasGeometricDataAsync( + public Task HasGeometricDataAsync( [Parent] Database database, GeometricDataPropositionInput? where, - string? locale, - QueryingDatabases queryingDatabases, + [GraphQLType] string? locale, IResolverContext resolverContext, CancellationToken cancellationToken ) { - return (await QueryDatabase( - database, - new GraphQLRequest( - await QueryingDatabases.ConstructQuery( - s_hasGeometricDataFileNames - ), - new - { - where, - locale - }, - "HasGeometricData" - ), - queryingDatabases, - resolverContext, - cancellationToken - ) - )?.HasGeometricData; + return dataQueries.HasGeometricDataAsync(database, where, locale, resolverContext, cancellationToken); } - public async Task HasHygrothermalDataAsync( + public Task HasHygrothermalDataAsync( [Parent] Database database, HygrothermalDataPropositionInput? where, - string? locale, - QueryingDatabases queryingDatabases, + [GraphQLType] string? locale, IResolverContext resolverContext, CancellationToken cancellationToken ) { - return (await QueryDatabase( - database, - new GraphQLRequest( - await QueryingDatabases.ConstructQuery( - s_hasHygrothermalDataFileNames - ), - new - { - where, - locale - }, - "HasHygrothermalData" - ), - queryingDatabases, - resolverContext, - cancellationToken - ) - )?.HasHygrothermalData; + return dataQueries.HasHygrothermalDataAsync(database, where, locale, resolverContext, cancellationToken); } - public async Task HasLifeCycleDataAsync( + public Task HasLifeCycleDataAsync( [Parent] Database database, LifeCycleDataPropositionInput? where, - string? locale, - QueryingDatabases queryingDatabases, + [GraphQLType] string? locale, IResolverContext resolverContext, CancellationToken cancellationToken ) { - return (await QueryDatabase( - database, - new GraphQLRequest( - await QueryingDatabases.ConstructQuery( - s_hasLifeCycleDataFileNames - ), - new - { - where, - locale - }, - "HasLifeCycleData" - ), - queryingDatabases, - resolverContext, - cancellationToken - ) - )?.HasLifeCycleData; + return dataQueries.HasLifeCycleDataAsync(database, where, locale, resolverContext, cancellationToken); } - public async Task HasOpticalDataAsync( + public Task HasOpticalDataAsync( [Parent] Database database, OpticalDataPropositionInput? where, - string? locale, - QueryingDatabases queryingDatabases, + [GraphQLType] string? locale, IResolverContext resolverContext, CancellationToken cancellationToken ) { - return (await QueryDatabase( - database, - new GraphQLRequest( - await QueryingDatabases.ConstructQuery( - s_hasOpticalDataFileNames - ), - new - { - where, - locale - }, - "HasOpticalData" - ), - queryingDatabases, - resolverContext, - cancellationToken - ) - )?.HasOpticalData; + return dataQueries.HasOpticalDataAsync(database, where, locale, resolverContext, cancellationToken); } - public async Task HasPhotovoltaicDataAsync( + public Task HasPhotovoltaicDataAsync( [Parent] Database database, PhotovoltaicDataPropositionInput? where, - string? locale, - QueryingDatabases queryingDatabases, + [GraphQLType] string? locale, IResolverContext resolverContext, CancellationToken cancellationToken ) { - return (await QueryDatabase( - database, - new GraphQLRequest( - await QueryingDatabases.ConstructQuery( - s_hasPhotovoltaicDataFileNames - ), - new - { - where, - locale - }, - "HasPhotovoltaicData" - ), - queryingDatabases, - resolverContext, - cancellationToken - ) - )?.HasPhotovoltaicData; - } - - private async - Task - QueryDatabase( - Database database, - GraphQLRequest request, - QueryingDatabases queryingDatabases, - IResolverContext resolverContext, - CancellationToken cancellationToken - ) - where TGraphQlResponse : class - { - try - { - var deserializedGraphQlResponse = - await queryingDatabases.QueryDatabase( - database, - request, - cancellationToken, - IsIgsdbDatabase(database) ? appSettings.Igsdb.ApiToken : null - ); - if (deserializedGraphQlResponse.Errors?.Length >= 1) - { - logger.FailedWithErrors( - JsonSerializer.Serialize(deserializedGraphQlResponse.Errors), - database.Locator, - JsonSerializer.Serialize(request, JsonSerializerSettings.GraphQl) - ); - foreach (var error in deserializedGraphQlResponse.Errors) - { - var errorBuilder = ErrorBuilder.New() - .SetCode("DATABASE_QUERY_ERROR") - // .SetPath(error.Path) // TODO Add the error path. Just using `error.Path` does not work as it contains non-"GraphQlName"s according to HotChocolate sometimes. - .SetMessage( - $"The GraphQL response received from the database {database.Locator} for the request {JsonSerializer.Serialize(request, JsonSerializerSettings.GraphQl)} reported the error {error.Message}."); - if (error.Extensions is not null) - { - foreach (var (key, value) in error.Extensions) - { - errorBuilder.SetExtension(key, value); - } - } - - // TODO Add `error.Locations` to `errorBuilder`. - resolverContext.ReportError(errorBuilder.Build()); - } - } - - return deserializedGraphQlResponse.Data; - } - catch (HttpRequestException e) - { - logger.FailedWithStatusCode(e, e.StatusCode, database.Locator, - JsonSerializer.Serialize(request, JsonSerializerSettings.GraphQl) - ); - resolverContext.ReportError( - ErrorBuilder.New() - .SetCode("DATABASE_REQUEST_FAILED") - .SetPath(resolverContext.Path) - .SetMessage($"Failed with status code {e.StatusCode} to request {database.Locator} for {JsonSerializer.Serialize(request, JsonSerializerSettings.GraphQl)}.") - .SetException(e) - .Build() - ); - return null; - } - catch (JsonException e) - { - logger.FailedToDeserialize(e, database.Locator, - JsonSerializer.Serialize(request, JsonSerializerSettings.GraphQl), - e.BytePositionInLine, - e.LineNumber, - e.Message, - e.Path - ); - resolverContext.ReportError( - ErrorBuilder.New() - .SetCode("DESERIALIZATION_FAILED") - .SetPath(resolverContext.Path) // TODO Add the error path. I would do it as follows as a workaround, however splitting the path at '.' is wrong in general: .SetPath(resolverContext.Path.ToList().Concat(e.Path?.Split('.') ?? []).ToList()) - .SetMessage($"Failed to deserialize GraphQL response of request to {database.Locator} for {JsonSerializer.Serialize(request, JsonSerializerSettings.GraphQl)}. The details given are: Zero-based number of bytes read within the current line before the exception are {e.BytePositionInLine}, zero-based number of lines read before the exception are {e.LineNumber}, message that describes the current exception is '{e.Message}', path within the JSON where the exception was encountered is {e.Path}.") - .SetException(e) - .Build() - ); - return null; - } - catch (Exception exception) - { - logger.FailedToRequestOrDeserialize( - exception, - database.Locator, - JsonSerializer.Serialize(request, JsonSerializerSettings.GraphQl) - ); - resolverContext.ReportError( - ErrorBuilder.New() - .SetCode("DATABASE_REQUEST_FAILED") - .SetPath(resolverContext.Path) - .SetMessage($"Failed to request {database.Locator} for {JsonSerializer.Serialize(request, JsonSerializerSettings.GraphQl)} or failed to deserialize the response.") - .SetException(exception) - .Build() - ); - return null; - } + return dataQueries.HasPhotovoltaicDataAsync(database, where, locale, resolverContext, cancellationToken); } - - private sealed record OpticalDataData(OpticalData OpticalData); - private sealed record HygrothermalDataData(HygrothermalData HygrothermalData); - private sealed record LifeCycleDataData(LifeCycleData LifeCycleData); - private sealed record CalorimetricDataData(CalorimetricData CalorimetricData); - private sealed record PhotovoltaicDataData(PhotovoltaicData PhotovoltaicData); - private sealed record GeometricDataData(GeometricData GeometricData); - private sealed record AllOpticalDataData(OpticalDataConnection AllOpticalData); - private sealed record AllHygrothermalDataData(HygrothermalDataConnection AllHygrothermalData); - private sealed record AllLifeCycleDataData(LifeCycleDataConnection AllLifeCycleData); - private sealed record AllCalorimetricDataData(CalorimetricDataConnection AllCalorimetricData); - private sealed record AllGeometricDataData(GeometricDataConnection AllGeometricData); - private sealed record AllPhotovoltaicDataData(PhotovoltaicDataConnection AllPhotovoltaicData); - private sealed record HasOpticalDataData(bool HasOpticalData); - private sealed record HasCalorimetricDataData(bool HasCalorimetricData); - private sealed record HasGeometricDataData(bool HasGeometricData); - private sealed record HasHygrothermalDataData(bool HasHygrothermalData); - private sealed record HasLifeCycleDataData(bool HasLifeCycleData); - private sealed record HasPhotovoltaicDataData(bool HasPhotovoltaicData); } \ No newline at end of file diff --git a/backend/src/GraphQl/Databases/DatabaseSortType.cs b/backend/src/GraphQl/Databases/DatabaseSortType.cs index 634d3d2a9..cdf28db4f 100644 --- a/backend/src/GraphQl/Databases/DatabaseSortType.cs +++ b/backend/src/GraphQl/Databases/DatabaseSortType.cs @@ -4,17 +4,16 @@ namespace Metabase.GraphQl.Databases; -public sealed class DatabaseSortType - : EntitySortType +public class DatabaseSortType + : AuditableEntitySortType { protected override void Configure( ISortInputTypeDescriptor descriptor ) { base.Configure(descriptor); - descriptor.Field(x => x.Name); - descriptor.Field(x => x.Description); - descriptor.Field(x => x.Locator); - descriptor.Field(x => x.Operator); + descriptor.Field(_ => _.Name); + descriptor.Field(_ => _.Description); + descriptor.Field(_ => _.Locator); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Databases/DatabaseType.cs b/backend/src/GraphQl/Databases/DatabaseType.cs index efe337d93..4f9256347 100644 --- a/backend/src/GraphQl/Databases/DatabaseType.cs +++ b/backend/src/GraphQl/Databases/DatabaseType.cs @@ -1,15 +1,12 @@ -using System; -using System.Linq.Expressions; using HotChocolate.Types; using Metabase.Data; -using Metabase.GraphQl.DataX; using Metabase.GraphQl.Entities; using Metabase.GraphQl.Users; namespace Metabase.GraphQl.Databases; public sealed class DatabaseType - : EntityType + : EntityType { protected override void Configure( IObjectTypeDescriptor descriptor @@ -27,161 +24,77 @@ IObjectTypeDescriptor descriptor descriptor .Field(t => t.OperatorId) .Ignore(); - ConfigureDataField( - descriptor, - "data", - _ => _.GetDataAsync(default!, default!, default, default, default!, default!, default) - ) - .Argument("kind", _ => _.Type>>()); - ConfigureHasDataField( - descriptor, - "hasData", - _ => _.HasDataAsync(default!, default!, default!, default, default!, default!, default) - ) - .Argument("kind", _ => _.Type>>()); - ConfigureDataField( - descriptor, - "calorimetricData", - _ => _.GetCalorimetricDataAsync(default!, default, default, default!, default!, default) - ); - ConfigureAllDataField( - descriptor, - "allCalorimetricData", - _ => _.GetAllCalorimetricDataAsync(default!, default, default, default, default, default, default, default!, default!, default) - ); - ConfigureHasDataField( - descriptor, - "hasCalorimetricData", - _ => _.HasCalorimetricDataAsync(default!, default, default, default!, default!, default) - ); - ConfigureDataField( - descriptor, - "geometricData", - _ => _.GetGeometricDataAsync(default!, default, default, default!, default!, default) - ); - ConfigureAllDataField( - descriptor, - "allGeometricData", - _ => _.GetAllGeometricDataAsync(default!, default, default, default, default, default, default, default!, default!, default) - ); - ConfigureHasDataField( - descriptor, - "hasGeometricData", - _ => _.HasGeometricDataAsync(default!, default, default, default!, default!, default) - ); - ConfigureDataField( - descriptor, - "hygrothermalData", - _ => _.GetHygrothermalDataAsync(default!, default, default, default!, default!, default) - ); - ConfigureAllDataField( - descriptor, - "allHygrothermalData", - _ => _.GetAllHygrothermalDataAsync(default!, default, default, default, default, default, default, default!, default!, default) - ); - ConfigureHasDataField( - descriptor, - "hasHygrothermalData", - _ => _.HasHygrothermalDataAsync(default!, default, default, default!, default!, default) - ); - ConfigureDataField( - descriptor, - "lifeCycleData", - _ => _.GetLifeCycleDataAsync(default!, default, default, default!, default!, default) - ); - ConfigureAllDataField( - descriptor, - "allLifeCycleData", - _ => _.GetAllLifeCycleDataAsync(default!, default, default, default, default, default, default, default!, default!, default) - ); - ConfigureHasDataField( - descriptor, - "hasLifeCycleData", - _ => _.HasLifeCycleDataAsync(default!, default, default, default!, default!, default) - ); - ConfigureDataField( - descriptor, - "opticalData", - _ => _.GetOpticalDataAsync(default!, default, default, default!, default!, default) - ); - ConfigureAllDataField( - descriptor, - "allOpticalData", - _ => _.GetAllOpticalDataAsync(default!, default, default, default, default, default, default, - default!, default!, default) - ); - ConfigureHasDataField( - descriptor, - "hasOpticalData", - _ => _.HasOpticalDataAsync(default!, default, default, default!, default!, default) - ); - ConfigureDataField( - descriptor, - "photovoltaicData", - _ => _.GetPhotovoltaicDataAsync(default!, default, default, default!, default!, default) - ); - ConfigureAllDataField( - descriptor, - "allPhotovoltaicData", - _ => _.GetAllPhotovoltaicDataAsync(default!, default, default, default, default, default, default, default!, default!, default) - ); - ConfigureHasDataField( - descriptor, - "hasPhotovoltaicData", - _ => _.HasPhotovoltaicDataAsync(default!, default, default, default!, default!, default) - ); + descriptor + .Field("data") + .ResolveWith(_ => _.GetDataAsync(default!, default!, default, default!, default!, default)); + descriptor + .Field("hasData") + .ResolveWith(_ => _.HasDataAsync(default!, default!, default!, default!, default!, default)); + descriptor + .Field("calorimetricData") + .ResolveWith(_ => _.GetCalorimetricDataAsync(default!, default, default!, default!, default)); + descriptor + .Field("allCalorimetricData") + .ResolveWith(_ => _.GetAllCalorimetricDataAsync(default!, default, default, default, default, default, default!, default!, default)); + descriptor + .Field("hasCalorimetricData") + .ResolveWith(_ => _.HasCalorimetricDataAsync(default!, default, default!, default!, default)); + descriptor + .Field("geometricData") + .ResolveWith(_ => _.GetGeometricDataAsync(default!, default, default!, default!, default)); + descriptor + .Field("allGeometricData") + .ResolveWith(_ => _.GetAllGeometricDataAsync(default!, default, default, default, default, default, default!, default!, default)); + descriptor + .Field("hasGeometricData") + .ResolveWith(_ => _.HasGeometricDataAsync(default!, default, default!, default!, default)); + descriptor + .Field("hygrothermalData") + .ResolveWith(_ => _.GetHygrothermalDataAsync(default!, default, default!, default!, default)); + descriptor + .Field("allHygrothermalData") + .ResolveWith(_ => _.GetAllHygrothermalDataAsync(default!, default, default, default, default, default, default!, default!, default)); + descriptor + .Field("hasHygrothermalData") + .ResolveWith(_ => _.HasHygrothermalDataAsync(default!, default, default!, default!, default)); + descriptor + .Field("lifeCycleData") + .ResolveWith(_ => _.GetLifeCycleDataAsync(default!, default, default!, default!, default)); + descriptor + .Field("allLifeCycleData") + .ResolveWith(_ => _.GetAllLifeCycleDataAsync(default!, default, default, default, default, default, default!, default!, default)); + descriptor + .Field("hasLifeCycleData") + .ResolveWith(_ => _.HasLifeCycleDataAsync(default!, default, default!, default!, default)); + descriptor + .Field("opticalData") + .ResolveWith(_ => _.GetOpticalDataAsync(default!, default, default!, default!, default)); + descriptor + .Field("allOpticalData") + .ResolveWith(_ => _.GetAllOpticalDataAsync(default!, default, default, default, default, default, default!, default!, default)); + descriptor + .Field("hasOpticalData") + .ResolveWith(_ => _.HasOpticalDataAsync(default!, default, default!, default!, default)); + descriptor + .Field("photovoltaicData") + .ResolveWith(_ => _.GetPhotovoltaicDataAsync(default!, default, default!, default!, default)); + descriptor + .Field("allPhotovoltaicData") + .ResolveWith(_ => _.GetAllPhotovoltaicDataAsync(default!, default, default, default, default, default, default!, default!, default)); + descriptor + .Field("hasPhotovoltaicData") + .ResolveWith(_ => _.HasPhotovoltaicDataAsync(default!, default, default!, default!, default)); descriptor .Field("isAuthorizedToUpdateNode") + .Cost(1) .ResolveWith(x => x.IsAuthorizedToUpdateNodeAsync(default!, default!, default!, default!)) .UseUserManager(); descriptor .Field("isAuthorizedToVerifyNode") + .Cost(1) .ResolveWith(x => x.IsAuthorizedToVerifyNodeAsync(default!, default!, default!, default!)) .UseUserManager(); } - - private static IObjectFieldDescriptor ConfigureDataField( - IObjectTypeDescriptor descriptor, - string fieldName, - Expression> resolverMethod - ) - { - return descriptor - .Field(fieldName) - .Argument("id", _ => _.Type>()) - .Argument("locale", _ => _.Type()) - .ResolveWith(resolverMethod); - } - - private static void ConfigureAllDataField( - IObjectTypeDescriptor descriptor, - string fieldName, - Expression> resolverMethod - ) - { - descriptor - .Field(fieldName) - .Argument("where", _ => _.Type>()) - .Argument("locale", _ => _.Type()) - .Argument("first", _ => _.Type()) - .Argument("after", _ => _.Type()) - .Argument("last", _ => _.Type()) - .Argument("before", _ => _.Type()) - .ResolveWith(resolverMethod); - } - - private static IObjectFieldDescriptor ConfigureHasDataField( - IObjectTypeDescriptor descriptor, - string fieldName, - Expression> resolverMethod - ) - { - return descriptor - .Field(fieldName) - .Argument("where", _ => _.Type>()) - .Argument("locale", _ => _.Type()) - .ResolveWith(resolverMethod); - } } \ No newline at end of file diff --git a/backend/src/GraphQl/Databases/Queries/database.graphqls b/backend/src/GraphQl/Databases/Queries/database.graphqls new file mode 100644 index 000000000..40c52df8f --- /dev/null +++ b/backend/src/GraphQl/Databases/Queries/database.graphqls @@ -0,0 +1,2837 @@ +""" +Read-only GraphQL schema for databases that store measurement and simulation +data in various formats and refer to the "metabase" for meta information like +data format specifications. + +It follows best practices for +* [Global Object Identification](https://graphql.org/learn/global-object-identification/) + as exemplified in + [GraphQL Server Specification: Object Identification](https://relay.dev/docs/en/graphql-server-specification.html#object-identification) + (see also [GraphQL Global Object Identification Specification](https://relay.dev/graphql/objectidentification.htm)), + and +* [Pagination](https://graphql.org/learn/pagination/), + by adhering to the + [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm), + as exemplified in + [GraphQL Server Specification: Connections](https://relay.dev/docs/en/graphql-server-specification.html#connections). + +Global object identifiers as required by the best practice are exposed by the +property `id` and universally unique identifiers as issued by the "metabase" +are exposed by the property `uuid`. In essence, `id` is the Base64-encoded +concatenation of `uuid` and the requested locale. + +For interoperability with the "metabase", implementations serve HTTP +requests as elaborated on +[Serving over HTTP](https://graphql.org/learn/serving-over-http/). + +The +[Internet Engineering Task Force (IETF)](https://www.ietf.org) +is an Internet standards organization. It publishes standards in +[Request for Comments (RFC)](https://ietf.org/standards/rfcs/) +documents. They are +[searchable](https://www.rfc-editor.org/search/rfc_search.php) +and +[indexed](https://www.rfc-editor.org/rfc-index.html). +Examples are +[Hypertext Transfer Protocol (HTTP)](https://tools.ietf.org/html/rfc7231) +and +[Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc8446). + +The +[Internet Assigned Numbers Authority (IANA)](https://www.iana.org) +allocates and maintains unique codes and numbering systems that are used in +technical standards that drive the Internet like +[domain names](https://www.iana.org/domains), +[Internet Protocol addresses](https://www.iana.org/numbers), and +[registries](https://www.iana.org/protocols). +Examples of the last are +[HTTP status code registry](https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml) +and +[media type registry](https://www.iana.org/assignments/media-types/media-types.xhtml). + +The +[International Organization for Standardization (ISO)](https://www.iso.org) +develops and publishes +[International Standards](https://www.iso.org/standards-catalogue/browse-by-ics.html). +Examples are +[ISO 8601 Date And Time Format](https://www.iso.org/iso-8601-date-and-time-format.html) +and +[ISO 3166 Country Codes](https://www.iso.org/iso-3166-country-codes.html). + +The library +[graphql-scalars](https://github.com/Urigo/graphql-scalars) +introduces custom scalar types for creating precise type-safe GraphQL schemas. +We took some inspiration from +[GitHub GraphQL API](https://docs.github.com/en/graphql). +""" +schema { + query: Query +} + +""" +[RFC 4122](https://tools.ietf.org/html/rfc4122) +compliant +[non-nil](https://tools.ietf.org/html/rfc4122#section-4.1.7) +[Universally Unique Identifier (UUID)](https://tools.ietf.org/html/rfc4122#section-4.1) +string represented as 32 hexadecimal digits in five groups separated by hyphens +in the form `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`, like +`"936da01f-9abd-4d9d-80c7-02af85c822a8"`. Such identifiers are not equal to +`"00000000-0000-0000-0000-000000000000"` and match the regular expression +`^[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}$`. +""" +scalar Uuid + +""" +[ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) +encoded date and time string with offset from Coordinated Universal Time (UTC), +where the fraction of a second permits at most 6 digits. For example, in UTC it +may be in the form `yyyy-MM-ddTHH:mm:ss.ffffffZ`, like +`"2009-06-15T13:45:30.381739Z"`, and in local time in the form +`yyyy-MM-ddTHH:mm:ss.ffffffzzz`, like `"2009-06-15T13:45:30.381739-07:00"`. + +Note that +[RFC 3339](https://tools.ietf.org/html/rfc3339) +is a profile of the ISO 8601 standard for representation of dates and times +using the Gregorian calendar. +""" +scalar DateTime + +""" +[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). +""" +scalar Url + +""" +arXiv identifier, that is, a string of the format +[Understanding the arXiv identifier](https://info.arxiv.org/help/arxiv_identifier.html). +""" +scalar ArXiv + +""" +Digital Object Identifier (DOI) name, that is, a string compliant with [ISO +26324:2025](https://www.iso.org/standard/88862.html). + +See also +[What is a DOI?](https://www.doi.org/the-identifier/what-is-a-doi/). In short, +a DOI name is a digital identifier of an object, any object — physical, +digital, or abstract. DOIs solve a common problem: keeping track of things. +Things can be matter, material, content, or activities. Designed to be used by +humans as well as machines, DOIs identify objects persistently. They allow +things to be uniquely identified and accessed reliably. You know what you have, +where it is, and others can track it too. +""" +scalar Doi + +""" +[BCP 47](https://tools.ietf.org/html/bcp47) +compliant +[Language Tag](https://tools.ietf.org/html/bcp47#section-2) +string like `"de-AT"`, `"sr-Latn-RS"`, `"en-US"`, or `"en-GB"`, where the language +part is essentially an +[ISO 639 Language Code](https://www.iso.org/iso-639-language-codes.html), +the script an +[ISO 15924:2004 Script Code](https://www.iso.org/standard/29546.html), +and the region an +[ISO 3166-1 Country Code](https://www.iso.org/iso-3166-country-codes.html). + +Note that the +[Internet Assigned Numbers Authority (IANA)](https://www.iana.org) +maintains the +[Language Subtag Registry](https://www.iana.org/assignments/lang-subtags-templates/lang-subtags-templates.xhtml). +""" +scalar Locale + +""" +[September 2025 GraphQL](https://spec.graphql.org/September2025/) +compliant +[query](https://spec.graphql.org/September2025/#sec-Language.Operations) +string without +[variables](http://spec.graphql.org/September2025/#sec-Language.Variables) +and without unnecessary white-space for the present schema. +""" +scalar GraphQlQuery + +""" +Anything +""" +scalar Any + +""" +Fetch data, ask whether there is data, and search for data. Each field except +`node` accepts +- an optional locale to specify essentially the language of prose in the result. +Each field's value is uniquely determined by the required arguments and locale. + +If locale is not given, the default locale is used, which may differ from +database to database. +""" +type Query { + """ + Fetch node by global object identifier as elaborated on + [Global Object Identification](https://graphql.org/learn/global-object-identification/) + where two nodes with identical identifiers are equal as elaborated in + [Field stability](https://graphql.org/learn/global-object-identification/#field-stability). + + See also + [Node root field](https://graphql.org/learn/global-object-identification/#node-root-field) + """ + node("Global object identifier." id: ID!): Node + + """ + Fetch calorimetric data by universally unique identifier, and optional + locale, where all three arguments together uniquely determine the data. + """ + calorimetricData( + "Data identifier." + id: Uuid! + "Locale. If not given, the default locale is used, which may differ from database to database." + locale: Locale + ): CalorimetricData + + """ + Confer `Query#calorimetricData`. + """ + geometricData(id: Uuid!, locale: Locale): GeometricData + + """ + Confer `Query#calorimetricData`. + """ + hygrothermalData(id: Uuid!, locale: Locale): HygrothermalData + + """ + Confer `Query#calorimetricData`. + """ + lifeCycleData(id: Uuid!, locale: Locale): LifeCycleData + + """ + Confer `Query#calorimetricData`. + """ + opticalData(id: Uuid!, locale: Locale): OpticalData + + """ + Confer `Query#calorimetricData`. + """ + photovoltaicData(id: Uuid!, locale: Locale): PhotovoltaicData + + """ + Search for all calorimetric data for which the proposition is true. + + The result is a + [connection](https://relay.dev/graphql/connections.htm#sec-Connection-Types) + with + [edges](https://relay.dev/graphql/connections.htm#sec-Edge-Types) + and + [nodes](https://relay.dev/graphql/connections.htm#sec-Node) + as specified in the + [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm), + where the nodes are the actual data, and connection and edges are used for + meta information and + [pagination](https://graphql.org/learn/pagination/) + together with the + [forward pagination arguments](https://relay.dev/graphql/connections.htm#sec-Forward-pagination-arguments) + `first` with the default and maximum 100 and `after` and the + [backward pagination arguments](https://relay.dev/graphql/connections.htm#sec-Backward-pagination-arguments) + `last` with the default and maximum 100 and `before`. + + Example: + ``` + query { + allCalorimetricData( + where: { + or: [ + { + componentId: { equalTo: "810e84b4-9ebf-416c-88ea-aade848f1fdf" }, + gValue: { greaterThanOrEqualTo: 0.5 }, + not: { + uValue: { lessThanOrEqualTo: 0.5 } + } + }, + { + nearnormalHemisphericalVisibleTransmittance: { + inClosedInterval: { + lowerBound: 0.2, + upperBound: 0.8 + } + } + } + ] + } + ) { + edges { + node { + uuid + } + } + } + } + ``` + """ + allCalorimetricData( + "Compound boolean expression that is true for all data being returned. If not given, a proposition that is always true is used." + where: CalorimetricDataPropositionInput + "Locale. If not given, the default locale is used, which may differ from database to database." + locale: Locale + "Maximum number of data edges after the cursor `after`. If not given, the default and maximum value 100 is used." + first: Int + "Data cursor after which to return at most `first` data edges." + after: String + "Maximum number of data edges before the cursor `before`." + last: Int + "Data cursor before which to return at most `last` data edges." + before: String + ): CalorimetricDataConnection + + """ + Confer `Query#allCalorimetricData`. + """ + allGeometricData( + where: GeometricDataPropositionInput + locale: Locale + first: Int + after: String + last: Int + before: String + ): GeometricDataConnection + + """ + Confer `Query#allCalorimetricData`. + """ + allHygrothermalData( + where: HygrothermalDataPropositionInput + locale: Locale + first: Int + after: String + last: Int + before: String + ): HygrothermalDataConnection + + """ + Confer `Query#allCalorimetricData`. + """ + allLifeCycleData( + where: LifeCycleDataPropositionInput + locale: Locale + first: Int + after: String + last: Int + before: String + ): LifeCycleDataConnection + + """ + Confer `Query#allCalorimetricData`. + """ + allOpticalData( + where: OpticalDataPropositionInput + locale: Locale + first: Int + after: String + last: Int + before: String + ): OpticalDataConnection + + """ + Confer `Query#allCalorimetricData`. + """ + allPhotovoltaicData( + where: PhotovoltaicDataPropositionInput + locale: Locale + first: Int + after: String + last: Int + before: String + ): PhotovoltaicDataConnection + + """ + Whether there is data for which the proposition is true in the given locale. + """ + hasCalorimetricData( + "Compound boolean expression that is true for data being searched for." + where: CalorimetricDataPropositionInput + "Locale. If not given, the default locale is used, which may differ from database to database." + locale: Locale + ): Boolean! + + """ + Confer `Query#hasCalorimetricData`. + """ + hasGeometricData( + where: GeometricDataPropositionInput + locale: Locale + ): Boolean! + + """ + Confer `Query#hasCalorimetricData`. + """ + hasHygrothermalData( + where: HygrothermalDataPropositionInput + locale: Locale + ): Boolean! + + """ + Confer `Query#hasCalorimetricData`. + """ + hasLifeCycleData( + where: LifeCycleDataPropositionInput + locale: Locale + ): Boolean! + + """ + Confer `Query#hasCalorimetricData`. + """ + hasOpticalData(where: OpticalDataPropositionInput, locale: Locale): Boolean! + + """ + Confer `Query#hasCalorimetricData`. + """ + hasPhotovoltaicData( + where: PhotovoltaicDataPropositionInput + locale: Locale + ): Boolean! + + """ + The verification code of the database that is generated by the metabase when + an institution registers a new database. It is used by the metabase to verify + the the institution is indeed under control of the database's GraphQL + endpoint. + + To register and verify your database to the metabase do the following: + + 1. On the metabase, sign-in as a representative of your institution. + 2. Register a new database and remember the generated verification code. + 3. Make the database's GraphQL endpoint return that code when queried for its + verification code. + 4. Ask the metabase to verify the database. + """ + verificationCode: String +} + +""" +[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm) +and +[Global Object Identification](https://graphql.org/learn/global-object-identification/) +compliant +[Node](https://relay.dev/graphql/connections.htm#sec-Node) +interface. + +See also +[Node](https://graphql.org/learn/global-object-identification/#node-interface). + +Note that according to +[Field stability](https://graphql.org/learn/global-object-identification/#field-stability), +if two objects appear in a query, both implementing `Node` with identical +identifiers, then the two objects are equal. For example, for queried data, the +identifier could be a Base64-encoded concatenation of the data identifier, +which is a `Uuid`, and the requested locale. +""" +interface Node { + """ + Base64-encoded identifier. + """ + id: ID! +} + +""" +[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm) +compliant +[PageInfo](https://relay.dev/graphql/connections.htm#sec-undefined.PageInfo) +type. + +Note that although the introspection query results on +[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm) +use the scalar type `String` for cursors, the specification of +[cursors](https://relay.dev/graphql/connections.htm#sec-Cursor) +says that a cursor is of +> a type that serializes as a `String`; this may be a `String`, a non‐null +> wrapper around a `String`, a custom scalar that serializes as a `String`, or +> a non‐null wrapper around a custom scalar that serializes as a `String`. +""" +type PageInfo { + """ + Whether more edges exist following the set defined by the client's arguments. + For details see + [PageInfo: Fields](https://relay.dev/graphql/connections.htm#sec-undefined.PageInfo.Fields). + """ + hasNextPage: Boolean! + + """ + Whether more edges exist prior to the set defined by the clients arguments. + For details see + [PageInfo: Fields](https://relay.dev/graphql/connections.htm#sec-undefined.PageInfo.Fields). + """ + hasPreviousPage: Boolean! + + """ + Cursor corresponding to the first edge in the set defined by the client's + arguments. For details see + [PageInfo: Fields](https://relay.dev/graphql/connections.htm#sec-undefined.PageInfo.Fields). + """ + startCursor: String + + """ + Cursor corresponding to the last edge in the set defined by the client's + arguments. For details see + [PageInfo: Fields](https://relay.dev/graphql/connections.htm#sec-undefined.PageInfo.Fields). + """ + endCursor: String +} + +""" +Closed interval from lower to upper bound. Each `Float` `x` is in the interval if +and only if `x` is greater than or equal to the lower bound and less than or +equal to the upper bound. In particular, if the lower bound is equal to the +upper bound, then the interval consists of exactly one float; and, if the lower +bound is greater than the upper bound, then the interval is empty. +""" +input ClosedIntervalInput { + """ + Lower bound. + """ + lowerBound: Float! + + """ + Upper bound. + """ + upperBound: Float! +} + +""" +Proposition for values of type `Float`. Multiple sub-propositions are combined +conjunctively, where the conjunction of an empty set of sub-propositions is true. +""" +input FloatPropositionInput { + """ + True for values that are equal to `equalTo`, otherwise false. + """ + equalTo: Float + + """ + True for values that are greater than or equal to `greaterThanOrEqualTo`, + otherwise false. + """ + greaterThanOrEqualTo: Float + + """ + True for values that are in the closed interval `inClosedInterval`, otherwise + false. + """ + inClosedInterval: ClosedIntervalInput + + """ + True for values that are less than or equal to `lessThanOrEqualTo`, otherwise + false. + """ + lessThanOrEqualTo: Float +} + +""" +Proposition for lists of values of type `Float`. Multiple sub-propositions are +combined conjunctively, where the conjunction of an empty set of +sub-propositions is true. +""" +input FloatsPropositionInput { + """ + True if each list value satisfies the proposition `all`, otherwise false. + """ + all: FloatPropositionInput + + """ + True if no list value satisfies the proposition `none`, otherwise false. + """ + none: FloatPropositionInput + + """ + True if at least one list value satisfies the proposition `some`, otherwise + false. + """ + some: FloatPropositionInput +} + +""" +Proposition for values of type `Uuid`. Multiple sub-propositions are combined +conjunctively, where the conjunction of an empty set of sub-propositions is true. +""" +input UuidPropositionInput { + """ + True for values that are equal to `equalTo`, otherwise false. + """ + equalTo: Uuid +} + +""" +Proposition for values of type `GetHttpsResource`. Multiple sub-propositions +are combined conjunctively, where the conjunction of an empty set of +sub-propositions is true. +""" +input GetHttpsResourcePropositionInput { + """ + True for GET HTTPS resources whose data format identifier satisfies the + proposition `dataFormatId`, otherwise false. + """ + dataFormatId: UuidPropositionInput + + """ + True for GET HTTPS resources whose archived files meta information satisfies + the proposition `archivedFilesMetaInformation`, otherwise false. + """ + archivedFilesMetaInformation: FilesMetaInformationPropositionInput +} + +""" +Proposition for lists of values of type `GetHttpsResource`. Multiple +sub-propositions are combined conjunctively, where the conjunction of an empty +set of sub-propositions is true. +""" +input GetHttpsResourcesPropositionInput { + """ + True if each list value satisfies the proposition `all`, otherwise false. + """ + all: GetHttpsResourcePropositionInput + + """ + True if no list value satisfies the proposition `none`, otherwise false. + """ + none: GetHttpsResourcePropositionInput + + """ + True if at least one list value satisfies the proposition `some`, otherwise + false. + """ + some: GetHttpsResourcePropositionInput +} + +""" +Proposition for values of type `FileMetaInformation`. Multiple sub-propositions +are combined conjunctively, where the conjunction of an empty set of +sub-propositions is true. +""" +input FileMetaInformationPropositionInput { + """ + True for file meta information whose data format identifier satisfies the + proposition `dataFormatId`, otherwise false. + """ + dataFormatId: UuidPropositionInput +} + +""" +Proposition for lists of values of type `FileMetaInformation`. Multiple +sub-propositions are combined conjunctively, where the conjunction of an empty +set of sub-propositions is true. +""" +input FilesMetaInformationPropositionInput { + """ + True if each list value satisfies the proposition `all`, otherwise false. + """ + all: FileMetaInformationPropositionInput + + """ + True if no list value satisfies the proposition `none`, otherwise false. + """ + none: FileMetaInformationPropositionInput + + """ + True if at least one list value satisfies the proposition `some`, otherwise + false. + """ + some: FileMetaInformationPropositionInput +} + +""" +Proposition for values of type `CielabColor`. Multiple sub-propositions are +combined conjunctively, where the conjunction of an empty set of +sub-propositions is true. +""" +input CielabColorPropositionInput { + """ + True for CIELAB Colors with a coordinate L* that satisfies the proposition + `lStar`, otherwise false. + """ + lStar: FloatPropositionInput + + """ + True CIELAB Colors with a coordinate a* that satisfies the proposition + `aStar`, otherwise false. + """ + aStar: FloatPropositionInput + + """ + True CIELAB Colors with a coordinate b* that satisfies the proposition + `bStar`, otherwise false. + """ + bStar: FloatPropositionInput +} + +""" +Proposition for lists of values of type `CielabColor`. Multiple +sub-propositions are combined conjunctively, where the conjunction of an empty +set of sub-propositions is true. +""" +input CielabColorsPropositionInput { + """ + True if each list value satisfies the proposition `all`, otherwise false. + """ + all: CielabColorPropositionInput + + """ + True if no list value satisfies the proposition `none`, otherwise false. + """ + none: CielabColorPropositionInput + + """ + True if at least one list value satisfies the proposition `some`, otherwise + false. + """ + some: CielabColorPropositionInput +} + +""" +Proposition for values of type `CalorimetricData`. Multiple sub-propositions +are combined conjunctively, where the conjunction of an empty set of +sub-propositions is true. +""" +input CalorimetricDataPropositionInput { + """ + True for data for the component with identifier `componentId`, otherwise + false. + """ + componentId: UuidPropositionInput + + """ + True for data for which each proposition in `and` is true, otherwise false. + In particular, it is true if there is no proposition in `and`. + """ + and: [CalorimetricDataPropositionInput!] + + """ + True for data for which the proposition `not` is false, otherwise false. + """ + not: CalorimetricDataPropositionInput + + """ + True for data for which at least one proposition in `or` is true, otherwise + false. In particular, it is false if there is no proposition in `or`. + """ + or: [CalorimetricDataPropositionInput!] + + """ + True for data whose list of resources satisfies the proposition `resources`, + otherwise false. + """ + resources: GetHttpsResourcesPropositionInput + + """ + True for data whose list of `g` values satisfies the proposition `gValues`, + otherwise false. + """ + gValues: FloatsPropositionInput + + """ + True for data whose list of `u` values satisfies the proposition `uValues`, + otherwise false. + """ + uValues: FloatsPropositionInput +} + +""" +Proposition for values of type `GeometricData`. Multiple sub-propositions are +combined conjunctively, where the conjunction of an empty set of +sub-propositions is true. +""" +input GeometricDataPropositionInput { + """ + True for data for the component with identifier `componentId`, otherwise + false. + """ + componentId: UuidPropositionInput + + """ + True for data for which each proposition in `and` is true, otherwise false. + In particular, it is true if there is no proposition in `and`. + """ + and: [GeometricDataPropositionInput!] + + """ + True for data for which the proposition `not` is false, otherwise false. + """ + not: GeometricDataPropositionInput + + """ + True for data for which at least one proposition in `or` is true, otherwise + false. In particular, it is false if there is no proposition in `or`. + """ + or: [GeometricDataPropositionInput!] + + """ + True for data whose list of resources satisfies the proposition `resources`, + otherwise false. + """ + resources: GetHttpsResourcesPropositionInput + + """ + True for data whose list of `thickness` values satisfies the proposition `thicknesses`, + otherwise false. + """ + thicknesses: FloatsPropositionInput +} + +""" +Proposition for values of type `HygrothermalData`. Multiple sub-propositions +are combined conjunctively, where the conjunction of an empty set of +sub-propositions is true. +""" +input HygrothermalDataPropositionInput { + """ + True for data for the component with identifier `componentId`, otherwise + false. + """ + componentId: UuidPropositionInput + + """ + True for data for which each proposition in `and` is true, otherwise false. + In particular, it is true if there is no proposition in `and`. + """ + and: [HygrothermalDataPropositionInput!] + + """ + True for data for which the proposition `not` is false, otherwise false. + """ + not: HygrothermalDataPropositionInput + + """ + True for data for which at least one proposition in `or` is true, otherwise + false. In particular, it is false if there is no proposition in `or`. + """ + or: [HygrothermalDataPropositionInput!] + + """ + True for data whose list of resources satisfies the proposition `resources`, + otherwise false. + """ + resources: GetHttpsResourcesPropositionInput +} + +""" +Proposition for values of type `LifeCycleData`. Multiple sub-propositions are +combined conjunctively, where the conjunction of an empty set of +sub-propositions is true. +""" +input LifeCycleDataPropositionInput { + """ + True for data for the component with identifier `componentId`, otherwise + false. + """ + componentId: UuidPropositionInput + + """ + True for data for which each proposition in `and` is true, otherwise false. + In particular, it is true if there is no proposition in `and`. + """ + and: [LifeCycleDataPropositionInput!] + + """ + True for data for which the proposition `not` is false, otherwise false. + """ + not: LifeCycleDataPropositionInput + + """ + True for data for which at least one proposition in `or` is true, otherwise + false. In particular, it is false if there is no proposition in `or`. + """ + or: [LifeCycleDataPropositionInput!] + + """ + True for data whose list of resources satisfies the proposition `resources`, + otherwise false. + """ + resources: GetHttpsResourcesPropositionInput +} + +""" +Proposition for values of enum type `OpticalComponentType`. Multiple +sub-propositions are combined conjunctively, where the conjunction of an empty +sub-propositions is true. +""" +input OpticalComponentTypePropositionInput { + equalTo: OpticalComponentType + notEqualTo: OpticalComponentType + in: [OpticalComponentType] + notIn: [OpticalComponentType] +} + +""" +Proposition for values of enum type `OpticalComponentSubtype`. Multiple +sub-propositions are combined conjunctively, where the conjunction of an empty +set of sub-propositions is true. +""" +input OpticalComponentSubtypePropositionInput { + equalTo: OpticalComponentSubtype + notEqualTo: OpticalComponentSubtype + in: [OpticalComponentSubtype] + notIn: [OpticalComponentSubtype] +} + +""" +Proposition for values of enum type `CoatedSide`. Multiple sub-propositions are +combined conjunctively, where the conjunction of an empty set of +sub-propositions is true. +""" +input CoatedSidePropositionInput { + equalTo: CoatedSide + notEqualTo: CoatedSide + in: [CoatedSide!] + notIn: [CoatedSide!] +} + +""" +Proposition for values of type `OpticalData`. Multiple sub-propositions are +combined conjunctively, where the conjunction of an empty set of +sub-propositions is true. +""" +input OpticalDataPropositionInput { + """ + True for data for the component with identifier `componentId`, otherwise + false. + """ + componentId: UuidPropositionInput + + """ + True for data for which each proposition in `and` is true, otherwise false. + In particular, it is true if there is no proposition in `and`. + """ + and: [OpticalDataPropositionInput!] + + """ + True for data for which the proposition `not` is false, otherwise false. + """ + not: OpticalDataPropositionInput + + """ + True for data for which at least one proposition in `or` is true, otherwise + false. In particular, it is false if there is no proposition in `or`. + """ + or: [OpticalDataPropositionInput!] + + """ + True for data whose list of resources satisfies the proposition `resources`, + otherwise false. + """ + resources: GetHttpsResourcesPropositionInput + + """ + True for data whose type satisfies the proposition `type`, otherwise false. + """ + type: OpticalComponentTypePropositionInput + + """ + True for data whose subtype satisfies the proposition `subtype`, otherwise + false. + """ + subtype: OpticalComponentSubtypePropositionInput + + """ + True for data whose coated side satisfies the proposition `coatedSide`, + otherwise false. + """ + coatedSide: CoatedSidePropositionInput + + """ + True for data whose list of nearnormal hemispherical visible transmittance + values satisfies the proposition + `nearnormalHemisphericalVisibleTransmittances`, otherwise false. + """ + nearnormalHemisphericalVisibleTransmittances: FloatsPropositionInput + + """ + True for data whose list of nearnormal hemispherical visible reflectance + values satisfies the proposition + `nearnormalHemisphericalVisibleReflectances`, otherwise false. + """ + nearnormalHemisphericalVisibleReflectances: FloatsPropositionInput + + """ + True for data whose list of nearnormal hemispherical solar transmittance + values satisfies the proposition + `nearnormalHemisphericalSolarTransmittances`, otherwise false. + """ + nearnormalHemisphericalSolarTransmittances: FloatsPropositionInput + + """ + True for data whose list of nearnormal hemispherical solar reflectance + values satisfies the proposition + `nearnormalHemisphericalSolarReflectances`, otherwise false. + """ + nearnormalHemisphericalSolarReflectances: FloatsPropositionInput + + """ + True for data whose list of infrared emittance values satisfies the + proposition `infraredEmittances`, otherwise false. + """ + infraredEmittances: FloatsPropositionInput + + """ + True for data whose list of Color Rendering Indices satisfies the proposition + `colorRenderingIndices`, otherwise false. + """ + colorRenderingIndices: FloatsPropositionInput + + """ + True for data whose list of CIELAB Color Space coordinates satisfies the + proposition `cielabColors`, otherwise false. + """ + cielabColors: CielabColorsPropositionInput +} + +""" +Proposition for values of type `PhotovoltaicData`. Multiple sub-propositions +are combined conjunctively, where the conjunction of an empty set of +sub-propositions is true. +""" +input PhotovoltaicDataPropositionInput { + """ + True for data for the component with identifier `componentId`, otherwise + false. + """ + componentId: UuidPropositionInput + + """ + True for data for which each proposition in `and` is true, otherwise false. + In particular, it is true if there is no proposition in `and`. + """ + and: [PhotovoltaicDataPropositionInput!] + + """ + True for data for which the proposition `not` is false, otherwise false. + """ + not: PhotovoltaicDataPropositionInput + + """ + True for data for which at least one proposition in `or` is true, otherwise + false. In particular, it is false if there is no proposition in `or`. + """ + or: [PhotovoltaicDataPropositionInput!] + + """ + True for data whose list of resources satisfies the proposition `resources`, + otherwise false. + """ + resources: GetHttpsResourcesPropositionInput +} + +""" +Materialized connection from somewhere to calorimetric data to paginate data +using opaque cursors as elaborated in the best practice +[Pagination](https://graphql.org/learn/pagination/) +and the specification +[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm), +see in particular +[Complete Connection Model](https://graphql.org/learn/pagination/#complete-connection-model) +and +[Connection Types](https://relay.dev/graphql/connections.htm#sec-Connection-Types). +""" +type CalorimetricDataConnection { + """ + Edges to data nodes that expose opaque cursors for pagination. + """ + edges: [CalorimetricDataEdge!] + + """ + Information about the present slice of all data edges (and nodes). + """ + pageInfo: PageInfo! + + """ + The total count of items in the connection. + """ + totalCount: Int! +} + +""" +Materialized edge from somewhere to calorimetric data to paginate data using +opaque cursors as elaborated in the best practice +[Pagination](https://graphql.org/learn/pagination/) +and the specification +[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm), +see in particular +[Complete Connection Model](https://graphql.org/learn/pagination/#complete-connection-model) +and +[Edge Types](https://relay.dev/graphql/connections.htm#sec-Edge-Types). +""" +type CalorimetricDataEdge { + """ + Opaque + [cursor](https://relay.dev/graphql/connections.htm#sec-Cursor) + that designates the present edge for pagination. + """ + cursor: String! + + """ + Data node the edge points to. + """ + node: CalorimetricData! +} + +""" +Materialized connection from somewhere to geometric data to paginate data using +opaque cursors as elaborated in the best practice +[Pagination](https://graphql.org/learn/pagination/) +and the specification +[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm), +see in particular +[Complete Connection Model](https://graphql.org/learn/pagination/#complete-connection-model) +and +[Connection Types](https://relay.dev/graphql/connections.htm#sec-Connection-Types). +""" +type GeometricDataConnection { + """ + Edges to data nodes that expose opaque cursors for pagination. + """ + edges: [GeometricDataEdge!] + + """ + Information about the present slice of all data edges (and nodes). + """ + pageInfo: PageInfo! + + """ + The total count of items in the connection. + """ + totalCount: Int! +} + +""" +Materialized edge from somewhere to geometric data to paginate data using +opaque cursors as elaborated in the best practice +[Pagination](https://graphql.org/learn/pagination/) +and the specification +[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm), +see in particular +[Complete Connection Model](https://graphql.org/learn/pagination/#complete-connection-model) +and +[Edge Types](https://relay.dev/graphql/connections.htm#sec-Edge-Types). +""" +type GeometricDataEdge { + """ + Opaque + [cursor](https://relay.dev/graphql/connections.htm#sec-Cursor) + that designates the present edge for pagination. + """ + cursor: String! + + """ + Data node the edge points to. + """ + node: GeometricData! +} + +""" +Materialized connection from somewhere to hygrothermal data to paginate data +using opaque cursors as elaborated in the best practice +[Pagination](https://graphql.org/learn/pagination/) +and the specification +[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm), +see in particular +[Complete Connection Model](https://graphql.org/learn/pagination/#complete-connection-model) +and +[Connection Types](https://relay.dev/graphql/connections.htm#sec-Connection-Types). +""" +type HygrothermalDataConnection { + """ + Edges to data nodes that expose opaque cursors for pagination. + """ + edges: [HygrothermalDataEdge!] + + """ + Information about the present slice of all data edges (and nodes). + """ + pageInfo: PageInfo! + + """ + The total count of items in the connection. + """ + totalCount: Int! +} + +""" +Materialized edge from somewhere to hygrothermal data to paginate data using +opaque cursors as elaborated in the best practice +[Pagination](https://graphql.org/learn/pagination/) +and the specification +[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm), +see in particular +[Complete Connection Model](https://graphql.org/learn/pagination/#complete-connection-model) +and +[Edge Types](https://relay.dev/graphql/connections.htm#sec-Edge-Types). +""" +type HygrothermalDataEdge { + """ + Opaque + [cursor](https://relay.dev/graphql/connections.htm#sec-Cursor) + that designates the present edge for pagination. + """ + cursor: String! + + """ + Data node the edge points to. + """ + node: HygrothermalData! +} + +""" +Materialized connection from somewhere to life-cycle data to paginate data +using opaque cursors as elaborated in the best practice +[Pagination](https://graphql.org/learn/pagination/) +and the specification +[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm), +see in particular +[Complete Connection Model](https://graphql.org/learn/pagination/#complete-connection-model) +and +[Connection Types](https://relay.dev/graphql/connections.htm#sec-Connection-Types). +""" +type LifeCycleDataConnection { + """ + Edges to data nodes that expose opaque cursors for pagination. + """ + edges: [LifeCycleDataEdge!] + + """ + Information about the present slice of all data edges (and nodes). + """ + pageInfo: PageInfo! + + """ + The total count of items in the connection. + """ + totalCount: Int! +} + +""" +Materialized edge from somewhere to life-cycle data to paginate data using opaque +cursors as elaborated in the best practice +[Pagination](https://graphql.org/learn/pagination/) +and the specification +[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm), +see in particular +[Complete Connection Model](https://graphql.org/learn/pagination/#complete-connection-model) +and +[Edge Types](https://relay.dev/graphql/connections.htm#sec-Edge-Types). +""" +type LifeCycleDataEdge { + """ + Opaque + [cursor](https://relay.dev/graphql/connections.htm#sec-Cursor) + that designates the present edge for pagination. + """ + cursor: String! + + """ + Data node the edge points to. + """ + node: LifeCycleData! +} + +""" +Materialized connection from somewhere to optical data to paginate data using +opaque cursors as elaborated in the best practice +[Pagination](https://graphql.org/learn/pagination/) +and the specification +[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm), +see in particular +[Complete Connection Model](https://graphql.org/learn/pagination/#complete-connection-model) +and +[Connection Types](https://relay.dev/graphql/connections.htm#sec-Connection-Types). +""" +type OpticalDataConnection { + """ + Edges to data nodes that expose opaque cursors for pagination. + """ + edges: [OpticalDataEdge!] + + """ + Information about the present slice of all data edges (and nodes). + """ + pageInfo: PageInfo! + + """ + The total count of items in the connection. + """ + totalCount: Int! +} + +""" +Materialized edge from somewhere to optical data to paginate data using opaque +cursors as elaborated in the best practice +[Pagination](https://graphql.org/learn/pagination/) +and the specification +[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm), +see in particular +[Complete Connection Model](https://graphql.org/learn/pagination/#complete-connection-model) +and +[Edge Types](https://relay.dev/graphql/connections.htm#sec-Edge-Types). +""" +type OpticalDataEdge { + """ + Opaque + [cursor](https://relay.dev/graphql/connections.htm#sec-Cursor) + that designates the present edge for pagination. + """ + cursor: String! + + """ + Data node the edge points to. + """ + node: OpticalData! +} + +""" +Materialized connection from somewhere to photovoltaic data to paginate data +using opaque cursors as elaborated in the best practice +[Pagination](https://graphql.org/learn/pagination/) +and the specification +[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm), +see in particular +[Complete Connection Model](https://graphql.org/learn/pagination/#complete-connection-model) +and +[Connection Types](https://relay.dev/graphql/connections.htm#sec-Connection-Types). +""" +type PhotovoltaicDataConnection { + """ + Edges to data nodes that expose opaque cursors for pagination. + """ + edges: [PhotovoltaicDataEdge!] + + """ + Information about the present slice of all data edges (and nodes). + """ + pageInfo: PageInfo! + + """ + The total count of items in the connection. + """ + totalCount: Int! +} + +""" +Materialized edge from somewhere to photovoltaic data to paginate data using +opaque cursors as elaborated in the best practice +[Pagination](https://graphql.org/learn/pagination/) +and the specification +[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm), +see in particular +[Complete Connection Model](https://graphql.org/learn/pagination/#complete-connection-model) +and +[Edge Types](https://relay.dev/graphql/connections.htm#sec-Edge-Types). +""" +type PhotovoltaicDataEdge { + """ + Opaque + [cursor](https://relay.dev/graphql/connections.htm#sec-Cursor) + that designates the present edge for pagination. + """ + cursor: String! + + """ + Data node the edge points to. + """ + node: PhotovoltaicData! +} + +""" +Type of data. +""" +enum OpticalComponentType { + GLAZING + SHADING +} + +""" +Subtype of data. +""" +enum OpticalComponentSubtype { + 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 + SOLAR_SCREEN + VENETIAN_BLIND + VERTICAL_LOUVER + WOVEN_SHADE +} + +""" +Coated side +""" +enum CoatedSide { + PRIME + NON_PRIME + BOTH + NEITHER + UNKNOWN + NOT_APPLICABLE +} + +""" +Measured, simulated, or calculated data. + +The data identifier identifies the data modulo patches and locale and together +with locale only modulo patches. + +For non-patch versions a new data identifier is needed. + +Note that the term 'patch' is used in the sense of +[Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html) +""" +interface Data { + """ + Identifier, which uniquely identifies data modulo patches and locale. + """ + uuid: Uuid! + + """ + Identifier of this database that manages the data. The database's meta data + can be obtained by sending the GraphQL query + `query { database(id: ...) { ... }}` to the metabas e. + """ + databaseId: Uuid! + + """ + Timestamp that designates the patch version. For example, correction of some + grammatical error in the description results in a new patch version + designated by a new timestamp, or adding an approval by some institution. + """ + timestamp: DateTime! + + """ + Component identifier issued by the metabase. The components's meta data can + be obtained by sending the GraphQL query + `query { component(id: ...) { ... } }` + to the metabase. + """ + componentId: Uuid! + + """ + Name + """ + name: String + + """ + Description for humans + """ + description: String + + """ + Critical information to avoid mistakes due to data misinterpretation. + """ + warnings: [String!]! + + """ + Creator identifier issued by the metabase. The creator's meta data can be + obtained by sending the GraphQL query + `query { institution(id: ...) { ... } }` + to the metabase. + """ + creatorId: Uuid! + + """ + Creation timestamp, designating when the present data set was created, for + example, after completion of a measurement, simulation, or calculation. + """ + createdAt: DateTime! + + """ + Method that was applied to create this data set. + """ + appliedMethod: AppliedMethod! + + """ + Approvals by institutions in the form of GnuPG signatures. See the type + `Approval` for details on how to verify data and approvals. + """ + approvals: [DataApproval!]! + + """ + Multiple HTTP resources with meta information of the actual data requestable + through the protocol HTTP over TLS, in short, HTTPS, with `GET` requests. The + first resource's data is the result of applying the method `appliedMethod`. + The other resources' data is the result of applying some conversion method or + a chain of such methods to the first resource's data. Detailed information + about which conversion method applied to which data resulted in which other + data is recorded in the tree `resourceTree` with the root `resourceTree.root` + and the non-root vertices `resourceTree.nonRootVertices`. + + Note that all resources describe the same data only in different formats. + """ + resources: [GetHttpsResource!]! + + """ + Multiple HTTP resources organized as tree with meta information of the actual + data requestable through the protocol HTTP over TLS, in short, HTTPS, with + `GET` requests. The root-resource's data is the result of applying the method + `appliedMethod`. Each non-root-resource's data is the result of applying + a specified conversion method to its parent resource's data. + + Note that all resources describe the same data only in different formats. + """ + resourceTree: GetHttpsResourceTree! + + """ + Approval by the database of the result without the field `approval` itself. + """ + approval: ResponseApproval! + + """ + Locale of text + """ + locale: Locale! +} + +""" +The CIELAB Color Space is used to define a color. It has three rectangular color coordinates, L* (lStar), a* (aStar) and b* (bStar). +""" +type CielabColor { + """ + The coordinate L* (lStar) defines the lightness of the color. + """ + lStar: Float! + + """ + The coordinate a* (aStar) defines the color between green and red. + """ + aStar: Float! + + """ + The coordinate b* (bStar) defines the color between blue and yellow. + """ + bStar: Float! +} + +type OpticalData implements Node & Data { + id: ID! + uuid: Uuid! + timestamp: DateTime! + locale: Locale! + databaseId: Uuid! + componentId: Uuid! + name: String + description: String + warnings: [String!]! + creatorId: Uuid! + createdAt: DateTime! + appliedMethod: AppliedMethod! + resources: [GetHttpsResource!]! + resourceTree: GetHttpsResourceTree! + approvals: [DataApproval!]! + approval: ResponseApproval! + + """ + Type + """ + type: OpticalComponentType + + """ + Subtype + """ + subtype: OpticalComponentSubtype + + """ + Coated side + """ + coatedSide: CoatedSide! + + """ + Integral nearnormal hemispherical visible transmittance values that occur in + the data If some or all of those values do not occur verbatim in the data, + they need not occur in the list, even if they can be calculated based on the + data. + """ + nearnormalHemisphericalVisibleTransmittances: [Float!]! + """ + Integral nearnormal hemispherical visible reflectance values that occur in + the data. If some or all of those values do not occur verbatim in the data, + they need not occur in the list, even if they can be calculated based on the + data. + """ + nearnormalHemisphericalVisibleReflectances: [Float!]! + + """ + Integral nearnormal hemispherical solar transmittance values that occur in + the data. If some or all of those values do not occur verbatim in the data, + they need not occur in the list, even if they can be calculated based on the + data. + """ + nearnormalHemisphericalSolarTransmittances: [Float!]! + + """ + Integral nearnormal hemispherical solar reflectance values that occur in the + data. If some or all of those values do not occur verbatim in the data, they + need not occur in the list, even if they can be calculated based on the data. + """ + nearnormalHemisphericalSolarReflectances: [Float!]! + + """ + Integral infrared emittance values that occur in the data. If some or all of + those values do not occur verbatim in the data, they need not occur in the + list, even if they can be calculated based on the data. + """ + infraredEmittances: [Float!]! + + """ + Integral values for the Color Rendering Index that occur in the data. If some + or all of those values do not occur verbatim in the data, they need not occur + in the list, even if they can be calculated based on the data. + """ + colorRenderingIndices: [Float!]! + + """ + Integral values for the coordinates in the CIELAB Color Space that occur in + the data. If some or all of those values do not occur verbatim in the data, + they need not occur in the list, even if they can be calculated based on the + data. + """ + cielabColors: [CielabColor!]! +} + +type CalorimetricData implements Node & Data { + id: ID! + uuid: Uuid! + timestamp: DateTime! + locale: Locale! + databaseId: Uuid! + componentId: Uuid! + name: String + description: String + warnings: [String!]! + creatorId: Uuid! + createdAt: DateTime! + appliedMethod: AppliedMethod! + resources: [GetHttpsResource!]! + resourceTree: GetHttpsResourceTree! + approvals: [DataApproval!]! + approval: ResponseApproval! + + """ + Integral `g` values that occur in the data. If some or all of those values do + not occur verbatim in the data, they need not occur in the list, even if they + can be calculated based on the data. + """ + gValues: [Float!]! + + """ + Integral `u` values in Watts per square meter Kelvin (W·m⁻²·K⁻¹) that occur + in the data. If some or all of those values do not occur verbatim in the in + the list, even if they can be calculated based on the data. + """ + uValues: [Float!]! +} + +type GeometricData implements Node & Data { + id: ID! + uuid: Uuid! + timestamp: DateTime! + locale: Locale! + databaseId: Uuid! + componentId: Uuid! + name: String + description: String + warnings: [String!]! + creatorId: Uuid! + createdAt: DateTime! + appliedMethod: AppliedMethod! + resources: [GetHttpsResource!]! + resourceTree: GetHttpsResourceTree! + approvals: [DataApproval!]! + approval: ResponseApproval! + + """ + Mirrored `thickness` values in meter (m) that occur in the data. If some or + all of those values do not occur verbatim in the data, they need not occur + in the list, even if they can be calculated based on the data. + """ + thicknesses: [Float!]! +} + +type HygrothermalData implements Node & Data { + id: ID! + uuid: Uuid! + timestamp: DateTime! + locale: Locale! + databaseId: Uuid! + componentId: Uuid! + name: String + description: String + warnings: [String!]! + creatorId: Uuid! + createdAt: DateTime! + appliedMethod: AppliedMethod! + resources: [GetHttpsResource!]! + resourceTree: GetHttpsResourceTree! + approvals: [DataApproval!]! + approval: ResponseApproval! +} + +type LifeCycleData implements Node & Data { + id: ID! + uuid: Uuid! + timestamp: DateTime! + locale: Locale! + databaseId: Uuid! + componentId: Uuid! + name: String + description: String + warnings: [String!]! + creatorId: Uuid! + createdAt: DateTime! + appliedMethod: AppliedMethod! + resources: [GetHttpsResource!]! + resourceTree: GetHttpsResourceTree! + approvals: [DataApproval!]! + approval: ResponseApproval! +} + +type PhotovoltaicData implements Node & Data { + id: ID! + uuid: Uuid! + timestamp: DateTime! + locale: Locale! + databaseId: Uuid! + componentId: Uuid! + name: String + description: String + warnings: [String!]! + creatorId: Uuid! + createdAt: DateTime! + appliedMethod: AppliedMethod! + resources: [GetHttpsResource!]! + resourceTree: GetHttpsResourceTree! + approvals: [DataApproval!]! + approval: ResponseApproval! +} + +""" +Applied method, that is the application of a method with fixed arguments to +specific source data. +""" +type AppliedMethod { + """ + Method identifier of method that was applied by the creator on the source + data. The method's meta data can be obtained by sending the GraphQL query + `query { method(id: ...) { ... } }` + to the metabase. + """ + methodId: Uuid! + + """ + Named arguments to configure the method. + - For each parameter required by the method, there is exactly one argument in + the list. + - Each argument in the list corresponds to an optional or required parameter + of the method. + - Each two distinct arguments in the list have different names. + """ + arguments: [NamedMethodArgument!]! + + """ + Named data sources to which the method was applied. + - For each source required by the method, there is exactly one source in the + list. + - Each source in the list corresponds to an optional or required source of + the method. + - Each two distinct sources in the list have different names. + """ + sources: [NamedMethodSource!]! +} + +""" +Applied conversion method to exactly one named source, namely the tree-vertex's +parent resource. +""" +type ToTreeVertexAppliedConversionMethod { + """ + Method identifier of conversion method that was applied by the creator on the + source data. The method's meta data can be obtained by sending the GraphQL + query + `query { method(id: ...) { ... } }` + to the metabase. + """ + methodId: Uuid! + + """ + Named arguments to configure the conversion method. + - For each parameter required by the method, there is exactly one argument in + the list. + - Each argument in the list corresponds to an optional or required parameter + of the method. + - Each two distinct arguments in the list have different names. + """ + arguments: [NamedMethodArgument!]! + + """ + Name of the named data source of the conversion method whose value is the + tree-vertex's parent resource. The conversion method requires at least one + named source and a value for one of these sources is given. + """ + sourceName: String! +} + +""" +Named argument of a method. +""" +type NamedMethodArgument { + """ + Name + """ + name: String! + + """ + Method argument value. + """ + value: Any! +} + +""" +Named data source of a method. +""" +type NamedMethodSource { + """ + Name + """ + name: String! + + """ + Method source as cross-database data reference. + """ + value: CrossDatabaseDataReference! +} + +""" +Cross-database data reference. +""" +type CrossDatabaseDataReference { + """ + Identifier of the referenced data. + """ + dataId: Uuid! + + """ + Timestamp of referenced data to designate the data's version. + """ + dataTimestamp: DateTime! + + """ + Kind of the referenced data. + """ + dataKind: DataKind! + + """ + Identifier of the database that manages the referenced data. + """ + databaseId: Uuid! +} + +""" +Data kind, either calorimetric, hygrothermal, lifeCycle, optical, or photovoltaic. +""" +enum DataKind { + CALORIMETRIC_DATA + GEOMETRIC_DATA + HYGROTHERMAL_DATA + LIFE_CYCLE_DATA + OPTICAL_DATA + PHOTOVOLTAIC_DATA +} + +""" +Numeration of a standard consisting of a main number and optional prefix and +suffix. + +For example, a standard may be named "ISO 52022-1" or "ASTM E2387", which have +"ISO" and "ASTM" as acronyms for the standardizers, "52022" and "2387" as main +numbers, "E" as prefix, and "1" as suffix. The suffix may indicate for example +a part number. +""" +type Numeration { + """ + Main number + """ + mainNumber: String! + + """ + Prefix + """ + prefix: String + + """ + Suffix + """ + suffix: String +} + +""" +Acronyms or abbreviations of standardization institutions. + +Additional information about each standardization institution is stored in the +metabase. +""" +enum Standardizer { + AERC + AGI + ASHRAE + BREEAM + BS + BSI + CEN + CIE + DGNB + DIN + DVWG + IEC + IES + IFT + ISO + JIS + LEED + NFRC + RIBA + UL + UNECE + VDI + VFF + WELL +} + +""" +Standard or publication. +""" +union Reference = Standard | Publication + +""" +`ISO 52022` is an example of the abbreviation of a standardizer and the main +number of the identifier. +""" +type Standard { + """ + Title + """ + title: String + + """ + Abstract + """ + abstract: String + + """ + Section referred to. + """ + section: String + + """ + Web address + """ + locator: Url + + """ + Numeration + """ + numeration: Numeration! + + """ + Standardizers + """ + standardizers: [Standardizer!]! + + """ + Year + + Important because there can be relevant updates of one standard. + """ + year: Int +} + +type Publication { + """ + Title + """ + title: String + + """ + Abstract + """ + abstract: String + + """ + Section referred to. + """ + section: String + + """ + Names of authors of the publication. + """ + authors: [String!] + + """ + arXiv identifier. [arXiv](https://arxiv.org) is a free distribution service + and an open-access archive for scholarly articles in the fields of physics, + mathematics, computer science, quantitative biology, quantitative finance, + statistics, electrical engineering and systems science, and economics. + """ + arXiv: ArXiv + + """ + Digital Object Identifier (DOI). It must be defined here if it exists. + """ + doi: Doi + + """ + Uniform Resource Name (URN). + """ + urn: Url + + """ + Web address. + + If a persistent identifiert like DOI is defined above, this address 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: Url +} + +""" +Approval with +[GnuPG](https://www.gnupg.org) +signature. + +To verify an approval, +1. install [GnuPG](https://www.gnupg.org), +2. save the approved message in the file `./message.json` (it is the value of + the field `message`), +3. save the ASCII-armored signature in the file `./message.json.asc` (it is the + value of the field `signature`), +4. * receive (and import) the public key for the given fingerprint from the + [OpenPGP Keyserver](https://keys.openpgp.org) by running + `gpg --keyserver hkps://keys.openpgp.org/ --receive-keys ` + where `` is the value of the field `keyFingerprint`. + * make sure that the key was not expired and was not revoked when the + approval was created by checking that the output of + `gpg --list-keys ` + does not say so: Look for the terms 'revoked' and 'expired'. + * make sure that the key belongs to the approver and was allowed for signing + and not revoked when the approval was created by sending a request of the + form + ``` + query { + institution(id: ) { + hasGnuPgKeyFingerprint( + where: { + and: [ + { fingerprint: { equalTo: } } + { allowedAt: { lessThanOrEqualTo: } } + { + or: [ + { forbiddenAt: { equalTo: null } } + { forbiddenAt: { greaterThan: } } + ] + } + ] + } + ) + } + } + ``` + to the metabase and checking that the response is + ``` + { + "data": { + "institution": { + "hasGnuPgKeyFingerprint": true + } + } + } + ``` + where ``, ``, and `` are the values + of the fields `approverId`, `keyFingerprint`, and `timestamp`. Alternatively, + retrieve the allowed-for-signing and not-revoked-at-the-given-time key + fingerprint from the metabase's GraphQL endpoint by sending a request of + the form + ``` + query { + institution(id: ) { + gnuPgKeyFingerprints( + where: { + and: [ + { fingerprint: { equalTo: } } + { allowedAt: { lessThanOrEqualTo: } } + { + or: [ + { forbiddenAt: { equalTo: null } } + { forbiddenAt: { greaterThan: } } + ] + } + ] + } + ) { + edges { + node { + fingerprint + allowedAt + forbiddenAt + user { + node { + uuid + name + } + } + } + } + totalCount + } + } + } + ``` + to the metabase and check that the response + ``` + { + "data": { + "institution": { + "gnuPgKeyFingerprints": { + "edges": [ + { + "node": { + "fingerprint": , + "allowedAt": ..., + "forbiddenAt": ..., + "user": { + "node": { + "uuid": ..., + "name": ... + } + } + } + } + ], + "totalCount": 1 + } + } + } + } + ``` + contains exactly one edge (and node). +5. verify the signature with the public key by running + `gpg --assert-signer --verify ./message.json.asc ./message.json`. + +To create an approval, +1. install [GnuPG](https://www.gnupg.org), +2. create a master key by running + `gpg --full-generate-key`, + selecting option `3. DSA (sign only)`, + choosing the keysize `2048`, + setting the validity `2y = key expires in 2 years`, and + answering all subsequent questions in particular adding a strong passphrase. +3. create a signing subkey by running + `gpg --edit-key `, + entering the command `addkey` + selecting option `(3) DSA (sign only)`, + choosing the keysize `2048`, + setting the validity `2y = key expires in 2 years`, and + answering all subsequent questions, and + entering the command `save`. +4. list your keys with fingerprints by running + `gpg --list-keys --with-subkey-fingerprint `. +5. add your signing key to the + [OpenPGP Keyserver](https://keys.openpgp.org) + by running + `gpg --keyserver hkps://keys.openpgp.org --send-keys ` + or by exporting the public key by running + `gpg --export --armor --output ./signing_key.pub.asc ` + and uploading it via the form on + [OpenPGP Key Upload](https://keys.openpgp.org/upload). +6. within a few minutes, verify the signing key's user ID, which is + ``, by following the verification link sent to you. +7. add the signing key fingerprint to the metabase with the GraphQL mutation + ``` + mutation { + addGnuPgKeyFingerprint(input: { + institutionId: + fingerprint: + }) { + errors { + code + message + path + } + } + } + ``` + and ask the institution owner to allow the key for signing with the GraphQL + mutation + ``` + mutation { + allowGnuPgKeyFingerprint(input: { + fingerprint: + }) { + errors { + code + message + path + } + } + } + ``` + Note that if you are the owner, the key is allowed automatically. + +For a response approval, +8. save the response of the present query in a file `./message.json`. +9. compute the signature and write it to `./message.json.asc` by running + `gpg --detach-sig --armor --local-user --output ./message.json.asc ./message.json`, and +10. add the approval to the final response of the present query through the + field `approval`. + +For a data approval, +8. query meta information of the data set to be signed with a GraphQL query + like + ``` + query { + opticalData(id: ) { + uuid + name + description + resourceTree { + root { + value { + hashValue + locator + dataFormatId + } + } + } + } + } + ``` + and load the data with an HTTPS GET request to the locator URL by running + `wget `. +9. review whether some statement is true for the data, for example, whether the + data conforms to some standard. If that is so, query the meta information to + be signed which at least contains the hash value of the data + ``` + query { + opticalData(id: ) { + resourceTree { + root { + value { + hashValue + } + } + } + } + } + ``` + make sure that the hash value is the same as before, wrap the statement and + the meta information (named `` below) like so + ``` + { + "statement": , + "response": + } + ``` + and save this in a file `./message.json`. The query should ask for the + information necessary to identify the meta information and the data set + itself uniquely like its ID and the data set's fingerprint, aka, hash value. + It should not ask for information whose change would invalidate the approval + unnecessaritly like the the verbal description of the data set. +10. compute the signature and write it to `./message.json.asc` by running + `gpg --detach-sig --armor --local-user --output ./message.json.asc ./message.json`, and +11. add the signature as data approval to the database through its interface, + which could for example have a GraphQL mutation `AddDataApproval`. + +Best practices for managing GnuPG keys within an institution: +* In the metabase, each institution has at least one representative. The + institution's GnuPG keys are the ones whose fingerprints are associated with + the institution. Fingerprints can be added by representatives and allowed and + forbidden for signing by instiution owners. The corresponding GraphQL + mutations are `addGnuPgKeyFingerprint`, `allowGnuPgKeyFingerprint`, and + `forbidGnuPgKeyFingerprint`. +* Each representative of an institution within the metabase who is supposed to + approve data for its institution adds its signing key to the + [OpenPGP Keyserver](https://keys.openpgp.org), + the key fingerprint to the metabase, and lets the fingerprint be allowed for + signing by the institution owner. +* When a representative's signing subkey is compromised she revokes it as + described in + [Revoking key components](https://www.gnupg.org/gph/en/manual.html#AEN305), + publishes the revocation to the keyserver by running + `gpg --keyserver hkps://keys.openpgp.org --send-key `, + and forbids the key for signing on the metabase with the mutation + `forbidGnuPgKeyFingerprint` +* When a representative leaves a company or is no longer allowed to approve data, + the metabase is informed about that with the GraphQL mutation + ``` + mutation { + removeInstitutionRepresentative(input: { + userId: + institutionId: + }) { + errors { + code + message + path + } + } + } + ``` + which also forbids all GnuPG keys associated with this representative from + signing. + +Best practices for accepting data approvals by databases: +* Check that at the time of approval creation, the fingerprint belonged to + a non-revoked and non-disabled GnuPG key. +* Check that the key was allowed to make signatures for that institution. +* Check that the query contains all necessary fields and sub-fields. +* Check that the message is constructed properly according to the documentation + of the implementing type `DataApproval`. +* Check that the signature is valid for the given message. + +Note that GnuPG is compliant with +[OpenPGP Message Format](https://tools.ietf.org/html/rfc4880). + +For an introduction to GnuPG consult +[The GNU Privacy Handbook](https://www.gnupg.org/gph/en/manual.html) +or the article series +[Getting started with GPG (GnuPG)](https://www.redhat.com/sysadmin/getting-started-gpg), +[How to create GPG keypairs](https://www.redhat.com/sysadmin/creating-gpg-keypairs), and +[Digital signatures with GnuPG](https://www.redhat.com/sysadmin/digital-signatures-gnupg). +""" +interface Approval { + """ + Creation timestamp + """ + timestamp: DateTime! + + """ + Detached ASCII-armored GnuPG signature of the message which includes the + GraphQL query response. + + With the message saved in the file `./message.json`, the signature can + be computed and written to `./message.json.asc` by running + `gpg --detach-sig --armor --output ./message.json.asc ./message.json` + (to write it to standard output instead, use the option `--output -`). And, + the signature can be verified by running + `gpg --verify ./message.json.asc ./message.json`. + Note that to compute a signature you need a GnuPG signing private key in your + key ring and to verify a signature you need the GnuPG signing public key + whose corresponding private key was used to compute the signature. + """ + signature: String! + + """ + Fingerprint of the GnuPG signing key used to compute the signature. + + The key's fingerprint, long identifier, and short identifier are related as + in the following example: + * fingerprint: 0D69 E11F 12BD BA07 7B37 26AB 4E1F 799A A4FF 2279 + * long id: 4E1F 799A A4FF 2279 + * short id: A4FF 2279 + Fingerprints can be used anywhere where an identifier is expected. For + security reasons it is used instead of long and short identifier. Its only + disadvantage is that it is less practical for manual human use. + """ + keyFingerprint: String! + + """ + [GraphQL query](https://graphql.org/learn/queries/) + whose response is included in the message being signed, for example, + ``` + query OpticalData($id: Uuid!) { + opticalData(id: $id) { ... } + } + ``` + Specifying the query explicitly is necessary because approvals shall not + become invalid when the GraphQL schema changes. + """ + query: GraphQlQuery! + + """ + [GraphQL variables](https://graphql.org/learn/queries/#variables) + defined by the GraphQL query, for example, + ``` + { "id": "018731a4-4c0a-49af-a5d9-f377fbd0a939" } + ``` + """ + variables: Any! + + """ + Message that is signed. + + For `ResponseApproval` it is the serialized GraphQL response of `query`. And + for `DataApproval` it is a serialized JSON combining the statement being made + and the response. See the documentation of `ResponseApproval#message` and + `DataApproval#message` for details. + + With the message put in the file `./message.json`, the GnuPG signature put in + the file `./message.json.asc`, and the GnuPG public key of the approver added + to the GnuPG key ring, the signature can be verified by running + `gpg --verify ./message.json.asc ./message.json`. + + To compare the message `./message.json` and another message `./another.json`, + run the command + ``` + diff --side-by-side --suppress-common-lines \ + <(jq --sort-keys --monochrome-output . ./message.json) \ + <(jq --sort-keys --monochrome-output . ./another.json) + ``` + in a + [Bourne-again shell (Bash)](https://www.gnu.org/software/bash/), + see + [Faster and simpler with the command line: deep-comparing two 5GB JSON files 3X faster by ditching the code](https://genius.engineering/faster-and-simpler-with-the-command-line-deep-comparing-two-5gb-json-files-3x-faster-by-ditching-the-code/) + and + [Using jq or alternative command line tools to compare JSON files](https://stackoverflow.com/questions/31930041/using-jq-or-alternative-command-line-tools-to-compare-json-files/37175540#37175540). + + Comparing messages is necessary to verify that the data `./another.json` you + received in a query that includes this approval matches the approved data. + Which is of course the case if all involved organizations are trustworthy. + However, verified signatures and matching messages together prove data + integrity (at least if private keys used to make the signatures in the first + place are not compromised). + """ + message: String! + + """ + Approver identifier issued by the metabase. The approver's meta data can be + obtained by sending the GraphQL query + `query { institution(id: ...) { ... } }` + to the metabase. For data approvals, the approver makes the statement about + the data identified by the query. For response approvals, the approver states + that the message is the response to the query by the present database. + """ + approverId: Uuid! +} + +""" +Approval of data by third party with GnuPG signature that is created at some +point in time. It becomes invalid when the approved data changes, even if the +change is but a spelling-error correction in meta data like the description. +In that case, the third party needs to re-create its approval. Unnecessary +invalidation can be avoided by choosing the meta data that is part of the +message being approved carefully. + +Steps to approve data: +1. An institution adds data with meta information to a database. +2. Some institution (may be the same) queries some meta information with + a GraphQL query like + ``` + query { + opticalData(id: ) { + uuid + name + description + resourceTree { + root { + value { + hashValue + locator + dataFormatId + } + } + } + } + } + ``` + and loads the data with an HTTPS GET request to the locator URL by running + `wget `. +3. The latter institution reviews whether some statement is true for the data, + for example, whether the data conforms to some standard. If that is so, the + institution queries the meta information to be signed which at least + contains the hash value of the data + ``` + query { + opticalData(id: ) { + resourceTree { + root { + value { + hashValue + } + } + } + } + } + ``` + makes sure that the hash value is the same as before, wraps the statement + and the meta information (named `` below) like so + ``` + { + "statement": , + "response": + } + ``` + and signs this JSON data. +4. The institution adds its approval of the data to some database or asks it to + be added. + +Storing the GraphQL query, variables, and message with the signature is +necessary because it needs to be known exactly which JSON data was signed (with +exact whitespaces). + +Note that the query to be signed only includes paths that are part of the data +and not part of some associates, which are actually all paths in the present +GraphQL schema. However, the actual GraphQL schema of the database is in +general an extension of the present GraphQL schema and may have paths to some +associates like data format specifics of HTTP resources that it may fetch from +the "metabase" and provides for convenience. These specifics should not be +signed as a change of, for example, the data format description would +invalidate the signature although the data itself did not change. +""" +type DataApproval implements Approval { + timestamp: DateTime! + signature: String! + keyFingerprint: String! + query: GraphQlQuery! + variables: Any! + approverId: Uuid! + + """ + Message that is signed. It is a string representation of the JSON + ``` + { + "statement": , + "response": + } + ``` + with whitespaces as when the signature was created, where `` is + the JSON of the field `statement` and `` is the JSON response of + sending the GraphQL request given by the field `query`. + """ + message: String! + + """ + Statement that is being made by the approver about the data identified by the + query. The reference could for example be an ISO standard, which would mean + that this approval states that the data conforms to this standard. + """ + statement: Reference! +} + +""" +Approval of response by database with +[GnuPG](https://www.gnupg.org) +signature. +""" +type ResponseApproval implements Approval { + timestamp: DateTime! + signature: String! + keyFingerprint: String! + query: GraphQlQuery! + variables: Any! + approverId: Uuid! + + """ + Serialized response of the GraphQL query with whitespaces as when the + signature was created. + """ + message: String! +} + +""" +HTTP resource with meta information requestable through the protocol +[HTTP over TLS](https://tools.ietf.org/html/rfc2818) +with `GET` requests. The protocol is colloquially called Hypertext Transfer +Protocol Secure (HTTPS) and uses +[Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc8446) +to secure +[Hypertext Transfer Protocol (HTTP)](https://tools.ietf.org/html/rfc7231) +connections over the internet. + +If an HTTP `GET` request of the resource is +[successful](https://tools.ietf.org/html/rfc7231#section-6.3) +with +[HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6) +[200](https://tools.ietf.org/html/rfc7231#section-6.3.1), +the response message body is the resource's content. Otherwise, there is some +issue that can be identified and hopefully resolved by examining the status +code. + +Note that the +[Internet Assigned Numbers Authority (IANA)](https://www.iana.org) +maintains the +[HTTP status code registry](https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml). +See also +[HTTP response status codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status). +""" +type GetHttpsResource { + """ + Description + """ + description: String + + """ + Hash value of successful HTTP response message body with + [HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6) + [200](https://tools.ietf.org/html/rfc7231#section-6.3.1) + computed with + Secure Hash Algorithm 256 (SHA-256) + as described in the + [United States's Federal Information Processing Standard 180-4 (FIPS 180-4)](https://csrc.nist.gov/publications/detail/fips/180/4/final) + by the + [National Institute of Standards and Technology (NIST)](https://www.nist.gov). + See also + [RFC 6234](https://tools.ietf.org/html/rfc6234). + + In Linux, + 1. install + [Coreutils - GNU core utilities](https://www.gnu.org/software/coreutils/), + to get the command-line tool + [`sha256sum`](http://www.gnu.org/software/coreutils/sha256sum), and + 2. save the message body in a file, say `message.body`. + + Then, to compute the hash value, + 3. run the command `sha256sum message.body`, which prints the hash value to + standard output. + + And, to check the hash value, + 4. run the command `echo ' message.body' | sha256sum --check`, + where `` needs to be replaced by the hash value. Note that + there are two spaces between `` and `message.body`. + + In Mac OS X, the command-line tool `shasum` comes pre-installed and can be + used instead of `sha256sum` by adding the option `--algorithm 256` that is + by literally replacing `sha256sum` in the above commands by + `shasum --algorithm 256`. + + In Windows, + 1. install + [PowerShell](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell) + 2. save the message body in a file, say `message.body`. + 3. open a PowerShell window, for example, by pressing `WIN+R`, + typing in `powershell`, and pressing ``, and + 4. change directory to the folder with the saved files. + + Then, to compute the hash value, + 5. run the command + `(Get-FileHash -Algorithm SHA256 .\message.body).Hash`, + which prints the hash value. For details see the documentation + [Get-FileHash](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-filehash). + + And, to check the hash value, + 6. save the message body's hash in a file, say `message.body.sha`, + 7. run the command + `(Get-FileHash -Algorithm SHA256 .\message.body).Hash -eq (Get-Content .\message.body.sha).split(" ")[0].ToUpper()`, + which prints `True` on success. + + In case [OpenSSL](https://www.openssl.org) is installed, to compute the hash + value, you may also run `openssl dgst -sha256 message.body`. + """ + hashValue: String! + + """ + Locator with scheme `https` to be used with protocol + [HTTP over TLS](https://tools.ietf.org/html/rfc2818). + """ + locator: Url! + + """ + Identifier of data format of successful HTTP response message body with + [HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6) + [200](https://tools.ietf.org/html/rfc7231#section-6.3.1). + The identifier is issued by the metabase. The format's meta data can be + obtained by sending the GraphQL query + `query { dataFormat(id: ...) { ... } }` + to the metabase. + + The data format consists of a + [media type](https://www.iana.org/assignments/media-types/media-types.xhtml) + and additional information to unambiguously identify the specific format and + to locate the format specification. + + In case of an + [archive format](https://en.wikipedia.org/wiki/Archive_file), + for each archived file (that is, each file in the archive), meta information + like its data format is specified in `archivedFilesMetaInformation`. The file + and directory names in the archive match the regular expression + `^[0-9a-z-._]$` for reasons elaborated in `FileMetaInformation#path`. See + also + [List of archive formats](https://en.wikipedia.org/wiki/List_of_archive_formats). + """ + dataFormatId: Uuid! + + """ + Archived files meta information with relative paths from the archive root and + data format specifications. In case the data format is not an archive, the + list is empty. Otherwise, in case the data format is an, for our purposes, + underspecified archive format like + [7-Zip](https://www.7-zip.org) + with an a priori unknown internal structure, for each archived file, there is + exactly one list entry. + + Note that the + [extensions of file names](https://en.wikipedia.org/wiki/Filename_extension) + in the archive are not sufficient as data format because file extensions are + not standardized and not sufficiently precise. For example, the extension + `docx` is commonly used for documents with the media type + [application/vnd.openxmlformats-officedocument.wordprocessingml.document](https://www.iana.org/assignments/media-types/application/vnd.openxmlformats-officedocument.wordprocessingml.document) + but does not tell which exact version of the standard is meant. + """ + archivedFilesMetaInformation: [FileMetaInformation!]! +} + +""" +Multiple HTTP resources organized as tree with meta information of the actual +data requestable through the protocol HTTP over TLS, in short, HTTPS, with +`GET` requests. Each vertex specifies +* its vertex identifier (unique among the tree vertices), and +* its HTTP resource. +Each non-root vertex also specifies +* its parent vertex's identifier, and +* an applied conversion method with its parent vertex's identifier as source, +where indeed the non-root-resource's data is the result of applying the +conversion method to its parent resource's data. + +The root vertex is in the field `root` and non-root resource vertices are in +the field `nonRootVertices`. + +Note that all resources describe the same data only in different formats. +""" +type GetHttpsResourceTree { + """ + Root resource vertex. + """ + root: GetHttpsResourceTreeRoot! + + """ + Non-root resource vertices in breadth-first order starting from, but not + including, the root vertex. + + By traversing the tree from the root at each vertex it is clear how the + vertex's data was obtained from the parent's data and by remembering or + composing the chain of applied conversion methods how the vertex's data was + obtained from the root's data. + """ + nonRootVertices: [GetHttpsResourceTreeNonRootVertex!]! +} + +""" +A root or non-root resource is the result of some measurement or simulation, or +of applying some conversion method to its parent resource. Its child resources +are the results of applying conversion methods to it, the child resources of +its child resources are the results of applying conversion methods to the child +resources, and so forth. The applied conversion methods are specified in the +child resource vertices. +""" +interface GetHttpsResourceTreeVertex { + """ + Vertex identifier unique among the tree vertices. It is used to set-up edges, + that is, parent/child relationships, in the flattened tree structure. + """ + vertexId: ID! + + """ + Resource + """ + value: GetHttpsResource! +} + +""" +A root resource is the direct result of some measurement or simulation. +""" +type GetHttpsResourceTreeRoot implements GetHttpsResourceTreeVertex { + vertexId: ID! + value: GetHttpsResource! +} + +""" +A non-root resource is the result of applying some conversion method to its +parent resource. +""" +type GetHttpsResourceTreeNonRootVertex implements GetHttpsResourceTreeVertex { + vertexId: ID! + value: GetHttpsResource! + + """ + Identifier of parent resource vertex. This vertex's resource data it obtained by + applying the conversion method to its parent resource data. + """ + parentId: ID! + + """ + Conversion method applied to the parent resource to obtain the present + vertex's resource. The parent resource is identified by the vertex identifier + `appliedConversionMethod.source.value`. + """ + appliedConversionMethod: ToTreeVertexAppliedConversionMethod! +} + +""" +File meta information. +""" +type FileMetaInformation { + """ + Relative file path, where + * the string-serialized + [POSIX-style path](https://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap03.html#tag_03_266) + is obtained by prepending the string `./` and joining all components with the + forward slash `/`, see also, + [IEEE/Open Group 1003.1-2017 - IEEE Standard for Information Technology--Portable Operating System Interface (POSIX(TR)) Base Specifications, Issue 7](https://standards.ieee.org/standard/1003_1-2017.html), + and + * the string-serialized + [Windows-style path](https://docs.microsoft.com/en-us/dotnet/standard/io/file-path-formats) + is obtained by prepending the + string `.\` and joining all components with the backward slash `\`, see also + [Naming Files, Paths, and Namespaces](https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file). + + Each path component is a valid file or directory name in popular operating + systems like various Linux distributions, Windows, and Mac OS X, and it is + usable. Because of various idiosyncrasies of different file systems and + command-line interpreters, each component matches the regular expression + `^[a-z0-9-._]+$`. For some reasons see + [Answer 1](https://superuser.com/questions/358855/what-characters-are-safe-in-cross-platform-file-names-for-linux-windows-and-os/358861#358861) + and + [Answer 2](https://superuser.com/questions/358855/what-characters-are-safe-in-cross-platform-file-names-for-linux-windows-and-os/748264#748264) + to the dated question + [What characters are safe in cross-platform file names for Linux, Windows and OS-X](https://superuser.com/questions/358855/what-characters-are-safe-in-cross-platform-file-names-for-linux-windows-and-os), + and also + [Best Practices for Filenames on the Web](https://www.surrealcms.com/blog/best-practices-for-filenames-on-the-web.html). + """ + path: [String!]! + + """ + Identifier of the data format. The identifier is issued by the metabase. The + format's meta data can be obtained by sending the GraphQL query + `query { dataFormat(id: ...) { ... } }` + to the metabase. + + The data format consists of a + [media type](https://www.iana.org/assignments/media-types/media-types.xhtml) + and additional information to unambiguously identify the specific format and + to locate the format specification. + """ + dataFormatId: Uuid! +} diff --git a/backend/src/GraphQl/DescriptionOrReferences/DescriptionOrReferenceFilterType.cs b/backend/src/GraphQl/DescriptionOrReferences/DescriptionOrReferenceFilterType.cs index 9400616e2..1ae7324a7 100644 --- a/backend/src/GraphQl/DescriptionOrReferences/DescriptionOrReferenceFilterType.cs +++ b/backend/src/GraphQl/DescriptionOrReferences/DescriptionOrReferenceFilterType.cs @@ -10,7 +10,9 @@ protected override void Configure( IFilterInputTypeDescriptor descriptor ) { + base.Configure(descriptor); + descriptor.Name(nameof(DescriptionOrReferenceFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); descriptor.BindFieldsExplicitly(); - descriptor.Field(x => x.Description); + descriptor.Field(_ => _.Description); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/DescriptionOrReferences/DescriptionOrReferenceSortType.cs b/backend/src/GraphQl/DescriptionOrReferences/DescriptionOrReferenceSortType.cs index 7f848c06f..d2b5be123 100644 --- a/backend/src/GraphQl/DescriptionOrReferences/DescriptionOrReferenceSortType.cs +++ b/backend/src/GraphQl/DescriptionOrReferences/DescriptionOrReferenceSortType.cs @@ -10,7 +10,9 @@ protected override void Configure( ISortInputTypeDescriptor descriptor ) { + base.Configure(descriptor); + descriptor.Name(nameof(DescriptionOrReferenceSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); descriptor.BindFieldsExplicitly(); - descriptor.Field(x => x.Description); + descriptor.Field(_ => _.Description); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/DescriptionOrReferences/DescriptionOrReferenceType.cs b/backend/src/GraphQl/DescriptionOrReferences/DescriptionOrReferenceType.cs index 69f5dfea8..e409642d3 100644 --- a/backend/src/GraphQl/DescriptionOrReferences/DescriptionOrReferenceType.cs +++ b/backend/src/GraphQl/DescriptionOrReferences/DescriptionOrReferenceType.cs @@ -18,6 +18,7 @@ IObjectTypeDescriptor descriptor descriptor .Field(t => t.Reference) .Type() + .Cost(0) .Resolve(context => context .Parent() .Reference? diff --git a/backend/src/GraphQl/Edge.cs b/backend/src/GraphQl/Edge.cs index 7e09a0209..d5bf9be3d 100644 --- a/backend/src/GraphQl/Edge.cs +++ b/backend/src/GraphQl/Edge.cs @@ -2,21 +2,21 @@ using System.Threading; using System.Threading.Tasks; using GreenDonut; +using HotChocolate.CostAnalysis.Types; namespace Metabase.GraphQl; public abstract class Edge( Guid nodeId - ) - where TNodeByIdDataLoader : IDataLoader +) + where TNodeByIdDataLoader : IDataLoader { - private readonly Guid _nodeId = nodeId; - + [Cost(0)] public async Task GetNodeAsync( TNodeByIdDataLoader byId, CancellationToken cancellationToken ) { - return (await byId.LoadAsync(_nodeId, cancellationToken))!; + return (await byId.LoadAsync(nodeId, cancellationToken))!; } } \ No newline at end of file diff --git a/backend/src/GraphQl/Entities/AssociationsByAssociateIdDataLoader.cs b/backend/src/GraphQl/Entities/AssociationsByAssociateIdDataLoader.cs deleted file mode 100644 index 20f7bd761..000000000 --- a/backend/src/GraphQl/Entities/AssociationsByAssociateIdDataLoader.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using GreenDonut; -using GreenDonut.Data; -using Metabase.Data; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Entities; - -public abstract class AssociationsByAssociateIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory, - Func, QueryContext, IQueryable> getAssociations, - Func getAssociateId - ) - : StatefulGroupedDataLoader(batchScheduler, options) -{ - private readonly IDbContextFactory _dbContextFactory = dbContextFactory; - - private readonly Func _getAssociateId = getAssociateId; - - private readonly Func, QueryContext, IQueryable> - _getAssociations = getAssociations; - - protected override async Task> LoadGroupedBatchAsync( - IReadOnlyList keys, - DataLoaderFetchContext context, - CancellationToken cancellationToken - ) - { - await using var dbContext = - _dbContextFactory.CreateDbContext(); - return ( - await _getAssociations( - dbContext, - keys, - context.GetQueryContext() - ) - .ToListAsync(cancellationToken) - ) - .ToLookup(_getAssociateId); - } -} \ No newline at end of file diff --git a/backend/src/GraphQl/Entities/AuditableEntityFilterType.cs b/backend/src/GraphQl/Entities/AuditableEntityFilterType.cs new file mode 100644 index 000000000..08a56dc4d --- /dev/null +++ b/backend/src/GraphQl/Entities/AuditableEntityFilterType.cs @@ -0,0 +1,20 @@ +using HotChocolate.Data.Filters; +using Metabase.Data; + +namespace Metabase.GraphQl.Entities; + +public abstract class AuditableEntityFilterType + : FilterInputType + where TEntity : IEntity, IAuditable +{ + protected override void Configure( + IFilterInputTypeDescriptor descriptor + ) + { + descriptor.BindFieldsExplicitly(); + descriptor.Field(_ => _.Id); + descriptor.Field(_ => _.CreatedAt); + descriptor.Field(_ => _.UpdatedAt); + // TODO Do we want to filter by: descriptor.Field(_ => _.Version); + } +} diff --git a/backend/src/GraphQl/Entities/EntitySortType.cs b/backend/src/GraphQl/Entities/AuditableEntitySortType.cs similarity index 51% rename from backend/src/GraphQl/Entities/EntitySortType.cs rename to backend/src/GraphQl/Entities/AuditableEntitySortType.cs index 8cb694f00..af1b2742a 100644 --- a/backend/src/GraphQl/Entities/EntitySortType.cs +++ b/backend/src/GraphQl/Entities/AuditableEntitySortType.cs @@ -3,15 +3,18 @@ namespace Metabase.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(_ => _.Id); + descriptor.Field(_ => _.CreatedAt); + descriptor.Field(_ => _.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 b6c5fad7f..000000000 --- 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 GreenDonut; -using Metabase.Data; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.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/EntityFilterType.cs b/backend/src/GraphQl/Entities/EntityFilterType.cs deleted file mode 100644 index 1c00bb230..000000000 --- a/backend/src/GraphQl/Entities/EntityFilterType.cs +++ /dev/null @@ -1,17 +0,0 @@ -using HotChocolate.Data.Filters; -using Metabase.Data; - -namespace Metabase.GraphQl.Entities; - -public abstract class EntityFilterType - : FilterInputType - where TEntity : IEntity -{ - protected override void Configure( - IFilterInputTypeDescriptor descriptor - ) - { - descriptor.BindFieldsExplicitly(); - descriptor.Field(x => x.Id); - } -} \ No newline at end of file diff --git a/backend/src/GraphQl/Entities/EntityType.cs b/backend/src/GraphQl/Entities/EntityType.cs index 73b056cc1..0afd3506c 100644 --- a/backend/src/GraphQl/Entities/EntityType.cs +++ b/backend/src/GraphQl/Entities/EntityType.cs @@ -2,18 +2,20 @@ using GreenDonut; using HotChocolate.Types; using Metabase.Data; +using Metabase.GraphQl.Scalars; namespace Metabase.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 a6f04e174..05a5bd539 100644 --- a/backend/src/GraphQl/ErrorLoggingDiagnosticEventListener.cs +++ b/backend/src/GraphQl/ErrorLoggingDiagnosticEventListener.cs @@ -1,15 +1,15 @@ using System; using System.Collections.Generic; -using System.Globalization; -using System.Text; using System.Text.RegularExpressions; using Metabase.Logging; using HotChocolate; using HotChocolate.Execution; using HotChocolate.Execution.Instrumentation; -using HotChocolate.Execution.Processing; using HotChocolate.Resolvers; using Microsoft.Extensions.Logging; +using HotChocolate.Language; +using System.Text; +using System.Globalization; namespace Metabase.GraphQl; @@ -27,45 +27,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( @@ -97,7 +99,8 @@ public static partial void OperationError( this ILogger logger, IOperationDocument? document, string? variables, - [TagProvider(typeof(HotChocolateIErrorTagProvider), nameof(HotChocolateIErrorTagProvider.RecordTags))] IError error + [TagProvider(typeof(HotChocolateIErrorTagProvider), nameof(HotChocolateIErrorTagProvider.RecordTags))] IError error, + Exception? exception ); [LoggerMessage( @@ -128,55 +131,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 +194,7 @@ IError error } public override void ValidationErrors( - IRequestContext context, + RequestContext context, IReadOnlyList errors ) { @@ -200,7 +205,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(); @@ -213,12 +218,8 @@ private sealed partial class RequestScope(ILogger 0 } operationResult) + if (context.Result is OperationResult { Errors.Count: > 0 } operationResult) { foreach (var error in operationResult.Errors) { - logger.OperationError(context.Request.Document, StringifyVariables(), error); + logger.OperationError(context.Request.Document, StringifyVariables(), error, error.Exception); } } - 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 000000000..28d4c9e61 --- /dev/null +++ b/backend/src/GraphQl/Extensions/PageExtensions.cs @@ -0,0 +1,71 @@ +using System.Threading.Tasks; +using System.Diagnostics.Contracts; +using GreenDonut.Data; +using HotChocolate.Types.Pagination; + +namespace Metabase.GraphQl.Extensions; + +// Inspired by https://github.com/ChilliCream/graphql-platform/blob/main/src/HotChocolate/Data/src/Data/Extensions/HotChocolatePaginationResultExtensions.cs +public static class PageExtensions +{ + [Pure] + public static async ValueTask GetTotalCountAsync( + this Task?> pagePromise + ) + { + return (await pagePromise)?.TotalCount ?? 0; + } + + [Pure] + 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() + ); + } + + // [Pure] + // 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); + // } + + // [Pure] + // 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 2c068ddbc..a5054d3b8 100644 --- a/backend/src/GraphQl/Extensions/ResolverContextExtensions.cs +++ b/backend/src/GraphQl/Extensions/ResolverContextExtensions.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.Contracts; using GreenDonut.Data; using HotChocolate.Data.Filters; using HotChocolate.Data.Sorting; @@ -8,16 +9,30 @@ namespace Metabase.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 + [Pure] 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 + [Pure] + 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 11bc96a7d..000000000 --- a/backend/src/GraphQl/Extensions/SortingContextExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Linq; -using HotChocolate.Data.Sorting; -using Metabase.Data; - -namespace Metabase.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); - }); - } -} \ No newline at end of file diff --git a/backend/src/GraphQl/Filters/ScalarFilterInputTypes.cs b/backend/src/GraphQl/Filters/ScalarFilterInputTypes.cs index 770600719..eba7c072a 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 Metabase.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 Metabase.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); } } @@ -136,6 +154,16 @@ protected override void Configure(IFilterInputTypeDescriptor descriptor) } } +public sealed class NonNegativeIntFilterInputType + : ExtendedComparableOperationFilterInputType +{ + protected override void Configure(IFilterInputTypeDescriptor descriptor) + { + descriptor.Name($"NonNegativeInt{GraphQlConstants.FilterInputSuffix}"); + base.Configure(descriptor); + } +} + public sealed class DecimalFilterInputType : ExtendedComparableOperationFilterInputType { @@ -146,12 +174,12 @@ protected override void Configure(IFilterInputTypeDescriptor descriptor) } } -public sealed class UrlFilterInputType - : ExtendedComparableOperationFilterInputType +public sealed class UriFilterInputType + : ExtendedComparableOperationFilterInputType { protected override void Configure(IFilterInputTypeDescriptor descriptor) { descriptor.Name($"Url{GraphQlConstants.FilterInputSuffix}"); base.Configure(descriptor); } -} +} \ No newline at end of file diff --git a/backend/src/GraphQl/GeometricDataX/GeometricData.cs b/backend/src/GraphQl/GeometricDataX/GeometricData.cs new file mode 100644 index 000000000..a44a998ce --- /dev/null +++ b/backend/src/GraphQl/GeometricDataX/GeometricData.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate.Resolvers; +using HotChocolate.Types.Relay; +using Metabase.Data; +using Metabase.GraphQl.DataX; +using Metabase.GraphQl.Requests; +using NodaTime; + +namespace Metabase.GraphQl.GeometricDataX; + +[Node(IdField = nameof(Id))] +public sealed record GeometricData( + string Id, + Guid Uuid, + OffsetDateTime Timestamp, + string Locale, + Guid DatabaseId, + Guid ComponentId, + string? Name, + string? Description, + IReadOnlyList Warnings, + Guid CreatorId, + OffsetDateTime CreatedAt, + AppliedMethod AppliedMethod, + IReadOnlyList Resources, + GetHttpsResourceTree ResourceTree, + IReadOnlyList Approvals, + // ResponseApproval Approval, + IReadOnlyList Thicknesses +) +: DataX.Data( + Id, + Uuid, + Timestamp, + Locale, + DatabaseId, + ComponentId, + Name, + Description, + Warnings, + CreatorId, + CreatedAt, + AppliedMethod, + Resources, + ResourceTree, + Approvals +// Approval +) +{ + public override DataKind Kind { get => DataKind.GEOMETRIC_DATA; } + + [NodeResolver] + public static Task GetAsync( + string id, + DataQueries dataQueries, + ApplicationDbContext databaseContext, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + return DataX.Data.FetchNodeAsync( + id, + (database, uuid, locale) => dataQueries.GetGeometricDataAsync( + database, + uuid, + locale, + resolverContext, + cancellationToken + ), + databaseContext, + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/GeometricDataConnection.cs b/backend/src/GraphQl/GeometricDataX/GeometricDataConnection.cs similarity index 52% rename from backend/src/GraphQl/DataX/GeometricDataConnection.cs rename to backend/src/GraphQl/GeometricDataX/GeometricDataConnection.cs index 430e4d7c7..92608283f 100644 --- a/backend/src/GraphQl/DataX/GeometricDataConnection.cs +++ b/backend/src/GraphQl/GeometricDataX/GeometricDataConnection.cs @@ -1,13 +1,14 @@ using System.Collections.Generic; using HotChocolate.Types.Pagination; +using Metabase.GraphQl.DataX; -namespace Metabase.GraphQl.DataX; +namespace Metabase.GraphQl.GeometricDataX; public sealed record GeometricDataConnection( - IReadOnlyList Edges, - uint TotalCount, + IReadOnlyList? Edges, + int TotalCount, ConnectionPageInfo PageInfo -) : DataConnectionBase( +) : DataConnection( Edges, TotalCount, PageInfo diff --git a/backend/src/GraphQl/DataX/GeometricDataEdge.cs b/backend/src/GraphQl/GeometricDataX/GeometricDataEdge.cs similarity index 54% rename from backend/src/GraphQl/DataX/GeometricDataEdge.cs rename to backend/src/GraphQl/GeometricDataX/GeometricDataEdge.cs index 40c3383f4..5c740c036 100644 --- a/backend/src/GraphQl/DataX/GeometricDataEdge.cs +++ b/backend/src/GraphQl/GeometricDataX/GeometricDataEdge.cs @@ -1,11 +1,12 @@ using System; +using Metabase.GraphQl.DataX; -namespace Metabase.GraphQl.DataX; +namespace Metabase.GraphQl.GeometricDataX; public sealed record GeometricDataEdge( string Cursor, GeometricData Node -) : DataEdgeBase( +) : DataEdge( Cursor, Node ); \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/GeometricDataPropositionInput.cs b/backend/src/GraphQl/GeometricDataX/GeometricDataPropositionInput.cs similarity index 65% rename from backend/src/GraphQl/DataX/GeometricDataPropositionInput.cs rename to backend/src/GraphQl/GeometricDataX/GeometricDataPropositionInput.cs index 404ef5ad7..0d7f3eba7 100644 --- a/backend/src/GraphQl/DataX/GeometricDataPropositionInput.cs +++ b/backend/src/GraphQl/GeometricDataX/GeometricDataPropositionInput.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; +using Metabase.GraphQl.DataX; -namespace Metabase.GraphQl.DataX; +namespace Metabase.GraphQl.GeometricDataX; public sealed record GeometricDataPropositionInput( UuidPropositionInput? ComponentId, @@ -9,4 +10,8 @@ public sealed record GeometricDataPropositionInput( IReadOnlyList? Or, GetHttpsResourcesPropositionInput? Resources, FloatsPropositionInput? Thicknesses -); \ No newline at end of file +) +{ + public static readonly GeometricDataPropositionInput Empty = + new(null, null, null, null, null, null); +}; \ No newline at end of file diff --git a/backend/src/GraphQl/GeometricDataX/GeometricDataQueries.cs b/backend/src/GraphQl/GeometricDataX/GeometricDataQueries.cs new file mode 100644 index 000000000..6e27642d3 --- /dev/null +++ b/backend/src/GraphQl/GeometricDataX/GeometricDataQueries.cs @@ -0,0 +1,105 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.CostAnalysis.Types; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using Metabase.Data; +using Metabase.GraphQl.Requests; +using Metabase.GraphQl.Scalars; +using Microsoft.EntityFrameworkCore; + +namespace Metabase.GraphQl.GeometricDataX; + +[ExtendObjectType(nameof(Query))] +public sealed class GeometricDataQueries +{ + public async Task GetGeometricDataAsync( + Guid databaseId, + Guid id, + [GraphQLType] string? locale, + DataQueries dataQueries, + ApplicationDbContext databaseContext, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + var database = await databaseContext.Databases.AsNoTracking() + .Where(_ => _.Id == databaseId) + .SingleOrDefaultAsync(cancellationToken); + if (database is null) + { + return null; + } + return await dataQueries.GetGeometricDataAsync( + database, + id, + locale, + resolverContext, + cancellationToken + ); + } + + [ListSize( + AssumedSize = (int)GraphQlConstants.MaximumPageSize - 1, + SlicingArguments = ["first", "last"], + SlicingArgumentDefaultValue = (int)GraphQlConstants.MaximumPageSize - 1, + SizedFields = ["edges", "nodes"], + RequireOneSlicingArgument = false + )] + public Task GetAllGeometricDataAsync( + GeometricDataPropositionInput? where, + [GraphQLType] string? locale, + int? first, + string? after, + int? last, + string? before, + DataQueries dataQueries, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + return dataQueries.GetAllDataAsync( + first, + after, + last, + before, + (edges, totalCount, pageInfo) => new GeometricDataConnection(edges, totalCount, pageInfo), + (node, cursor) => new GeometricDataEdge(cursor, node), + (database, first, after, last, before) => dataQueries.GetAllGeometricDataAsync( + database, + where, + locale, + first, + after, + last, + before, + resolverContext, + cancellationToken + ), + cancellationToken + ); + } + + public Task HasGeometricDataAsync( + GeometricDataPropositionInput? where, + [GraphQLType] string? locale, + DataQueries dataQueries, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + return dataQueries.HasDataAsync( + (database) => dataQueries.HasGeometricDataAsync( + database, + where, + locale, + resolverContext, + cancellationToken + ), + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintByFingerprintDataLoader.cs b/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintByFingerprintDataLoader.cs deleted file mode 100644 index 91d70afae..000000000 --- a/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintByFingerprintDataLoader.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using GreenDonut; -using Metabase.Data; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.GnuPgKeyFingerprints; - -public sealed class GnuPgKeyFingerprintByFingerprintDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : BatchDataLoader(batchScheduler, options) -{ - protected override async Task> LoadBatchAsync( - IReadOnlyList keys, - CancellationToken cancellationToken - ) - { - await using var dbContext = - dbContextFactory.CreateDbContext(); - return await dbContext.GnuPgKeyFingerprints.AsNoTrackingWithIdentityResolution() - .Where(f => keys.Contains(f.Fingerprint)) - .ToDictionaryAsync( - f => f.Fingerprint, - f => (GnuPgKeyFingerprint?)f, - cancellationToken - ); - } -} \ No newline at end of file diff --git a/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintByIdDataLoader.cs b/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintByIdDataLoader.cs deleted file mode 100644 index 09894d8ad..000000000 --- a/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintByIdDataLoader.cs +++ /dev/null @@ -1,20 +0,0 @@ -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.GnuPgKeyFingerprints; - -public sealed class GnuPgKeyFingerprintByIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : EntityByIdDataLoader( - batchScheduler, - options, - dbContextFactory, - dbContext => dbContext.GnuPgKeyFingerprints - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintDataLoaders.cs b/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintDataLoaders.cs new file mode 100644 index 000000000..86a32da6c --- /dev/null +++ b/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintDataLoaders.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut; +using GreenDonut.Data; +using Metabase.Data; +using Microsoft.EntityFrameworkCore; + +namespace Metabase.GraphQl.GnuPgKeyFingerprints; + +public sealed class GnuPgKeyFingerprintDataLoaders +: DataLoaders +{ + [DataLoader] + public static ValueTask> GetGnuPgKeyFingerprintByIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetEntityByIdAsync( + ids, + databaseContext => databaseContext.GnuPgKeyFingerprints, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static async ValueTask> GetGnuPgKeyFingerprintByFingerprintAsync( + IReadOnlyList fingerprints, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + await using var databaseContext = await databaseContextFactory.CreateDbContextAsync(cancellationToken); + return await databaseContext.GnuPgKeyFingerprints + .AsNoTrackingWithIdentityResolution() + .Where(_ => fingerprints.Contains(_.Fingerprint)) + .With(queryContext, Sorting.DefaultEntityOrder) + .ToDictionaryAsync(_ => _.Fingerprint, cancellationToken); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintFilterType.cs b/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintFilterType.cs index 753e9cb62..f5a1d35c6 100644 --- a/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintFilterType.cs +++ b/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintFilterType.cs @@ -5,18 +5,22 @@ namespace Metabase.GraphQl.GnuPgKeyFingerprints; public class GnuPgKeyFingerprintFilterType - : EntityFilterType + : AuditableEntityFilterType { protected override void Configure( IFilterInputTypeDescriptor descriptor ) { base.Configure(descriptor); - descriptor.Field(x => x.Fingerprint); - descriptor.Field(x => x.CreatedAt); - descriptor.Field(x => x.AllowedAt); - descriptor.Field(x => x.ForbiddenAt); - descriptor.Field(x => x.User); - descriptor.Field(x => x.Institution); + // TODO Remove Id, CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(_ => _.Id); + descriptor.Field(_ => _.CreatedAt); + descriptor.Field(_ => _.UpdatedAt); + descriptor.Field(_ => _.Fingerprint); + descriptor.Field(_ => _.CreatedAt); + descriptor.Field(_ => _.AllowedAt); + descriptor.Field(_ => _.ForbiddenAt); + descriptor.Field(_ => _.User); + descriptor.Field(_ => _.Institution); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintInstitutionEdge.cs b/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintInstitutionEdge.cs index 07627d59b..193e49edf 100644 --- a/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintInstitutionEdge.cs +++ b/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintInstitutionEdge.cs @@ -6,6 +6,6 @@ namespace Metabase.GraphQl.GnuPgKeyFingerprints; public sealed class GnuPgKeyFingerprintInstitutionEdge( GnuPgKeyFingerprint association ) - : Edge(association.InstitutionId) + : Edge(association.InstitutionId) { } \ No newline at end of file diff --git a/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintMutations.cs b/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintMutations.cs index bb86cd774..169403ad6 100644 --- a/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintMutations.cs +++ b/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintMutations.cs @@ -12,6 +12,7 @@ using Metabase.GraphQl.Users; using Metabase.Services; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Metabase.GraphQl.GnuPgKeyFingerprints; @@ -26,6 +27,7 @@ public async Task AddGnuPgKeyFingerprintAsync( GnuPgKeyFingerprintAuthorization authorization, GnuPgService gnuPgService, ApplicationDbContext context, + IClock clock, CancellationToken cancellationToken ) { @@ -78,9 +80,10 @@ CancellationToken cancellationToken ); } - var gnuPgKeyVerificationResult = await gnuPgService.VerifyGnuPgKey( + var gnuPgKeyVerificationResult = await gnuPgService.VerifyGnuPgKeyAsync( normalizedFingerprint, - user.Email ?? "" + user.Email ?? "", + cancellationToken ); if (gnuPgKeyVerificationResult is not GnuPgKeyVerificationResult.SUCCESS) { @@ -123,7 +126,7 @@ CancellationToken cancellationToken ) ) { - fingerprint.Allow(); + fingerprint.Allow(clock); } context.GnuPgKeyFingerprints.Add(fingerprint); await context.SaveChangesAsync(cancellationToken); @@ -137,6 +140,7 @@ public async Task AllowGnuPgKeyFingerprintAsync ClaimsPrincipal claimsPrincipal, GnuPgKeyFingerprintAuthorization authorization, ApplicationDbContext context, + IClock clock, CancellationToken cancellationToken ) { @@ -170,7 +174,7 @@ CancellationToken cancellationToken ) ); } - fingerprint.Allow(); + fingerprint.Allow(clock); await context.SaveChangesAsync(cancellationToken); return new AllowGnuPgKeyFingerprintPayload(fingerprint); } @@ -182,6 +186,7 @@ public async Task ForbidGnuPgKeyFingerprintAsy ClaimsPrincipal claimsPrincipal, GnuPgKeyFingerprintAuthorization authorization, ApplicationDbContext context, + IClock clock, CancellationToken cancellationToken ) { @@ -215,7 +220,7 @@ CancellationToken cancellationToken ) ); } - fingerprint.Forbid(); + fingerprint.Forbid(clock); await context.SaveChangesAsync(cancellationToken); return new ForbidGnuPgKeyFingerprintPayload(fingerprint); } diff --git a/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintQueries.cs b/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintQueries.cs index 813e0d205..77b0991bd 100644 --- a/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintQueries.cs +++ b/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintQueries.cs @@ -1,11 +1,10 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using HotChocolate.Authorization; +using GreenDonut.Data; using HotChocolate.Data; -using HotChocolate.Data.Sorting; +using HotChocolate.Resolvers; using HotChocolate.Types; -using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Extensions; using Microsoft.EntityFrameworkCore; @@ -16,23 +15,24 @@ namespace Metabase.GraphQl.GnuPgKeyFingerprints; public sealed class GnuPgKeyFingerprintQueries { [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] - [Authorize(Policy = AuthorizationPolicies.ManageGnuPgScopePolicy)] - public IQueryable GetGnuPgKeyFingerprints( - ApplicationDbContext context, - ISortingContext sorting + public ValueTask> GetGnuPgKeyFingerprintsAsync( + IResolverContext resolverContext, + ApplicationDbContext databaseContext, + CancellationToken cancellationToken ) { - sorting.StabilizeOrder(); - return context.GnuPgKeyFingerprints.AsNoTracking(); + return databaseContext.GnuPgKeyFingerprints + .AsNoTracking() + .With(resolverContext.GetQueryContext(), Sorting.DefaultEntityOrder) + .ToPageAsync(resolverContext.GetPagingArguments(), cancellationToken) + .ToConnectionAsync(); } - [Authorize(Policy = AuthorizationPolicies.ManageGnuPgScopePolicy)] public Task GetGnuPgKeyFingerprintAsync( string fingerprint, - GnuPgKeyFingerprintByFingerprintDataLoader byFingerprint, + IGnuPgKeyFingerprintByFingerprintDataLoader byFingerprint, CancellationToken cancellationToken ) { diff --git a/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintSortType.cs b/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintSortType.cs index aca5a3d33..34cd54898 100644 --- a/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintSortType.cs +++ b/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintSortType.cs @@ -5,18 +5,16 @@ namespace Metabase.GraphQl.GnuPgKeyFingerprints; public class GnuPgKeyFingerprintSortType - : EntitySortType + : AuditableEntitySortType { protected override void Configure( ISortInputTypeDescriptor descriptor ) { base.Configure(descriptor); - descriptor.Field(x => x.Fingerprint); - descriptor.Field(x => x.CreatedAt); - descriptor.Field(x => x.AllowedAt); - descriptor.Field(x => x.ForbiddenAt); - descriptor.Field(x => x.User); - descriptor.Field(x => x.Institution); + descriptor.Name(nameof(GnuPgKeyFingerprintSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); + descriptor.Field(_ => _.Fingerprint); + descriptor.Field(_ => _.AllowedAt); + descriptor.Field(_ => _.ForbiddenAt); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintType.cs b/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintType.cs index c1c5d5196..9e01660eb 100644 --- a/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintType.cs +++ b/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintType.cs @@ -11,7 +11,7 @@ namespace Metabase.GraphQl.GnuPgKeyFingerprints; public sealed class GnuPgKeyFingerprintType - : EntityType + : EntityType { protected override void Configure( IObjectTypeDescriptor descriptor @@ -38,12 +38,14 @@ IObjectTypeDescriptor descriptor ); descriptor .Field("isAuthorizedToAllowNode") + .Cost(1) .ResolveWith(x => GnuPgKeyFingerprintResolvers.IsAuthorizedToAllowNodeAsync(default!, default!, default!, default!) ) .UseUserManager(); descriptor .Field("isAuthorizedToForbidNode") + .Cost(1) .ResolveWith(x => GnuPgKeyFingerprintResolvers.IsAuthorizedToForbidNodeAsync(default!, default!, default!, default!) ) diff --git a/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintUserEdge.cs b/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintUserEdge.cs index 93a422165..df2641ab0 100644 --- a/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintUserEdge.cs +++ b/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintUserEdge.cs @@ -6,6 +6,6 @@ namespace Metabase.GraphQl.GnuPgKeyFingerprints; public sealed class GnuPgKeyFingerprintUserEdge( GnuPgKeyFingerprint association ) - : Edge(association.UserId) + : Edge(association.UserId) { } \ No newline at end of file diff --git a/backend/src/GraphQl/GraphQlConstants.cs b/backend/src/GraphQl/GraphQlConstants.cs index c6a4d5b80..77b41e2cf 100644 --- a/backend/src/GraphQl/GraphQlConstants.cs +++ b/backend/src/GraphQl/GraphQlConstants.cs @@ -2,6 +2,7 @@ namespace Metabase.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"; diff --git a/backend/src/GraphQl/GraphQlThrowHelper.cs b/backend/src/GraphQl/GraphQlThrowHelper.cs new file mode 100644 index 000000000..d41c52992 --- /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 Metabase.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 000000000..49548f397 --- /dev/null +++ b/backend/src/GraphQl/GraphQlTypeResources.cs @@ -0,0 +1,8 @@ +namespace Metabase.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/HygrothermalData.cs b/backend/src/GraphQl/HygrothermalDataX/HygrothermalData.cs new file mode 100644 index 000000000..e839d58c7 --- /dev/null +++ b/backend/src/GraphQl/HygrothermalDataX/HygrothermalData.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate.Resolvers; +using HotChocolate.Types.Relay; +using Metabase.Data; +using Metabase.GraphQl.DataX; +using Metabase.GraphQl.Requests; +using NodaTime; + +namespace Metabase.GraphQl.HygrothermalDataX; + +[Node(IdField = nameof(Id))] +public sealed record HygrothermalData( + string Id, + Guid Uuid, + OffsetDateTime Timestamp, + string Locale, + Guid DatabaseId, + Guid ComponentId, + string? Name, + string? Description, + IReadOnlyList Warnings, + Guid CreatorId, + OffsetDateTime CreatedAt, + AppliedMethod AppliedMethod, + IReadOnlyList Resources, + GetHttpsResourceTree ResourceTree, + IReadOnlyList Approvals +// ResponseApproval Approval +) +: DataX.Data( + Id, + Uuid, + Timestamp, + Locale, + DatabaseId, + ComponentId, + Name, + Description, + Warnings, + CreatorId, + CreatedAt, + AppliedMethod, + Resources, + ResourceTree, + Approvals +) +{ + public override DataKind Kind { get => DataKind.HYGROTHERMAL_DATA; } + + [NodeResolver] + public static Task GetAsync( + string id, + DataQueries dataQueries, + ApplicationDbContext databaseContext, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + return DataX.Data.FetchNodeAsync( + id, + (database, uuid, locale) => dataQueries.GetHygrothermalDataAsync( + database, + uuid, + locale, + resolverContext, + cancellationToken + ), + databaseContext, + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/HygrothermalDataConnection.cs b/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataConnection.cs similarity index 51% rename from backend/src/GraphQl/DataX/HygrothermalDataConnection.cs rename to backend/src/GraphQl/HygrothermalDataX/HygrothermalDataConnection.cs index 8ebb2a439..81ad0d4da 100644 --- a/backend/src/GraphQl/DataX/HygrothermalDataConnection.cs +++ b/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataConnection.cs @@ -1,13 +1,14 @@ using System.Collections.Generic; using HotChocolate.Types.Pagination; +using Metabase.GraphQl.DataX; -namespace Metabase.GraphQl.DataX; +namespace Metabase.GraphQl.HygrothermalDataX; public sealed record HygrothermalDataConnection( - IReadOnlyList Edges, - uint TotalCount, + IReadOnlyList? Edges, + int TotalCount, ConnectionPageInfo PageInfo -) : DataConnectionBase( +) : DataConnection( Edges, TotalCount, PageInfo diff --git a/backend/src/GraphQl/DataX/HygrothermalDataEdge.cs b/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataEdge.cs similarity index 50% rename from backend/src/GraphQl/DataX/HygrothermalDataEdge.cs rename to backend/src/GraphQl/HygrothermalDataX/HygrothermalDataEdge.cs index e84d43ae7..50ddea579 100644 --- a/backend/src/GraphQl/DataX/HygrothermalDataEdge.cs +++ b/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataEdge.cs @@ -1,9 +1,11 @@ -namespace Metabase.GraphQl.DataX; +using Metabase.GraphQl.DataX; + +namespace Metabase.GraphQl.HygrothermalDataX; public sealed record HygrothermalDataEdge( string Cursor, HygrothermalData Node -) : DataEdgeBase( +) : DataEdge( Cursor, Node ); \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/HygrothermalDataPropositionInput.cs b/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataPropositionInput.cs similarity index 63% rename from backend/src/GraphQl/DataX/HygrothermalDataPropositionInput.cs rename to backend/src/GraphQl/HygrothermalDataX/HygrothermalDataPropositionInput.cs index 9acad7f8b..9d85ab6cd 100644 --- a/backend/src/GraphQl/DataX/HygrothermalDataPropositionInput.cs +++ b/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataPropositionInput.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; +using Metabase.GraphQl.DataX; -namespace Metabase.GraphQl.DataX; +namespace Metabase.GraphQl.HygrothermalDataX; public sealed record HygrothermalDataPropositionInput( UuidPropositionInput? ComponentId, @@ -8,4 +9,8 @@ public sealed record HygrothermalDataPropositionInput( HygrothermalDataPropositionInput? Not, IReadOnlyList? Or, GetHttpsResourcesPropositionInput? Resources -); \ No newline at end of file +) +{ + public static readonly HygrothermalDataPropositionInput Empty = + new(null, null, null, null, null); +}; \ No newline at end of file diff --git a/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataQueries.cs b/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataQueries.cs new file mode 100644 index 000000000..b5ac4b9f2 --- /dev/null +++ b/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataQueries.cs @@ -0,0 +1,105 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.CostAnalysis.Types; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using Metabase.Data; +using Metabase.GraphQl.Scalars; +using Metabase.GraphQl.Requests; +using Microsoft.EntityFrameworkCore; + +namespace Metabase.GraphQl.HygrothermalDataX; + +[ExtendObjectType(nameof(Query))] +public sealed class HygrothermalDataQueries +{ + public async Task GetHygrothermalDataAsync( + Guid databaseId, + Guid id, + [GraphQLType] string? locale, + DataQueries dataQueries, + ApplicationDbContext databaseContext, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + var database = await databaseContext.Databases.AsNoTracking() + .Where(_ => _.Id == databaseId) + .SingleOrDefaultAsync(cancellationToken); + if (database is null) + { + return null; + } + return await dataQueries.GetHygrothermalDataAsync( + database, + id, + locale, + resolverContext, + cancellationToken + ); + } + + [ListSize( + AssumedSize = (int)GraphQlConstants.MaximumPageSize - 1, + SlicingArguments = ["first", "last"], + SlicingArgumentDefaultValue = (int)GraphQlConstants.MaximumPageSize - 1, + SizedFields = ["edges", "nodes"], + RequireOneSlicingArgument = false + )] + public Task GetAllHygrothermalDataAsync( + HygrothermalDataPropositionInput? where, + [GraphQLType] string? locale, + int? first, + string? after, + int? last, + string? before, + DataQueries dataQueries, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + return dataQueries.GetAllDataAsync( + first, + after, + last, + before, + (edges, totalCount, pageInfo) => new HygrothermalDataConnection(edges, totalCount, pageInfo), + (node, cursor) => new HygrothermalDataEdge(cursor, node), + (database, first, after, last, before) => dataQueries.GetAllHygrothermalDataAsync( + database, + where, + locale, + first, + after, + last, + before, + resolverContext, + cancellationToken + ), + cancellationToken + ); + } + + public Task HasHygrothermalDataAsync( + HygrothermalDataPropositionInput? where, + [GraphQLType] string? locale, + DataQueries dataQueries, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + return dataQueries.HasDataAsync( + (database) => dataQueries.HasHygrothermalDataAsync( + database, + where, + locale, + resolverContext, + cancellationToken + ), + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/InstitutionMethodDevelopers/AddInstitutionMethodDeveloperPayload.cs b/backend/src/GraphQl/InstitutionMethodDevelopers/AddInstitutionMethodDeveloperPayload.cs index 06c47b0ca..907118b55 100644 --- a/backend/src/GraphQl/InstitutionMethodDevelopers/AddInstitutionMethodDeveloperPayload.cs +++ b/backend/src/GraphQl/InstitutionMethodDevelopers/AddInstitutionMethodDeveloperPayload.cs @@ -11,7 +11,10 @@ public AddInstitutionMethodDeveloperPayload( InstitutionMethodDeveloper institutionMethodDeveloper ) { - DevelopedMethodEdge = new InstitutionDevelopedMethodEdge(institutionMethodDeveloper); + DevelopedMethodEdge = new InstitutionDevelopedMethodEdge( + institutionMethodDeveloper, + PaginationHelpers.ConstructCursor(institutionMethodDeveloper.MethodId, institutionMethodDeveloper.InstitutionId) + ); MethodDeveloperEdge = new InstitutionMethodDeveloperEdge(institutionMethodDeveloper); } diff --git a/backend/src/GraphQl/InstitutionMethodDevelopers/ConfirmInstitutionMethodDeveloperPayload.cs b/backend/src/GraphQl/InstitutionMethodDevelopers/ConfirmInstitutionMethodDeveloperPayload.cs index 6709931d1..96a558212 100644 --- a/backend/src/GraphQl/InstitutionMethodDevelopers/ConfirmInstitutionMethodDeveloperPayload.cs +++ b/backend/src/GraphQl/InstitutionMethodDevelopers/ConfirmInstitutionMethodDeveloperPayload.cs @@ -11,7 +11,10 @@ public ConfirmInstitutionMethodDeveloperPayload( InstitutionMethodDeveloper institutionMethodDeveloper ) { - DevelopedMethodEdge = new InstitutionDevelopedMethodEdge(institutionMethodDeveloper); + DevelopedMethodEdge = new InstitutionDevelopedMethodEdge( + institutionMethodDeveloper, + PaginationHelpers.ConstructCursor(institutionMethodDeveloper.MethodId, institutionMethodDeveloper.InstitutionId) + ); MethodDeveloperEdge = new InstitutionMethodDeveloperEdge(institutionMethodDeveloper); } diff --git a/backend/src/GraphQl/InstitutionMethodDevelopers/InstitutionMethodDeveloperFilterType.cs b/backend/src/GraphQl/InstitutionMethodDevelopers/InstitutionMethodDeveloperFilterType.cs index 106b009be..90d8181cf 100644 --- a/backend/src/GraphQl/InstitutionMethodDevelopers/InstitutionMethodDeveloperFilterType.cs +++ b/backend/src/GraphQl/InstitutionMethodDevelopers/InstitutionMethodDeveloperFilterType.cs @@ -1,17 +1,21 @@ using HotChocolate.Data.Filters; using Metabase.Data; +using Metabase.GraphQl.Associations; namespace Metabase.GraphQl.InstitutionMethodDevelopers; public abstract class InstitutionMethodDeveloperFilterType - : FilterInputType + : AuditableAssociationFilterType { protected override void Configure( IFilterInputTypeDescriptor descriptor ) { - descriptor.BindFieldsExplicitly(); - descriptor.Field(x => x.Method); - descriptor.Field(x => x.Institution); + base.Configure(descriptor); + // TODO Remove CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(_ => _.CreatedAt); + descriptor.Field(_ => _.UpdatedAt); + descriptor.Field(_ => _.Method); + descriptor.Field(_ => _.Institution); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/InstitutionMethodDevelopers/InstitutionMethodDeveloperSortType.cs b/backend/src/GraphQl/InstitutionMethodDevelopers/InstitutionMethodDeveloperSortType.cs index eb6e948ec..a54d4c4be 100644 --- a/backend/src/GraphQl/InstitutionMethodDevelopers/InstitutionMethodDeveloperSortType.cs +++ b/backend/src/GraphQl/InstitutionMethodDevelopers/InstitutionMethodDeveloperSortType.cs @@ -1,17 +1,16 @@ using HotChocolate.Data.Sorting; using Metabase.Data; +using Metabase.GraphQl.Associations; namespace Metabase.GraphQl.InstitutionMethodDevelopers; -public sealed class InstitutionMethodDeveloperSortType - : SortInputType +public abstract class InstitutionMethodDeveloperSortType + : AuditableAssociationSortType { protected override void Configure( ISortInputTypeDescriptor descriptor ) { - descriptor.BindFieldsExplicitly(); - descriptor.Field(x => x.Method); - descriptor.Field(x => x.Institution); + base.Configure(descriptor); } } \ No newline at end of file diff --git a/backend/src/GraphQl/InstitutionMethodDevelopers/RemoveInstitutionMethodDeveloperPayload.cs b/backend/src/GraphQl/InstitutionMethodDevelopers/RemoveInstitutionMethodDeveloperPayload.cs index 7c90dbf2e..d7fe58621 100644 --- a/backend/src/GraphQl/InstitutionMethodDevelopers/RemoveInstitutionMethodDeveloperPayload.cs +++ b/backend/src/GraphQl/InstitutionMethodDevelopers/RemoveInstitutionMethodDeveloperPayload.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Metabase.Data; using Metabase.GraphQl.Institutions; using Metabase.GraphQl.Methods; @@ -7,12 +9,13 @@ namespace Metabase.GraphQl.InstitutionMethodDevelopers; public sealed class RemoveInstitutionMethodDeveloperPayload { + private readonly InstitutionMethodDeveloper? _association; + public RemoveInstitutionMethodDeveloperPayload( InstitutionMethodDeveloper institutionMethodDeveloper ) { - DevelopedMethodEdge = new InstitutionDevelopedMethodEdge(institutionMethodDeveloper); - MethodDeveloperEdge = new InstitutionMethodDeveloperEdge(institutionMethodDeveloper); + _association = institutionMethodDeveloper; } public RemoveInstitutionMethodDeveloperPayload( @@ -29,7 +32,29 @@ RemoveInstitutionMethodDeveloperError error { } - public InstitutionDevelopedMethodEdge? DevelopedMethodEdge { get; } - public InstitutionMethodDeveloperEdge? MethodDeveloperEdge { get; } public IReadOnlyCollection? Errors { get; } + + public async Task GetMethodAsync( + IMethodByIdDataLoader byId, + CancellationToken cancellationToken + ) + { + if (_association is null) + { + return null; + } + return await byId.LoadAsync(_association.MethodId, cancellationToken); + } + + public async Task GetInstitutionAsync( + IInstitutionByIdDataLoader byId, + CancellationToken cancellationToken + ) + { + if (_association is null) + { + return null; + } + return await byId.LoadAsync(_association.InstitutionId, cancellationToken); + } } \ No newline at end of file diff --git a/backend/src/GraphQl/InstitutionRepresentatives/InstitutionRepresentativeFilterType.cs b/backend/src/GraphQl/InstitutionRepresentatives/InstitutionRepresentativeFilterType.cs index 7d46ecb5f..5a39e9c7e 100644 --- a/backend/src/GraphQl/InstitutionRepresentatives/InstitutionRepresentativeFilterType.cs +++ b/backend/src/GraphQl/InstitutionRepresentatives/InstitutionRepresentativeFilterType.cs @@ -1,18 +1,22 @@ using HotChocolate.Data.Filters; using Metabase.Data; +using Metabase.GraphQl.Associations; namespace Metabase.GraphQl.InstitutionRepresentatives; public abstract class InstitutionRepresentativeFilterType - : FilterInputType + : AuditableAssociationFilterType { protected override void Configure( IFilterInputTypeDescriptor descriptor ) { - descriptor.BindFieldsExplicitly(); - descriptor.Field(x => x.Institution); - descriptor.Field(x => x.User); - descriptor.Field(x => x.Role); + base.Configure(descriptor); + // TODO Remove CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(_ => _.CreatedAt); + descriptor.Field(_ => _.UpdatedAt); + descriptor.Field(_ => _.Institution); + descriptor.Field(_ => _.User); + descriptor.Field(_ => _.Role); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/InstitutionRepresentatives/InstitutionRepresentativeMutations.cs b/backend/src/GraphQl/InstitutionRepresentatives/InstitutionRepresentativeMutations.cs index b3d0bab5c..ef0058c66 100644 --- a/backend/src/GraphQl/InstitutionRepresentatives/InstitutionRepresentativeMutations.cs +++ b/backend/src/GraphQl/InstitutionRepresentatives/InstitutionRepresentativeMutations.cs @@ -11,10 +11,9 @@ using Metabase.Enumerations; using Metabase.Extensions; using Metabase.GraphQl.Users; -using Microsoft.AspNetCore.Antiforgery; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Metabase.GraphQl.InstitutionRepresentatives; @@ -117,6 +116,7 @@ public async Task RemoveInstitutionRepre ClaimsPrincipal claimsPrincipal, InstitutionRepresentativeAuthorization authorization, ApplicationDbContext context, + IClock clock, CancellationToken cancellationToken ) { @@ -213,7 +213,7 @@ await context.InstitutionRepresentatives.AsQueryable() && f.UserId == input.UserId )) { - fingerprint.Forbid(); + fingerprint.Forbid(clock); } context.InstitutionRepresentatives.Remove(institutionRepresentative); await context.SaveChangesAsync(cancellationToken); diff --git a/backend/src/GraphQl/InstitutionRepresentatives/InstitutionRepresentativeSortType.cs b/backend/src/GraphQl/InstitutionRepresentatives/InstitutionRepresentativeSortType.cs index 5bd005b74..685e19c81 100644 --- a/backend/src/GraphQl/InstitutionRepresentatives/InstitutionRepresentativeSortType.cs +++ b/backend/src/GraphQl/InstitutionRepresentatives/InstitutionRepresentativeSortType.cs @@ -1,18 +1,17 @@ using HotChocolate.Data.Sorting; using Metabase.Data; +using Metabase.GraphQl.Associations; namespace Metabase.GraphQl.InstitutionRepresentatives; -public sealed class InstitutionRepresentativeSortType - : SortInputType +public abstract class InstitutionRepresentativeSortType + : AuditableAssociationSortType { protected override void Configure( ISortInputTypeDescriptor descriptor ) { - descriptor.BindFieldsExplicitly(); - descriptor.Field(x => x.Institution); - descriptor.Field(x => x.User); - descriptor.Field(x => x.Role); + base.Configure(descriptor); + descriptor.Field(_ => _.Role); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/InstitutionRepresentatives/RemoveInstitutionRepresentativePayload.cs b/backend/src/GraphQl/InstitutionRepresentatives/RemoveInstitutionRepresentativePayload.cs index 1c20b1473..c1b71a6f3 100644 --- a/backend/src/GraphQl/InstitutionRepresentatives/RemoveInstitutionRepresentativePayload.cs +++ b/backend/src/GraphQl/InstitutionRepresentatives/RemoveInstitutionRepresentativePayload.cs @@ -35,7 +35,7 @@ RemoveInstitutionRepresentativeError error public IReadOnlyCollection? Errors { get; } public async Task GetInstitution( - InstitutionByIdDataLoader byId, + IInstitutionByIdDataLoader byId, CancellationToken cancellationToken ) { diff --git a/backend/src/GraphQl/Institutions/GnuPgKeyFingerprintsByInstitutionIdDataLoader.cs b/backend/src/GraphQl/Institutions/GnuPgKeyFingerprintsByInstitutionIdDataLoader.cs deleted file mode 100644 index 027b8cdf3..000000000 --- a/backend/src/GraphQl/Institutions/GnuPgKeyFingerprintsByInstitutionIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Institutions; - -public sealed class GnuPgKeyFingerprintsByInstitutionIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.GnuPgKeyFingerprints.AsNoTracking().Where(x => - ids.Contains(x.InstitutionId) - ).With(queryContext), - x => x.InstitutionId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionByIdDataLoader.cs b/backend/src/GraphQl/Institutions/InstitutionByIdDataLoader.cs deleted file mode 100644 index 365c75d6f..000000000 --- a/backend/src/GraphQl/Institutions/InstitutionByIdDataLoader.cs +++ /dev/null @@ -1,20 +0,0 @@ -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Institutions; - -public sealed class InstitutionByIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : EntityByIdDataLoader( - batchScheduler, - options, - dbContextFactory, - dbContext => dbContext.Institutions - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionDataLoaders.cs b/backend/src/GraphQl/Institutions/InstitutionDataLoaders.cs new file mode 100644 index 000000000..63f258add --- /dev/null +++ b/backend/src/GraphQl/Institutions/InstitutionDataLoaders.cs @@ -0,0 +1,289 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut; +using GreenDonut.Data; +using Metabase.Data; +using Metabase.Data.OpenIdConnect; +using Microsoft.EntityFrameworkCore; + +namespace Metabase.GraphQl.Institutions; + +public sealed class InstitutionDataLoaders +: DataLoaders +{ + [DataLoader] + public static ValueTask> GetInstitutionByIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetEntityByIdAsync( + ids, + databaseContext => databaseContext.Institutions, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask>> GetInstitutionManufacturedComponentsByInstitutionIdAsync( + IReadOnlyList ids, + PagingArguments pagingArguments, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetAssociationsByOneIdAsync( + ids, + (databaseContext) => databaseContext.ComponentManufacturers.Where(_ => !_.Pending), + _ => _.InstitutionId, + _ => _.ComponentId, + pagingArguments, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask>> GetPendingInstitutionManufacturedComponentsByInstitutionIdAsync( + IReadOnlyList ids, + PagingArguments pagingArguments, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetAssociationsByOneIdAsync( + ids, + (databaseContext) => databaseContext.ComponentManufacturers.Where(_ => _.Pending), + _ => _.InstitutionId, + _ => _.ComponentId, + pagingArguments, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask>> GetInstitutionDevelopedMethodsByInstitutionIdAsync( + IReadOnlyList ids, + PagingArguments pagingArguments, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetAssociationsByOneIdAsync( + ids, + (databaseContext) => databaseContext.InstitutionMethodDevelopers.Where(_ => !_.Pending), + _ => _.InstitutionId, + _ => _.MethodId, + pagingArguments, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask>> GetPendingInstitutionDevelopedMethodsByInstitutionIdAsync( + IReadOnlyList ids, + PagingArguments pagingArguments, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetAssociationsByOneIdAsync( + ids, + (databaseContext) => databaseContext.InstitutionMethodDevelopers.Where(_ => _.Pending), + _ => _.InstitutionId, + _ => _.MethodId, + pagingArguments, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask> GetInstitutionOperatedDatabasesByInstitutionIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetManyByOneIdAsync( + ids, + (databaseContext) => databaseContext.Databases, + _ => _.OperatorId, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask> GetInstitutionOwnedOpenIdConnectApplicationsByInstitutionIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetManyByOneIdAsync( + ids, + (databaseContext) => databaseContext.OpenIdConnectApplications, + _ => _.OwnerId, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask> GetInstitutionRepresentativesByInstitutionIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetAssociationsByOneIdAsync( + ids, + (databaseContext) => databaseContext.InstitutionRepresentatives.Where(_ => !_.Pending), + _ => _.InstitutionId, + _ => _.UserId, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask> GetPendingInstitutionRepresentativesByInstitutionIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetAssociationsByOneIdAsync( + ids, + (databaseContext) => databaseContext.InstitutionRepresentatives.Where(_ => _.Pending), + _ => _.InstitutionId, + _ => _.UserId, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask> GetGnuPgKeyFingerprintsByInstitutionIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetManyByOneIdAsync( + ids, + (databaseContext) => databaseContext.GnuPgKeyFingerprints, + _ => _.InstitutionId, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask>> GetInstitutionManagedComponentsByInstitutionIdAsync( + IReadOnlyList ids, + PagingArguments pagingArguments, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetManyByOneIdAsync( + ids, + (databaseContext) => databaseContext.Components, + _ => _.ManagerId, + pagingArguments, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask>> GetInstitutionManagedDataFormatsByInstitutionIdAsync( + IReadOnlyList ids, + PagingArguments pagingArguments, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetManyByOneIdAsync( + ids, + (databaseContext) => databaseContext.DataFormats, + _ => _.ManagerId, + pagingArguments, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask>> GetInstitutionManagedInstitutionsByInstitutionIdAsync( + IReadOnlyList ids, + PagingArguments pagingArguments, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetManyByOneIdAsync( + ids, + (databaseContext) => databaseContext.Institutions, + _ => _.ManagerId ?? Guid.Empty, + pagingArguments, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask>> GetInstitutionManagedMethodsByInstitutionIdAsync( + IReadOnlyList ids, + PagingArguments pagingArguments, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetManyByOneIdAsync( + ids, + (databaseContext) => databaseContext.Methods, + _ => _.ManagerId, + pagingArguments, + queryContext, + databaseContextFactory, + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionDevelopedMethodConnection.cs b/backend/src/GraphQl/Institutions/InstitutionDevelopedMethodConnection.cs index b0e971916..66b46111a 100644 --- a/backend/src/GraphQl/Institutions/InstitutionDevelopedMethodConnection.cs +++ b/backend/src/GraphQl/Institutions/InstitutionDevelopedMethodConnection.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using GreenDonut.Data; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Users; @@ -10,11 +11,13 @@ namespace Metabase.GraphQl.Institutions; public sealed class InstitutionDevelopedMethodConnection( Institution institution, + PagingArguments pagingArguments, QueryContext queryContext ) - : Connection( + : PaginatedConnection( institution, - x => new InstitutionDevelopedMethodEdge(x), + (association, cursor) => new InstitutionDevelopedMethodEdge(association, cursor), + pagingArguments, queryContext ) { @@ -22,17 +25,20 @@ QueryContext queryContext public sealed class PendingInstitutionDevelopedMethodConnection( Institution institution, + PagingArguments pagingArguments, QueryContext queryContext ) - : AuthorizedConnection( + : AuthorizedPaginatedConnection( institution, - x => new InstitutionDevelopedMethodEdge(x), - (claimsPrincipal, institution, authorization, cancellationToken) => + (association, cursor) => new InstitutionDevelopedMethodEdge(association, cursor), + (claimsPrincipal, authorization, cancellationToken) => authorization.IsAuthorizedToConfirm(claimsPrincipal, institution.Id, cancellationToken), + pagingArguments, queryContext ) { [UseUserManager] + [Cost(1)] public Task IsAuthorizedToConfirmEdgesAsync( ClaimsPrincipal claimsPrincipal, InstitutionMethodDeveloperAuthorization authorization, diff --git a/backend/src/GraphQl/Institutions/InstitutionDevelopedMethodEdge.cs b/backend/src/GraphQl/Institutions/InstitutionDevelopedMethodEdge.cs index 8f91851c3..b70c48fae 100644 --- a/backend/src/GraphQl/Institutions/InstitutionDevelopedMethodEdge.cs +++ b/backend/src/GraphQl/Institutions/InstitutionDevelopedMethodEdge.cs @@ -1,11 +1,49 @@ +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate.CostAnalysis.Types; +using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Methods; +using Metabase.GraphQl.Users; namespace Metabase.GraphQl.Institutions; public sealed class InstitutionDevelopedMethodEdge( - InstitutionMethodDeveloper association - ) - : Edge(association.MethodId) + InstitutionMethodDeveloper association, + string cursor +) +: PaginatedEdge( + association.MethodId, cursor +) { + [UseUserManager] + [Cost(1)] + public Task IsAuthorizedToConfirmEdgeAsync( + ClaimsPrincipal claimsPrincipal, + InstitutionMethodDeveloperAuthorization authorization, + CancellationToken cancellationToken + ) + { + return authorization.IsAuthorizedToConfirm( + claimsPrincipal, + association.InstitutionId, + cancellationToken + ); + } + + [UseUserManager] + [Cost(1)] + public Task IsAuthorizedToRemoveEdgeAsync( + ClaimsPrincipal claimsPrincipal, + InstitutionMethodDeveloperAuthorization authorization, + CancellationToken cancellationToken + ) + { + return authorization.IsAuthorizedToRemove( + claimsPrincipal, + association.MethodId, + cancellationToken + ); + } } \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionDevelopedMethodFilterType.cs b/backend/src/GraphQl/Institutions/InstitutionDevelopedMethodFilterType.cs index 7a8f1b429..01ce8cd0e 100644 --- a/backend/src/GraphQl/Institutions/InstitutionDevelopedMethodFilterType.cs +++ b/backend/src/GraphQl/Institutions/InstitutionDevelopedMethodFilterType.cs @@ -13,6 +13,6 @@ IFilterInputTypeDescriptor descriptor { base.Configure(descriptor); descriptor.Name(nameof(InstitutionDevelopedMethodFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); - descriptor.Field(x => x.Institution).Ignore(); + descriptor.Field(_ => _.Institution).Ignore(); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Institutions/InstitutionDevelopedMethodSortType.cs b/backend/src/GraphQl/Institutions/InstitutionDevelopedMethodSortType.cs new file mode 100644 index 000000000..84ab85b09 --- /dev/null +++ b/backend/src/GraphQl/Institutions/InstitutionDevelopedMethodSortType.cs @@ -0,0 +1,17 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data; +using Metabase.GraphQl.InstitutionMethodDevelopers; + +namespace Metabase.GraphQl.Institutions; + +public sealed class InstitutionDevelopedMethodSortType + : InstitutionMethodDeveloperSortType +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.Name(nameof(InstitutionDevelopedMethodSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionDevelopedMethodsByInstitutionIdDataLoader.cs b/backend/src/GraphQl/Institutions/InstitutionDevelopedMethodsByInstitutionIdDataLoader.cs deleted file mode 100644 index a7e356500..000000000 --- a/backend/src/GraphQl/Institutions/InstitutionDevelopedMethodsByInstitutionIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Institutions; - -public sealed class InstitutionDevelopedMethodsByInstitutionIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.InstitutionMethodDevelopers.AsNoTracking().Where(x => - !x.Pending && ids.Contains(x.InstitutionId) - ).With(queryContext), - x => x.InstitutionId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionFilterType.cs b/backend/src/GraphQl/Institutions/InstitutionFilterType.cs index 47a44b3ef..4342223e4 100644 --- a/backend/src/GraphQl/Institutions/InstitutionFilterType.cs +++ b/backend/src/GraphQl/Institutions/InstitutionFilterType.cs @@ -5,13 +5,17 @@ namespace Metabase.GraphQl.Institutions; public class InstitutionFilterType - : EntityFilterType + : AuditableEntityFilterType { protected override void Configure( IFilterInputTypeDescriptor descriptor ) { base.Configure(descriptor); + // TODO Remove Id, CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(_ => _.Id); + descriptor.Field(_ => _.CreatedAt); + descriptor.Field(_ => _.UpdatedAt); descriptor.Field(_ => _.Name); descriptor.Field(_ => _.Abbreviation); descriptor.Field(_ => _.Description); @@ -31,4 +35,4 @@ IFilterInputTypeDescriptor descriptor descriptor.Field(_ => _.RepresentativeEdges); descriptor.Field(_ => _.GnuPgKeyFingerprints); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Institutions/InstitutionGnuPgKeyFingerprintConnection.cs b/backend/src/GraphQl/Institutions/InstitutionGnuPgKeyFingerprintConnection.cs index 45a3d940d..9cadecff1 100644 --- a/backend/src/GraphQl/Institutions/InstitutionGnuPgKeyFingerprintConnection.cs +++ b/backend/src/GraphQl/Institutions/InstitutionGnuPgKeyFingerprintConnection.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using GreenDonut.Data; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Users; @@ -14,8 +15,8 @@ QueryContext queryContext ) : Connection< Institution, GnuPgKeyFingerprint, - GnuPgKeyFingerprintsByInstitutionIdDataLoader, - InstitutionGnuPgKeyFingerprintEdge + InstitutionGnuPgKeyFingerprintEdge, + GnuPgKeyFingerprintsByInstitutionIdDataLoader > ( institution, @@ -24,6 +25,7 @@ QueryContext queryContext ) { [UseUserManager] + [Cost(1)] public Task IsAuthorizedToAddEdgeAsync( ClaimsPrincipal claimsPrincipal, GnuPgKeyFingerprintAuthorization authorization, diff --git a/backend/src/GraphQl/Institutions/InstitutionGnuPgKeyFingerprintFilterType.cs b/backend/src/GraphQl/Institutions/InstitutionGnuPgKeyFingerprintFilterType.cs index e189930aa..c60c8e4c1 100644 --- a/backend/src/GraphQl/Institutions/InstitutionGnuPgKeyFingerprintFilterType.cs +++ b/backend/src/GraphQl/Institutions/InstitutionGnuPgKeyFingerprintFilterType.cs @@ -1,5 +1,4 @@ using HotChocolate.Data.Filters; -using Metabase.Configuration; using Metabase.Data; using Metabase.GraphQl.GnuPgKeyFingerprints; @@ -14,6 +13,6 @@ IFilterInputTypeDescriptor descriptor { base.Configure(descriptor); descriptor.Name(nameof(InstitutionGnuPgKeyFingerprintFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); - descriptor.Field(x => x.Institution).Ignore(); + descriptor.Field(_ => _.Institution).Ignore(); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Institutions/InstitutionGnuPgKeyFingerprintSortType.cs b/backend/src/GraphQl/Institutions/InstitutionGnuPgKeyFingerprintSortType.cs new file mode 100644 index 000000000..5f1f63073 --- /dev/null +++ b/backend/src/GraphQl/Institutions/InstitutionGnuPgKeyFingerprintSortType.cs @@ -0,0 +1,17 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data; +using Metabase.GraphQl.GnuPgKeyFingerprints; + +namespace Metabase.GraphQl.Institutions; + +public sealed class InstitutionGnuPgKeyFingerprintSortType + : GnuPgKeyFingerprintSortType +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.Name(nameof(InstitutionGnuPgKeyFingerprintSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionManagedComponentConnection.cs b/backend/src/GraphQl/Institutions/InstitutionManagedComponentConnection.cs index 2849d80c7..f28631cc4 100644 --- a/backend/src/GraphQl/Institutions/InstitutionManagedComponentConnection.cs +++ b/backend/src/GraphQl/Institutions/InstitutionManagedComponentConnection.cs @@ -2,25 +2,27 @@ using System.Threading; using System.Threading.Tasks; using GreenDonut.Data; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Users; -using Microsoft.AspNetCore.Identity; namespace Metabase.GraphQl.Institutions; public sealed class InstitutionManagedComponentConnection( Institution institution, + PagingArguments pagingArguments, QueryContext queryContext ) - : Connection( + : PaginatedConnection( institution, - x => new InstitutionManagedComponentEdge(x), + (node, cursor) => new InstitutionManagedComponentEdge(node, cursor), + pagingArguments, queryContext ) { [UseUserManager] + [Cost(1)] public Task IsAuthorizedToAddEdgeAsync( ClaimsPrincipal claimsPrincipal, ComponentAuthorization authorization, diff --git a/backend/src/GraphQl/Institutions/InstitutionManagedComponentEdge.cs b/backend/src/GraphQl/Institutions/InstitutionManagedComponentEdge.cs index 875da81e1..bcc9a9fe5 100644 --- a/backend/src/GraphQl/Institutions/InstitutionManagedComponentEdge.cs +++ b/backend/src/GraphQl/Institutions/InstitutionManagedComponentEdge.cs @@ -2,9 +2,11 @@ namespace Metabase.GraphQl.Institutions; -public sealed class InstitutionManagedComponentEdge( - Component node - ) -{ - public Component Node { get; } = node; -} \ No newline at end of file +public sealed record InstitutionManagedComponentEdge( + Component Node, + string Cursor +) +: PaginatedEdge( + Node, + Cursor +); \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionManagedComponentFilterType.cs b/backend/src/GraphQl/Institutions/InstitutionManagedComponentFilterType.cs index 5e3c41b69..661e6a749 100644 --- a/backend/src/GraphQl/Institutions/InstitutionManagedComponentFilterType.cs +++ b/backend/src/GraphQl/Institutions/InstitutionManagedComponentFilterType.cs @@ -12,6 +12,6 @@ IFilterInputTypeDescriptor descriptor { base.Configure(descriptor); descriptor.Name(nameof(InstitutionManagedComponentFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); - descriptor.Field(x => x.Manager).Ignore(); + descriptor.Field(_ => _.Manager).Ignore(); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Institutions/InstitutionManagedComponentSortType.cs b/backend/src/GraphQl/Institutions/InstitutionManagedComponentSortType.cs new file mode 100644 index 000000000..f2e458d57 --- /dev/null +++ b/backend/src/GraphQl/Institutions/InstitutionManagedComponentSortType.cs @@ -0,0 +1,16 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data; + +namespace Metabase.GraphQl.Components; + +public sealed class InstitutionManagedComponentSortType + : ComponentSortType +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.Name(nameof(InstitutionManagedComponentSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionManagedComponentsByInstitutionIdDataLoader.cs b/backend/src/GraphQl/Institutions/InstitutionManagedComponentsByInstitutionIdDataLoader.cs deleted file mode 100644 index 7fd4fa6de..000000000 --- a/backend/src/GraphQl/Institutions/InstitutionManagedComponentsByInstitutionIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Institutions; - -public sealed class InstitutionManagedComponentsByInstitutionIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.Components.AsNoTracking().Where(x => - ids.Contains(x.ManagerId) - ).With(queryContext), - x => x.ManagerId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionManagedDataFormatConnection.cs b/backend/src/GraphQl/Institutions/InstitutionManagedDataFormatConnection.cs index 1a9fe223f..fe484aef4 100644 --- a/backend/src/GraphQl/Institutions/InstitutionManagedDataFormatConnection.cs +++ b/backend/src/GraphQl/Institutions/InstitutionManagedDataFormatConnection.cs @@ -2,25 +2,27 @@ using System.Threading; using System.Threading.Tasks; using GreenDonut.Data; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Users; -using Microsoft.AspNetCore.Identity; namespace Metabase.GraphQl.Institutions; public sealed class InstitutionManagedDataFormatConnection( Institution institution, + PagingArguments pagingArguments, QueryContext queryContext ) - : Connection( + : PaginatedConnection( institution, - x => new InstitutionManagedDataFormatEdge(x), + (node, cursor) => new InstitutionManagedDataFormatEdge(node, cursor), + pagingArguments, queryContext ) { [UseUserManager] + [Cost(1)] public Task IsAuthorizedToAddEdgeAsync( ClaimsPrincipal claimsPrincipal, DataFormatAuthorization authorization, diff --git a/backend/src/GraphQl/Institutions/InstitutionManagedDataFormatEdge.cs b/backend/src/GraphQl/Institutions/InstitutionManagedDataFormatEdge.cs index af05b948e..a707c07ee 100644 --- a/backend/src/GraphQl/Institutions/InstitutionManagedDataFormatEdge.cs +++ b/backend/src/GraphQl/Institutions/InstitutionManagedDataFormatEdge.cs @@ -2,9 +2,11 @@ namespace Metabase.GraphQl.Institutions; -public sealed class InstitutionManagedDataFormatEdge( - DataFormat node - ) -{ - public DataFormat Node { get; } = node; -} \ No newline at end of file +public sealed record InstitutionManagedDataFormatEdge( + DataFormat Node, + string Cursor +) +: PaginatedEdge( + Node, + Cursor +); \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionManagedDataFormatFilterType.cs b/backend/src/GraphQl/Institutions/InstitutionManagedDataFormatFilterType.cs index 07a5f842a..ec7d55056 100644 --- a/backend/src/GraphQl/Institutions/InstitutionManagedDataFormatFilterType.cs +++ b/backend/src/GraphQl/Institutions/InstitutionManagedDataFormatFilterType.cs @@ -12,6 +12,6 @@ IFilterInputTypeDescriptor descriptor { base.Configure(descriptor); descriptor.Name(nameof(InstitutionManagedDataFormatFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); - descriptor.Field(x => x.Manager).Ignore(); + descriptor.Field(_ => _.Manager).Ignore(); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Institutions/InstitutionManagedDataFormatSortType.cs b/backend/src/GraphQl/Institutions/InstitutionManagedDataFormatSortType.cs new file mode 100644 index 000000000..86fbbdf23 --- /dev/null +++ b/backend/src/GraphQl/Institutions/InstitutionManagedDataFormatSortType.cs @@ -0,0 +1,16 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data; + +namespace Metabase.GraphQl.DataFormats; + +public sealed class InstitutionManagedDataFormatSortType + : DataFormatSortType +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.Name(nameof(InstitutionManagedDataFormatSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionManagedDataFormatsByInstitutionIdDataLoader.cs b/backend/src/GraphQl/Institutions/InstitutionManagedDataFormatsByInstitutionIdDataLoader.cs deleted file mode 100644 index 0d0061f4d..000000000 --- a/backend/src/GraphQl/Institutions/InstitutionManagedDataFormatsByInstitutionIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Institutions; - -public sealed class InstitutionManagedDataFormatsByInstitutionIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.DataFormats.AsNoTracking().Where(x => - ids.Contains(x.ManagerId) - ).With(queryContext), - x => x.ManagerId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionManagedInstitutionConnection.cs b/backend/src/GraphQl/Institutions/InstitutionManagedInstitutionConnection.cs index ff42f6d06..f46eba87c 100644 --- a/backend/src/GraphQl/Institutions/InstitutionManagedInstitutionConnection.cs +++ b/backend/src/GraphQl/Institutions/InstitutionManagedInstitutionConnection.cs @@ -2,25 +2,27 @@ using System.Threading; using System.Threading.Tasks; using GreenDonut.Data; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Users; -using Microsoft.AspNetCore.Identity; namespace Metabase.GraphQl.Institutions; public sealed class InstitutionManagedInstitutionConnection( Institution institution, + PagingArguments pagingArguments, QueryContext queryContext - ) - : Connection( +) +: PaginatedConnection( institution, - x => new InstitutionManagedInstitutionEdge(x), + (node, cursor) => new InstitutionManagedInstitutionEdge(node, cursor), + pagingArguments, queryContext - ) +) { [UseUserManager] + [Cost(1)] public Task IsAuthorizedToAddEdgeAsync( ClaimsPrincipal claimsPrincipal, InstitutionAuthorization authorization, diff --git a/backend/src/GraphQl/Institutions/InstitutionManagedInstitutionEdge.cs b/backend/src/GraphQl/Institutions/InstitutionManagedInstitutionEdge.cs index 2ff6c4ba1..52a437fc2 100644 --- a/backend/src/GraphQl/Institutions/InstitutionManagedInstitutionEdge.cs +++ b/backend/src/GraphQl/Institutions/InstitutionManagedInstitutionEdge.cs @@ -2,9 +2,11 @@ namespace Metabase.GraphQl.Institutions; -public sealed class InstitutionManagedInstitutionEdge( - Institution node - ) -{ - public Institution Node { get; } = node; -} \ No newline at end of file +public sealed record InstitutionManagedInstitutionEdge( + Institution Node, + string Cursor +) +: PaginatedEdge( + Node, + Cursor +); \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionManagedInstitutionFilterType.cs b/backend/src/GraphQl/Institutions/InstitutionManagedInstitutionFilterType.cs index 0b0a9e492..35da52aa7 100644 --- a/backend/src/GraphQl/Institutions/InstitutionManagedInstitutionFilterType.cs +++ b/backend/src/GraphQl/Institutions/InstitutionManagedInstitutionFilterType.cs @@ -1,5 +1,4 @@ using HotChocolate.Data.Filters; -using Metabase.Configuration; using Metabase.Data; namespace Metabase.GraphQl.Institutions; @@ -13,6 +12,6 @@ IFilterInputTypeDescriptor descriptor { base.Configure(descriptor); descriptor.Name(nameof(InstitutionManagedInstitutionFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); - descriptor.Field(x => x.Manager).Ignore(); + descriptor.Field(_ => _.Manager).Ignore(); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Institutions/InstitutionManagedInstitutionSortType.cs b/backend/src/GraphQl/Institutions/InstitutionManagedInstitutionSortType.cs new file mode 100644 index 000000000..bd0cb6f78 --- /dev/null +++ b/backend/src/GraphQl/Institutions/InstitutionManagedInstitutionSortType.cs @@ -0,0 +1,16 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data; + +namespace Metabase.GraphQl.Institutions; + +public sealed class InstitutionManagedInstitutionSortType + : InstitutionSortType +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.Name(nameof(InstitutionManagedInstitutionSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionManagedInstitutionsByInstitutionIdDataLoader.cs b/backend/src/GraphQl/Institutions/InstitutionManagedInstitutionsByInstitutionIdDataLoader.cs deleted file mode 100644 index cf41e4cd7..000000000 --- a/backend/src/GraphQl/Institutions/InstitutionManagedInstitutionsByInstitutionIdDataLoader.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Institutions; - -public sealed class InstitutionManagedInstitutionsByInstitutionIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.Institutions.AsNoTracking().Where(x => - ids.Contains(x.ManagerId ?? Guid.Empty) - ).With(queryContext), - x => x.ManagerId ?? Guid.Empty - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionManagedMethodConnection.cs b/backend/src/GraphQl/Institutions/InstitutionManagedMethodConnection.cs index e5d8da00f..6fd0262b1 100644 --- a/backend/src/GraphQl/Institutions/InstitutionManagedMethodConnection.cs +++ b/backend/src/GraphQl/Institutions/InstitutionManagedMethodConnection.cs @@ -2,25 +2,27 @@ using System.Threading; using System.Threading.Tasks; using GreenDonut.Data; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Users; -using Microsoft.AspNetCore.Identity; namespace Metabase.GraphQl.Institutions; public sealed class InstitutionManagedMethodConnection( Institution institution, + PagingArguments pagingArguments, QueryContext queryContext - ) - : Connection( +) +: PaginatedConnection( institution, - x => new InstitutionManagedMethodEdge(x), + (node, cursor) => new InstitutionManagedMethodEdge(node, cursor), + pagingArguments, queryContext ) { [UseUserManager] + [Cost(1)] public Task IsAuthorizedToAddEdgeAsync( ClaimsPrincipal claimsPrincipal, MethodAuthorization authorization, diff --git a/backend/src/GraphQl/Institutions/InstitutionManagedMethodEdge.cs b/backend/src/GraphQl/Institutions/InstitutionManagedMethodEdge.cs index 3ddbf87c4..c7d715921 100644 --- a/backend/src/GraphQl/Institutions/InstitutionManagedMethodEdge.cs +++ b/backend/src/GraphQl/Institutions/InstitutionManagedMethodEdge.cs @@ -2,9 +2,11 @@ namespace Metabase.GraphQl.Institutions; -public sealed class InstitutionManagedMethodEdge( - Method node - ) -{ - public Method Node { get; } = node; -} \ No newline at end of file +public sealed record InstitutionManagedMethodEdge( + Method Node, + string Cursor +) +: PaginatedEdge( + Node, + Cursor +); \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionManagedMethodFilterType.cs b/backend/src/GraphQl/Institutions/InstitutionManagedMethodFilterType.cs index 38a0feb14..f2529d6e6 100644 --- a/backend/src/GraphQl/Institutions/InstitutionManagedMethodFilterType.cs +++ b/backend/src/GraphQl/Institutions/InstitutionManagedMethodFilterType.cs @@ -1,5 +1,4 @@ using HotChocolate.Data.Filters; -using Metabase.Configuration; using Metabase.Data; using Metabase.GraphQl.Methods; @@ -14,6 +13,6 @@ IFilterInputTypeDescriptor descriptor { base.Configure(descriptor); descriptor.Name(nameof(InstitutionManagedMethodFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); - descriptor.Field(x => x.Manager).Ignore(); + descriptor.Field(_ => _.Manager).Ignore(); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Institutions/InstitutionManagedMethodSortType.cs b/backend/src/GraphQl/Institutions/InstitutionManagedMethodSortType.cs new file mode 100644 index 000000000..95f9e2d86 --- /dev/null +++ b/backend/src/GraphQl/Institutions/InstitutionManagedMethodSortType.cs @@ -0,0 +1,17 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data; +using Metabase.GraphQl.Methods; + +namespace Metabase.GraphQl.Institutions; + +public sealed class InstitutionManagedMethodSortType + : MethodSortType +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.Name(nameof(InstitutionManagedMethodSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionManagedMethodsByInstitutionIdDataLoader.cs b/backend/src/GraphQl/Institutions/InstitutionManagedMethodsByInstitutionIdDataLoader.cs deleted file mode 100644 index 79c712bd3..000000000 --- a/backend/src/GraphQl/Institutions/InstitutionManagedMethodsByInstitutionIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Institutions; - -public sealed class InstitutionManagedMethodsByInstitutionIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.Methods.AsNoTracking().Where(x => - ids.Contains(x.ManagerId) - ).With(queryContext), - x => x.ManagerId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionManagerEdge.cs b/backend/src/GraphQl/Institutions/InstitutionManagerEdge.cs index 9fc6bd0f5..5916096be 100644 --- a/backend/src/GraphQl/Institutions/InstitutionManagerEdge.cs +++ b/backend/src/GraphQl/Institutions/InstitutionManagerEdge.cs @@ -6,6 +6,6 @@ namespace Metabase.GraphQl.Institutions; public sealed class InstitutionManagerEdge( Institution association ) - : Edge(association.ManagerId ?? Guid.Empty) + : Edge(association.ManagerId ?? Guid.Empty) { } \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionManufacturedComponentByInstitutionIdDataLoader.cs b/backend/src/GraphQl/Institutions/InstitutionManufacturedComponentByInstitutionIdDataLoader.cs deleted file mode 100644 index d01be9182..000000000 --- a/backend/src/GraphQl/Institutions/InstitutionManufacturedComponentByInstitutionIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Institutions; - -public sealed class InstitutionManufacturedComponentsByInstitutionIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.ComponentManufacturers.AsNoTracking().Where(x => - !x.Pending && ids.Contains(x.InstitutionId) - ).With(queryContext), - x => x.InstitutionId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionManufacturedComponentConnection.cs b/backend/src/GraphQl/Institutions/InstitutionManufacturedComponentConnection.cs index e1e60a1bf..631267972 100644 --- a/backend/src/GraphQl/Institutions/InstitutionManufacturedComponentConnection.cs +++ b/backend/src/GraphQl/Institutions/InstitutionManufacturedComponentConnection.cs @@ -2,38 +2,43 @@ using System.Threading; using System.Threading.Tasks; using GreenDonut.Data; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Users; -using Microsoft.AspNetCore.Identity; namespace Metabase.GraphQl.Institutions; public sealed class InstitutionManufacturedComponentConnection( - Institution institution, + Institution subject, + PagingArguments pagingArguments, QueryContext queryContext - ) - : Connection( - institution, - x => new InstitutionManufacturedComponentEdge(x), - queryContext - ) +) +: PaginatedConnection( + subject, + (association, cursor) => new InstitutionManufacturedComponentEdge(association, cursor), + pagingArguments, + queryContext +) { } public sealed class PendingInstitutionManufacturedComponentConnection( - Institution institution, + Institution subject, + PagingArguments pagingArguments, QueryContext queryContext - ) - : AuthorizedConnection( - institution, - x => new InstitutionManufacturedComponentEdge(x), - (claimsPrincipal, institution, authorization, cancellationToken) => - authorization.IsAuthorizedToConfirm(claimsPrincipal, institution.Id, cancellationToken), - queryContext - ) +) +: AuthorizedPaginatedConnection( + subject, + (association, cursor) => new InstitutionManufacturedComponentEdge(association, cursor), + (claimsPrincipal, authorization, cancellationToken) => + authorization.IsAuthorizedToConfirm(claimsPrincipal, subject.Id, cancellationToken), + pagingArguments, + queryContext +) { [UseUserManager] + [Cost(1)] public Task IsAuthorizedToConfirmEdgesAsync( ClaimsPrincipal claimsPrincipal, ComponentManufacturerAuthorization authorization, diff --git a/backend/src/GraphQl/Institutions/InstitutionManufacturedComponentEdge.cs b/backend/src/GraphQl/Institutions/InstitutionManufacturedComponentEdge.cs index adc23aba2..76aae2737 100644 --- a/backend/src/GraphQl/Institutions/InstitutionManufacturedComponentEdge.cs +++ b/backend/src/GraphQl/Institutions/InstitutionManufacturedComponentEdge.cs @@ -4,8 +4,12 @@ namespace Metabase.GraphQl.Institutions; public sealed class InstitutionManufacturedComponentEdge( - ComponentManufacturer association - ) - : Edge(association.ComponentId) + ComponentManufacturer association, + string cursor +) +: PaginatedEdge( + association.ComponentId, + cursor +) { } \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionManufacturedComponentFilterType.cs b/backend/src/GraphQl/Institutions/InstitutionManufacturedComponentFilterType.cs index bb9e9666b..d44753928 100644 --- a/backend/src/GraphQl/Institutions/InstitutionManufacturedComponentFilterType.cs +++ b/backend/src/GraphQl/Institutions/InstitutionManufacturedComponentFilterType.cs @@ -13,6 +13,6 @@ IFilterInputTypeDescriptor descriptor { base.Configure(descriptor); descriptor.Name(nameof(InstitutionManufacturedComponentFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); - descriptor.Field(x => x.Institution).Ignore(); + descriptor.Field(_ => _.Institution).Ignore(); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Institutions/InstitutionManufacturedComponentSortType.cs b/backend/src/GraphQl/Institutions/InstitutionManufacturedComponentSortType.cs new file mode 100644 index 000000000..4c3f1bc9d --- /dev/null +++ b/backend/src/GraphQl/Institutions/InstitutionManufacturedComponentSortType.cs @@ -0,0 +1,17 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data; +using Metabase.GraphQl.ComponentManufacturers; + +namespace Metabase.GraphQl.Institutions; + +public sealed class InstitutionManufacturedComponentSortType + : ComponentManufacturerSortType +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.Name(nameof(InstitutionManufacturedComponentSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionMutations.cs b/backend/src/GraphQl/Institutions/InstitutionMutations.cs index 89d7e7bd7..de4e92c9f 100644 --- a/backend/src/GraphQl/Institutions/InstitutionMutations.cs +++ b/backend/src/GraphQl/Institutions/InstitutionMutations.cs @@ -67,7 +67,7 @@ CancellationToken cancellationToken if (input.InstitutionId is not null && await context.Institutions.AsQueryable() .AnyAsync( - x => x.Id == input.InstitutionId, + _ => _.Id == input.InstitutionId, cancellationToken ) ) @@ -113,7 +113,7 @@ await context.Users.AsQueryable() if (input.ManagerId is not null && !await context.Institutions.AsQueryable() .AnyAsync( - x => x.Id == input.ManagerId, + _ => _.Id == input.ManagerId, cancellationToken ) ) @@ -172,9 +172,9 @@ input.InstitutionId is null if (institution.State == InstitutionState.PENDING) { var verifiers = await authorization.GetUsersInRoleAsync(UserRole.VERIFIER); - var institutionsUri = new UriBuilder(appSettings.Uri) + var institutionUri = new UriBuilder(appSettings.Uri) { - Path = "/institutions" + Path = $"/institutions/{institution.Id:D}" }.Uri; await Task.WhenAll( verifiers.Select(verifier => @@ -183,7 +183,7 @@ verifier.Email is null : emailSender.SendAsync( (verifier.Name, verifier.Email), $"New institution `{institution.Name}` in metabase awaits verification", - $"Dear {verifier.Name}, please verify institution '{institution.Name}' with UUID {institution.Id:D} on {institutionsUri.AbsoluteUri} Have a nice day! :-)" + $"Dear {verifier.Name}, please verify institution '{institution.Name}' with UUID '{institution.Id:D}' on {institutionUri.AbsoluteUri} Have a nice day! :-)" ) ) ); @@ -499,4 +499,4 @@ await context.Institutions.AsQueryable() await context.SaveChangesAsync(cancellationToken); return new SetInstitutionExtrasPayload(institution); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Institutions/InstitutionOperatedDatabaseConnection.cs b/backend/src/GraphQl/Institutions/InstitutionOperatedDatabaseConnection.cs index 88bbccbf5..84469a4ce 100644 --- a/backend/src/GraphQl/Institutions/InstitutionOperatedDatabaseConnection.cs +++ b/backend/src/GraphQl/Institutions/InstitutionOperatedDatabaseConnection.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using GreenDonut.Data; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Users; @@ -12,14 +13,14 @@ public sealed class InstitutionOperatedDatabaseConnection( Institution institution, QueryContext queryContext ) - : Connection( + : Connection( institution, x => new InstitutionOperatedDatabaseEdge(x), queryContext ) { [UseUserManager] + [Cost(1)] public Task IsAuthorizedToAddEdgeAsync( ClaimsPrincipal claimsPrincipal, DatabaseAuthorization authorization, diff --git a/backend/src/GraphQl/Institutions/InstitutionOperatedDatabaseFilterType.cs b/backend/src/GraphQl/Institutions/InstitutionOperatedDatabaseFilterType.cs index aefc8bf6c..a0751074f 100644 --- a/backend/src/GraphQl/Institutions/InstitutionOperatedDatabaseFilterType.cs +++ b/backend/src/GraphQl/Institutions/InstitutionOperatedDatabaseFilterType.cs @@ -1,5 +1,4 @@ using HotChocolate.Data.Filters; -using Metabase.Configuration; using Metabase.Data; using Metabase.GraphQl.Databases; @@ -14,6 +13,6 @@ IFilterInputTypeDescriptor descriptor { base.Configure(descriptor); descriptor.Name(nameof(InstitutionOperatedDatabaseFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); - descriptor.Field(x => x.Operator).Ignore(); + descriptor.Field(_ => _.Operator).Ignore(); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Institutions/InstitutionOperatedDatabaseSortType.cs b/backend/src/GraphQl/Institutions/InstitutionOperatedDatabaseSortType.cs new file mode 100644 index 000000000..1975f45dc --- /dev/null +++ b/backend/src/GraphQl/Institutions/InstitutionOperatedDatabaseSortType.cs @@ -0,0 +1,17 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data; +using Metabase.GraphQl.Databases; + +namespace Metabase.GraphQl.Institutions; + +public sealed class InstitutionOperatedDatabaseSortType + : DatabaseSortType +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.Name(nameof(InstitutionOperatedDatabaseSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionOperatedDatabasesByInstitutionIdDataLoader.cs b/backend/src/GraphQl/Institutions/InstitutionOperatedDatabasesByInstitutionIdDataLoader.cs deleted file mode 100644 index 62aef385c..000000000 --- a/backend/src/GraphQl/Institutions/InstitutionOperatedDatabasesByInstitutionIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Institutions; - -public sealed class InstitutionOperatedDatabasesByInstitutionIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.Databases.AsNoTracking().Where(x => - ids.Contains(x.OperatorId) - ).With(queryContext), - x => x.OperatorId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionOwnedOpenIdConnectApplicationConnection.cs b/backend/src/GraphQl/Institutions/InstitutionOwnedOpenIdConnectApplicationConnection.cs index 19dd95ae0..25c9ee5d6 100644 --- a/backend/src/GraphQl/Institutions/InstitutionOwnedOpenIdConnectApplicationConnection.cs +++ b/backend/src/GraphQl/Institutions/InstitutionOwnedOpenIdConnectApplicationConnection.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using GreenDonut.Data; +using HotChocolate.CostAnalysis.Types; using Metabase.Data; using Metabase.Data.OpenIdConnect; using Metabase.GraphQl.Users; @@ -14,8 +15,8 @@ QueryContext queryContext ) : AuthorizedConnection< Institution, OpenIdConnectApplication, - InstitutionOwnedOpenIdConnectApplicationsByInstitutionIdDataLoader, InstitutionOwnedOpenIdConnectApplicationEdge, + InstitutionOwnedOpenIdConnectApplicationsByInstitutionIdDataLoader, Authorization.OpenIdConnectAuthorization > ( @@ -27,6 +28,7 @@ QueryContext queryContext ) { [UseUserManager] + [Cost(1)] public Task IsAuthorizedToAddEdgeAsync( ClaimsPrincipal claimsPrincipal, Authorization.OpenIdConnectAuthorization authorization, @@ -41,6 +43,7 @@ CancellationToken cancellationToken } [UseUserManager] + [Cost(1)] public Task IsAuthorizedToRemoveEdgeAsync( ClaimsPrincipal claimsPrincipal, Authorization.OpenIdConnectAuthorization authorization, diff --git a/backend/src/GraphQl/Institutions/InstitutionOwnedOpenIdConnectApplicationEdge.cs b/backend/src/GraphQl/Institutions/InstitutionOwnedOpenIdConnectApplicationEdge.cs index bc946c7bd..9613c9dac 100644 --- a/backend/src/GraphQl/Institutions/InstitutionOwnedOpenIdConnectApplicationEdge.cs +++ b/backend/src/GraphQl/Institutions/InstitutionOwnedOpenIdConnectApplicationEdge.cs @@ -1,6 +1,7 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using HotChocolate.CostAnalysis.Types; using Metabase.Data.OpenIdConnect; using Metabase.GraphQl.Users; @@ -13,6 +14,7 @@ OpenIdConnectApplication node public OpenIdConnectApplication Node { get; } = node; [UseUserManager] + [Cost(1)] public Task IsAuthorizedToRemoveEdgeAsync( ClaimsPrincipal claimsPrincipal, Authorization.OpenIdConnectAuthorization authorization, diff --git a/backend/src/GraphQl/Institutions/InstitutionOwnedOpenIdConnectApplicationFilterType.cs b/backend/src/GraphQl/Institutions/InstitutionOwnedOpenIdConnectApplicationFilterType.cs index 118e3f50a..501a82847 100644 --- a/backend/src/GraphQl/Institutions/InstitutionOwnedOpenIdConnectApplicationFilterType.cs +++ b/backend/src/GraphQl/Institutions/InstitutionOwnedOpenIdConnectApplicationFilterType.cs @@ -13,6 +13,6 @@ IFilterInputTypeDescriptor descriptor { base.Configure(descriptor); descriptor.Name(nameof(InstitutionOwnedOpenIdConnectApplicationFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); - descriptor.Field(x => x.Owner).Ignore(); + descriptor.Field(_ => _.Owner).Ignore(); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Institutions/InstitutionOwnedOpenIdConnectApplicationSortType.cs b/backend/src/GraphQl/Institutions/InstitutionOwnedOpenIdConnectApplicationSortType.cs new file mode 100644 index 000000000..0ec7e7ddb --- /dev/null +++ b/backend/src/GraphQl/Institutions/InstitutionOwnedOpenIdConnectApplicationSortType.cs @@ -0,0 +1,17 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data.OpenIdConnect; +using Metabase.GraphQl.OpenIdConnect.Applications; + +namespace Metabase.GraphQl.Institutions; + +public sealed class InstitutionOwnedOpenIdConnectApplicationSortType + : OpenIdConnectApplicationSortType +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.Name(nameof(InstitutionOwnedOpenIdConnectApplicationSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionOwnedOpenIdConnectApplicationsByInstitutionIdDataLoader.cs b/backend/src/GraphQl/Institutions/InstitutionOwnedOpenIdConnectApplicationsByInstitutionIdDataLoader.cs deleted file mode 100644 index 0f97ebfe6..000000000 --- a/backend/src/GraphQl/Institutions/InstitutionOwnedOpenIdConnectApplicationsByInstitutionIdDataLoader.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.Data.OpenIdConnect; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Institutions; - -public sealed class InstitutionOwnedOpenIdConnectApplicationsByInstitutionIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) : - AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.OpenIdConnectApplications.AsNoTracking().Where(x => - ids.Contains(x.OwnerId) - ).With(queryContext), - x => x.OwnerId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionQueries.cs b/backend/src/GraphQl/Institutions/InstitutionQueries.cs index 7ab174157..aacf944e7 100644 --- a/backend/src/GraphQl/Institutions/InstitutionQueries.cs +++ b/backend/src/GraphQl/Institutions/InstitutionQueries.cs @@ -3,9 +3,10 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using GreenDonut.Data; using HotChocolate.Authorization; using HotChocolate.Data; -using HotChocolate.Data.Sorting; +using HotChocolate.Resolvers; using HotChocolate.Types; using Metabase.Authorization; using Metabase.Data; @@ -46,49 +47,47 @@ application is null } [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 GetInstitutions( - ApplicationDbContext context, - ISortingContext sorting + public ValueTask> GetInstitutionsAsync( + IResolverContext resolverContext, + ApplicationDbContext databaseContext, + CancellationToken cancellationToken ) { - sorting.StabilizeOrder(); - var institutions = context.Institutions.AsNoTracking() - .Where(d => d.State == InstitutionState.VERIFIED); - - return institutions; + return databaseContext.Institutions + .AsNoTracking() + .Where(d => d.State == InstitutionState.VERIFIED) + .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] [Authorize(Policy = AuthorizationPolicies.WriteScopePolicy)] [Authorize(Policy = AuthorizationPolicies.VerifyScopePolicy)] - public IQueryable GetPendingInstitutions( - ApplicationDbContext context, - ISortingContext sorting + public ValueTask> GetPendingInstitutionsAsync( + IResolverContext resolverContext, + ApplicationDbContext databaseContext, + CancellationToken cancellationToken ) { - sorting.StabilizeOrder(); - return - context.Institutions.AsNoTracking() - .Where(d => d.State == InstitutionState.PENDING); + return databaseContext.Institutions + .AsNoTracking() + .Where(d => d.State == InstitutionState.PENDING) + .With(resolverContext.GetQueryContext(), Sorting.DefaultEntityOrder) + .ToPageAsync(resolverContext.GetPagingArguments(), cancellationToken) + .ToConnectionAsync(); } public Task GetInstitutionAsync( Guid id, - InstitutionByIdDataLoader institutionById, + IInstitutionByIdDataLoader byId, CancellationToken cancellationToken ) { - return institutionById.LoadAsync( - id, - cancellationToken - ); + return byId.LoadAsync(id, cancellationToken); } } \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionRepresentativeConnection.cs b/backend/src/GraphQl/Institutions/InstitutionRepresentativeConnection.cs index 91b94f686..91c707899 100644 --- a/backend/src/GraphQl/Institutions/InstitutionRepresentativeConnection.cs +++ b/backend/src/GraphQl/Institutions/InstitutionRepresentativeConnection.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using GreenDonut.Data; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Users; @@ -12,13 +13,14 @@ public sealed class InstitutionRepresentativeConnection( Institution institution, QueryContext queryContext ) - : Connection( + : Connection( institution, x => new InstitutionRepresentativeEdge(x), queryContext ) { [UseUserManager] + [Cost(1)] public Task IsAuthorizedToAddEdgeAsync( ClaimsPrincipal claimsPrincipal, InstitutionRepresentativeAuthorization authorization, @@ -37,7 +39,7 @@ public sealed class PendingInstitutionRepresentativeConnection( Institution institution, QueryContext queryContext ) - : AuthorizedConnection( + : AuthorizedConnection( institution, x => new InstitutionRepresentativeEdge(x), (claimsPrincipal, institution, authorization, cancellationToken) => @@ -46,6 +48,7 @@ QueryContext queryContext ) { [UseUserManager] + [Cost(1)] public Task IsAuthorizedToAddEdgeAsync( ClaimsPrincipal claimsPrincipal, InstitutionRepresentativeAuthorization authorization, diff --git a/backend/src/GraphQl/Institutions/InstitutionRepresentativeEdge.cs b/backend/src/GraphQl/Institutions/InstitutionRepresentativeEdge.cs index a94e38787..0aa21320a 100644 --- a/backend/src/GraphQl/Institutions/InstitutionRepresentativeEdge.cs +++ b/backend/src/GraphQl/Institutions/InstitutionRepresentativeEdge.cs @@ -1,6 +1,7 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.Enumerations; @@ -11,11 +12,12 @@ namespace Metabase.GraphQl.Institutions; public sealed class InstitutionRepresentativeEdge( InstitutionRepresentative association ) -: Edge(association.UserId) +: Edge(association.UserId) { public InstitutionRepresentativeRole Role { get; } = association.Role; [UseUserManager] + [Cost(1)] public Task IsAuthorizedToRemoveEdgeAsync( ClaimsPrincipal claimsPrincipal, InstitutionRepresentativeAuthorization authorization, diff --git a/backend/src/GraphQl/Institutions/InstitutionRepresentativeFilterType.cs b/backend/src/GraphQl/Institutions/InstitutionRepresentativeFilterType.cs index 7531c4adc..1c65374f4 100644 --- a/backend/src/GraphQl/Institutions/InstitutionRepresentativeFilterType.cs +++ b/backend/src/GraphQl/Institutions/InstitutionRepresentativeFilterType.cs @@ -1,5 +1,4 @@ using HotChocolate.Data.Filters; -using Metabase.Configuration; using Metabase.Data; namespace Metabase.GraphQl.Institutions; @@ -13,6 +12,6 @@ IFilterInputTypeDescriptor descriptor { base.Configure(descriptor); descriptor.Name(nameof(InstitutionRepresentativeFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); - descriptor.Field(x => x.Institution).Ignore(); + descriptor.Field(_ => _.Institution).Ignore(); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Institutions/InstitutionRepresentativeSortType.cs b/backend/src/GraphQl/Institutions/InstitutionRepresentativeSortType.cs new file mode 100644 index 000000000..49557c82c --- /dev/null +++ b/backend/src/GraphQl/Institutions/InstitutionRepresentativeSortType.cs @@ -0,0 +1,16 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data; + +namespace Metabase.GraphQl.Institutions; + +public sealed class InstitutionRepresentativeSortType + : InstitutionRepresentatives.InstitutionRepresentativeSortType +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.Name(nameof(InstitutionRepresentativeSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionRepresentativesByInstitutionIdDataLoader.cs b/backend/src/GraphQl/Institutions/InstitutionRepresentativesByInstitutionIdDataLoader.cs deleted file mode 100644 index 91c1cc941..000000000 --- a/backend/src/GraphQl/Institutions/InstitutionRepresentativesByInstitutionIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Institutions; - -public sealed class InstitutionRepresentativesByInstitutionIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.InstitutionRepresentatives.AsNoTracking().Where(x => - !x.Pending && ids.Contains(x.InstitutionId) - ).With(queryContext), - x => x.InstitutionId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionSortType.cs b/backend/src/GraphQl/Institutions/InstitutionSortType.cs index bfc883e53..1d8dce3d3 100644 --- a/backend/src/GraphQl/Institutions/InstitutionSortType.cs +++ b/backend/src/GraphQl/Institutions/InstitutionSortType.cs @@ -4,8 +4,8 @@ namespace Metabase.GraphQl.Institutions; -public sealed class InstitutionSortType - : EntitySortType +public class InstitutionSortType + : AuditableEntitySortType { protected override void Configure( ISortInputTypeDescriptor descriptor @@ -17,6 +17,5 @@ ISortInputTypeDescriptor descriptor descriptor.Field(_ => _.Description); descriptor.Field(_ => _.Contact); descriptor.Field(_ => _.State); - descriptor.Field(_ => _.Manager); } } \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/InstitutionType.cs b/backend/src/GraphQl/Institutions/InstitutionType.cs index 814bf4f00..f47282947 100644 --- a/backend/src/GraphQl/Institutions/InstitutionType.cs +++ b/backend/src/GraphQl/Institutions/InstitutionType.cs @@ -10,7 +10,6 @@ using Metabase.Authorization; using Metabase.Data; using Metabase.Data.OpenIdConnect; -using Metabase.Extensions; using Metabase.GraphQl.Components; using Metabase.GraphQl.DataFormats; using Metabase.GraphQl.Entities; @@ -21,7 +20,7 @@ namespace Metabase.GraphQl.Institutions; public sealed class InstitutionType - : EntityType + : EntityType { protected override void Configure( IObjectTypeDescriptor descriptor @@ -31,20 +30,26 @@ IObjectTypeDescriptor descriptor descriptor .Field(t => t.DevelopedMethods) .Type>>() + .AddPagingArguments() .UseFiltering() + .UseSorting() .Resolve(context => new InstitutionDevelopedMethodConnection( context.Parent(), + context.GetPagingArguments(), context.GetQueryContext() ) ); descriptor .Field($"{GraphQlConstants.PendingPrefix}{nameof(Institution.DevelopedMethods)}") .Type>>() + .AddPagingArguments() .UseFiltering() + .UseSorting() .Resolve(context => new PendingInstitutionDevelopedMethodConnection( context.Parent(), + context.GetPagingArguments(), context.GetQueryContext() ) ); @@ -54,20 +59,26 @@ IObjectTypeDescriptor descriptor descriptor .Field(t => t.ManufacturedComponents) .Type>>() + .AddPagingArguments() .UseFiltering() + .UseSorting() .Resolve(context => new InstitutionManufacturedComponentConnection( context.Parent(), + context.GetPagingArguments(), context.GetQueryContext() ) ); descriptor .Field($"{GraphQlConstants.PendingPrefix}{nameof(Institution.ManufacturedComponents)}") .Type>>() + .AddPagingArguments() .UseFiltering() + .UseSorting() .Resolve(context => new PendingInstitutionManufacturedComponentConnection( context.Parent(), + context.GetPagingArguments(), context.GetQueryContext() ) ); @@ -77,40 +88,52 @@ IObjectTypeDescriptor descriptor descriptor .Field(t => t.ManagedComponents) .Type>>() + .AddPagingArguments() .UseFiltering() + .UseSorting() .Resolve(context => new InstitutionManagedComponentConnection( context.Parent(), + context.GetPagingArguments(), context.GetQueryContext() ) ); descriptor .Field(t => t.ManagedDataFormats) .Type>>() + .AddPagingArguments() .UseFiltering() + .UseSorting() .Resolve(context => new InstitutionManagedDataFormatConnection( context.Parent(), + context.GetPagingArguments(), context.GetQueryContext() ) ); descriptor .Field(t => t.ManagedInstitutions) .Type>>() + .AddPagingArguments() .UseFiltering() + .UseSorting() .Resolve(context => new InstitutionManagedInstitutionConnection( context.Parent(), + context.GetPagingArguments(), context.GetQueryContext() ) ); descriptor .Field(t => t.ManagedMethods) .Type>>() + .AddPagingArguments() .UseFiltering() + .UseSorting() .Resolve(context => new InstitutionManagedMethodConnection( context.Parent(), + context.GetPagingArguments(), context.GetQueryContext() ) ); @@ -132,6 +155,7 @@ IObjectTypeDescriptor descriptor .Field(t => t.OperatedDatabases) .Type>>() .UseFiltering() + .UseSorting() .Resolve(context => new InstitutionOperatedDatabaseConnection( context.Parent(), @@ -141,9 +165,8 @@ IObjectTypeDescriptor descriptor descriptor .Field(t => t.Representatives) .Type>>() - // .UseProjection() .UseFiltering() - // .UseSorting() + .UseSorting() .Resolve(context => new InstitutionRepresentativeConnection( context.Parent(), @@ -154,9 +177,8 @@ IObjectTypeDescriptor descriptor .Field($"{GraphQlConstants.PendingPrefix}{nameof(Institution.Representatives)}") .Type>() .Authorize(AuthorizationPolicies.ManageInstitutionRepresentativeScopePolicy) - // .UseProjection() .UseFiltering() - // .UseSorting() + .UseSorting() .Resolve(context => new PendingInstitutionRepresentativeConnection( context.Parent(), @@ -170,6 +192,7 @@ IObjectTypeDescriptor descriptor .Field(t => t.OpenIdConnectApplications) .Type>>() .UseFiltering() + .UseSorting() .Resolve(context => new InstitutionOwnedOpenIdConnectApplicationConnection( context.Parent(), @@ -180,6 +203,7 @@ IObjectTypeDescriptor descriptor .Field(t => t.GnuPgKeyFingerprints) .Type>>() .UseFiltering() + .UseSorting() .Resolve(context => new InstitutionGnuPgKeyFingerprintConnection( context.Parent(), @@ -189,25 +213,30 @@ IObjectTypeDescriptor descriptor descriptor .Field("has" + nameof(GnuPgKeyFingerprint)) .UseFiltering() + .UseSorting() .ResolveWith(x => InstitutionResolvers.HasGnuPgKeyFingerprintsAsync(default!, default!, default!, default!)); descriptor .Field("isAuthorizedToUpdateNode") + .Cost(1) .ResolveWith(x => InstitutionResolvers.IsAuthorizedToUpdateNodeAsync(default!, default!, default!, default!)) .UseUserManager(); descriptor .Field("isAuthorizedToVerifyNode") + .Cost(1) .ResolveWith(x => InstitutionResolvers.IsAuthorizedToVerifyNodeAsync(default!, default!, default!, default!)) .UseUserManager(); descriptor .Field("isAuthorizedToDeleteNode") + .Cost(1) .ResolveWith(x => InstitutionResolvers.IsAuthorizedToDeleteNodeAsync(default!, default!, default!, default!)) .UseUserManager(); descriptor .Field("isAuthorizedToSwitchOperatingStateOfNode") + .Cost(1) .ResolveWith(x => InstitutionResolvers.IsAuthorizedToSwitchOperatingStateOfNodeAsync(default!, default!, default!, default!)) .UseUserManager(); @@ -267,5 +296,35 @@ CancellationToken cancellationToken { return authorization.IsAuthorizedToSwitchInstitutionOperatingState(claimsPrincipal, institution.Id, cancellationToken); } + + // internal static Task> GetManufacturedComponentsAsync( + // [Parent] Component component, + // IInstitutionManufactureredComponentsByInstitutionIdDataLoader dataLoader, + // IComponentByIdDataLoader nodeById, + // ApplicationDbContext databaseContext, + // PagingArguments pagingArguments, + // QueryContext queryContext, + // QueryContext nodeQueryContext, + // CancellationToken cancellationToken + // ) + // { + // return dataLoader + // .With(pagingArguments, queryContext) + // .LoadAsync(institution.Id, cancellationToken) + // .ToConnectionAsync( + // async (page, entry) => + // { + // return new InstitutionManufactureredComponentEdge( + // entry.Item, + // await nodeById + // .With(nodeQueryContext) + // .LoadAsync(entry.Item.ComponentId, cancellationToken), + // page.CreateCursor(entry) + // ); + // }, + // (edges, pageInfo, totalCount) => + // new InstitutionManufacturedComponentConnection(institution, edges, pageInfo, totalCount) + // ); + // } } } \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/PendingInstitutionDevelopedMethodsByInstitutionIdDataLoader.cs b/backend/src/GraphQl/Institutions/PendingInstitutionDevelopedMethodsByInstitutionIdDataLoader.cs deleted file mode 100644 index f8950a6f4..000000000 --- a/backend/src/GraphQl/Institutions/PendingInstitutionDevelopedMethodsByInstitutionIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Institutions; - -public sealed class PendingInstitutionDevelopedMethodsByInstitutionIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.InstitutionMethodDevelopers.AsNoTracking().Where(x => - x.Pending && ids.Contains(x.InstitutionId) - ).With(queryContext), - x => x.InstitutionId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/PendingInstitutionManufacturedComponentsByInstitutionIdDataLoader.cs b/backend/src/GraphQl/Institutions/PendingInstitutionManufacturedComponentsByInstitutionIdDataLoader.cs deleted file mode 100644 index 5e4308822..000000000 --- a/backend/src/GraphQl/Institutions/PendingInstitutionManufacturedComponentsByInstitutionIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Institutions; - -public sealed class PendingInstitutionManufacturedComponentsByInstitutionIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.ComponentManufacturers.AsNoTracking().Where(x => - x.Pending && ids.Contains(x.InstitutionId) - ).With(queryContext), - x => x.InstitutionId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Institutions/PendingInstitutionRepresentativesByInstitutionIdDataLoader.cs b/backend/src/GraphQl/Institutions/PendingInstitutionRepresentativesByInstitutionIdDataLoader.cs deleted file mode 100644 index 0a25ebc59..000000000 --- a/backend/src/GraphQl/Institutions/PendingInstitutionRepresentativesByInstitutionIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Institutions; - -public sealed class PendingInstitutionRepresentativesByInstitutionIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.InstitutionRepresentatives.AsNoTracking().Where(x => - x.Pending && ids.Contains(x.InstitutionId) - ).With(queryContext), - x => x.InstitutionId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/LifeCycleDataX/LifeCycleData.cs b/backend/src/GraphQl/LifeCycleDataX/LifeCycleData.cs new file mode 100644 index 000000000..da1e39134 --- /dev/null +++ b/backend/src/GraphQl/LifeCycleDataX/LifeCycleData.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate.Resolvers; +using HotChocolate.Types.Relay; +using Metabase.Data; +using Metabase.GraphQl.DataX; +using Metabase.GraphQl.Requests; +using NodaTime; + +namespace Metabase.GraphQl.LifeCycleDataX; + +[Node(IdField = nameof(Id))] +public sealed record LifeCycleData( + string Id, + Guid Uuid, + OffsetDateTime Timestamp, + string Locale, + Guid DatabaseId, + Guid ComponentId, + string? Name, + string? Description, + IReadOnlyList Warnings, + Guid CreatorId, + OffsetDateTime CreatedAt, + AppliedMethod AppliedMethod, + IReadOnlyList Resources, + GetHttpsResourceTree ResourceTree, + IReadOnlyList Approvals +// ResponseApproval Approval +) +: DataX.Data( + Id, + Uuid, + Timestamp, + Locale, + DatabaseId, + ComponentId, + Name, + Description, + Warnings, + CreatorId, + CreatedAt, + AppliedMethod, + Resources, + ResourceTree, + Approvals +) +{ + public override DataKind Kind { get => DataKind.LIFE_CYCLE_DATA; } + + [NodeResolver] + public static Task GetAsync( + string id, + DataQueries dataQueries, + ApplicationDbContext databaseContext, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + return DataX.Data.FetchNodeAsync( + id, + (database, uuid, locale) => dataQueries.GetLifeCycleDataAsync( + database, + uuid, + locale, + resolverContext, + cancellationToken + ), + databaseContext, + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/LifeCycleDataConnection.cs b/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataConnection.cs similarity index 52% rename from backend/src/GraphQl/DataX/LifeCycleDataConnection.cs rename to backend/src/GraphQl/LifeCycleDataX/LifeCycleDataConnection.cs index 77f77a724..1fdc401ed 100644 --- a/backend/src/GraphQl/DataX/LifeCycleDataConnection.cs +++ b/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataConnection.cs @@ -1,13 +1,14 @@ using System.Collections.Generic; using HotChocolate.Types.Pagination; +using Metabase.GraphQl.DataX; -namespace Metabase.GraphQl.DataX; +namespace Metabase.GraphQl.LifeCycleDataX; public sealed record LifeCycleDataConnection( - IReadOnlyList Edges, - uint TotalCount, + IReadOnlyList? Edges, + int TotalCount, ConnectionPageInfo PageInfo -) : DataConnectionBase( +) : DataConnection( Edges, TotalCount, PageInfo diff --git a/backend/src/GraphQl/DataX/LifeCycleDataEdge.cs b/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataEdge.cs similarity index 50% rename from backend/src/GraphQl/DataX/LifeCycleDataEdge.cs rename to backend/src/GraphQl/LifeCycleDataX/LifeCycleDataEdge.cs index 6118ae4cf..f6238d23d 100644 --- a/backend/src/GraphQl/DataX/LifeCycleDataEdge.cs +++ b/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataEdge.cs @@ -1,9 +1,11 @@ -namespace Metabase.GraphQl.DataX; +using Metabase.GraphQl.DataX; + +namespace Metabase.GraphQl.LifeCycleDataX; public sealed record LifeCycleDataEdge( string Cursor, LifeCycleData Node -) : DataEdgeBase( +) : DataEdge( Cursor, Node ); \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/LifeCycleDataPropositionInput.cs b/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataPropositionInput.cs similarity index 63% rename from backend/src/GraphQl/DataX/LifeCycleDataPropositionInput.cs rename to backend/src/GraphQl/LifeCycleDataX/LifeCycleDataPropositionInput.cs index fa000e806..bc7341d80 100644 --- a/backend/src/GraphQl/DataX/LifeCycleDataPropositionInput.cs +++ b/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataPropositionInput.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; +using Metabase.GraphQl.DataX; -namespace Metabase.GraphQl.DataX; +namespace Metabase.GraphQl.LifeCycleDataX; public sealed record LifeCycleDataPropositionInput( UuidPropositionInput? ComponentId, @@ -8,4 +9,8 @@ public sealed record LifeCycleDataPropositionInput( LifeCycleDataPropositionInput? Not, IReadOnlyList? Or, GetHttpsResourcesPropositionInput? Resources -); \ No newline at end of file +) +{ + public static readonly LifeCycleDataPropositionInput Empty = + new(null, null, null, null, null); +}; \ No newline at end of file diff --git a/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataQueries.cs b/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataQueries.cs new file mode 100644 index 000000000..748bc79cb --- /dev/null +++ b/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataQueries.cs @@ -0,0 +1,105 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.CostAnalysis.Types; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using Metabase.Data; +using Metabase.GraphQl.Scalars; +using Metabase.GraphQl.Requests; +using Microsoft.EntityFrameworkCore; + +namespace Metabase.GraphQl.LifeCycleDataX; + +[ExtendObjectType(nameof(Query))] +public sealed class LifeCycleDataQueries +{ + public async Task GetLifeCycleDataAsync( + Guid databaseId, + Guid id, + [GraphQLType] string? locale, + DataQueries dataQueries, + ApplicationDbContext databaseContext, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + var database = await databaseContext.Databases.AsNoTracking() + .Where(_ => _.Id == databaseId) + .SingleOrDefaultAsync(cancellationToken); + if (database is null) + { + return null; + } + return await dataQueries.GetLifeCycleDataAsync( + database, + id, + locale, + resolverContext, + cancellationToken + ); + } + + [ListSize( + AssumedSize = (int)GraphQlConstants.MaximumPageSize - 1, + SlicingArguments = ["first", "last"], + SlicingArgumentDefaultValue = (int)GraphQlConstants.MaximumPageSize - 1, + SizedFields = ["edges", "nodes"], + RequireOneSlicingArgument = false + )] + public Task GetAllLifeCycleDataAsync( + LifeCycleDataPropositionInput? where, + [GraphQLType] string? locale, + int? first, + string? after, + int? last, + string? before, + DataQueries dataQueries, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + return dataQueries.GetAllDataAsync( + first, + after, + last, + before, + (edges, totalCount, pageInfo) => new LifeCycleDataConnection(edges, totalCount, pageInfo), + (node, cursor) => new LifeCycleDataEdge(cursor, node), + (database, first, after, last, before) => dataQueries.GetAllLifeCycleDataAsync( + database, + where, + locale, + first, + after, + last, + before, + resolverContext, + cancellationToken + ), + cancellationToken + ); + } + + public Task HasLifeCycleDataAsync( + LifeCycleDataPropositionInput? where, + [GraphQLType] string? locale, + DataQueries dataQueries, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + return dataQueries.HasDataAsync( + (database) => dataQueries.HasLifeCycleDataAsync( + database, + where, + locale, + resolverContext, + cancellationToken + ), + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/LocaleType.cs b/backend/src/GraphQl/LocaleType.cs deleted file mode 100644 index 770cb0fc2..000000000 --- a/backend/src/GraphQl/LocaleType.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Text.RegularExpressions; -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 Metabase.GraphQl; - -/// -/// [BCP 47](https://tools.ietf.org/html/bcp47) -/// compliant -/// [Language Tag](https://tools.ietf.org/html/bcp47#section-2) -/// string like `"de-AT"`, `"sr-Latn-RS"`, `"en-US"`, or `"en-GB"`, where the language -/// part is essentially an -/// [ISO 639 Language Code](https://www.iso.org/iso-639-language-codes.html), -/// the script an -/// [ISO 15924:2004 Script Code](https://www.iso.org/standard/29546.html), -/// and the region an -/// [ISO 3166-1 Country Code](https://www.iso.org/iso-3166-country-codes.html). -/// Note that the -/// [Internet Assigned Numbers Authority (IANA)](https://www.iana.org) -/// maintains the -/// [Language Subtag Registry](https://www.iana.org/assignments/lang-subtags-templates/lang-subtags-templates.xhtml). -/// -/// -/// Initializes a new instance of the class. -/// -public sealed class LocaleType( - string name, - string? description = null, - BindingBehavior bind = BindingBehavior.Explicit) - : RegexType( - name, - ValidationPattern, - description, - RegexOptions.Compiled | RegexOptions.IgnoreCase, - bind) -{ - private const string ValidationPattern = - "^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)?(-[a-zA-Z0-9]+)?$"; - - /// - /// Initializes a new instance of the class. - /// - public LocaleType() - : this( - nameof(LocaleType)[..^"Type".Length], - "BCP 47 compliant Language Tag string" - ) - { - } -} \ No newline at end of file diff --git a/backend/src/GraphQl/Methods/InstitutionMethodDeveloperEdge.cs b/backend/src/GraphQl/Methods/InstitutionMethodDeveloperEdge.cs index 31a1b3f86..981655308 100644 --- a/backend/src/GraphQl/Methods/InstitutionMethodDeveloperEdge.cs +++ b/backend/src/GraphQl/Methods/InstitutionMethodDeveloperEdge.cs @@ -1,22 +1,21 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Institutions; using Metabase.GraphQl.Users; -using Microsoft.AspNetCore.Identity; namespace Metabase.GraphQl.Methods; public sealed class InstitutionMethodDeveloperEdge( InstitutionMethodDeveloper association ) - : Edge(association.InstitutionId) + : Edge(association.InstitutionId) { - private readonly InstitutionMethodDeveloper _association = association; - [UseUserManager] + [Cost(1)] public Task IsAuthorizedToConfirmEdgeAsync( ClaimsPrincipal claimsPrincipal, InstitutionMethodDeveloperAuthorization authorization, @@ -25,12 +24,13 @@ CancellationToken cancellationToken { return authorization.IsAuthorizedToConfirm( claimsPrincipal, - _association.InstitutionId, + association.InstitutionId, cancellationToken ); } [UseUserManager] + [Cost(1)] public Task IsAuthorizedToRemoveEdgeAsync( ClaimsPrincipal claimsPrincipal, InstitutionMethodDeveloperAuthorization authorization, @@ -39,7 +39,7 @@ CancellationToken cancellationToken { return authorization.IsAuthorizedToRemove( claimsPrincipal, - _association.MethodId, + association.MethodId, cancellationToken ); } diff --git a/backend/src/GraphQl/Methods/InstitutionMethodDevelopersByMethodIdDataLoader.cs b/backend/src/GraphQl/Methods/InstitutionMethodDevelopersByMethodIdDataLoader.cs deleted file mode 100644 index 44ead8520..000000000 --- a/backend/src/GraphQl/Methods/InstitutionMethodDevelopersByMethodIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Methods; - -public sealed class InstitutionMethodDevelopersByMethodIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.InstitutionMethodDevelopers.AsNoTracking().Where(x => - !x.Pending && ids.Contains(x.MethodId) - ).With(queryContext), - x => x.MethodId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Methods/MethodByIdDataLoader.cs b/backend/src/GraphQl/Methods/MethodByIdDataLoader.cs deleted file mode 100644 index fadf0f93d..000000000 --- a/backend/src/GraphQl/Methods/MethodByIdDataLoader.cs +++ /dev/null @@ -1,20 +0,0 @@ -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Methods; - -public sealed class MethodByIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : EntityByIdDataLoader( - batchScheduler, - options, - dbContextFactory, - dbContext => dbContext.Methods - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Methods/MethodDataLoaders.cs b/backend/src/GraphQl/Methods/MethodDataLoaders.cs new file mode 100644 index 000000000..183eb4026 --- /dev/null +++ b/backend/src/GraphQl/Methods/MethodDataLoaders.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut; +using GreenDonut.Data; +using Metabase.Data; +using Microsoft.EntityFrameworkCore; + +namespace Metabase.GraphQl.Methods; + +public sealed class MethodDataLoaders +: DataLoaders +{ + [DataLoader] + public static ValueTask> GetMethodByIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetEntityByIdAsync( + ids, + databaseContext => databaseContext.Methods, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask> GetInstitutionMethodDevelopersByMethodIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetAssociationsByOneIdAsync( + ids, + (databaseContext) => databaseContext.InstitutionMethodDevelopers.Where(_ => !_.Pending), + _ => _.MethodId, + _ => _.InstitutionId, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask> GetPendingInstitutionMethodDevelopersByMethodIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetAssociationsByOneIdAsync( + ids, + (databaseContext) => databaseContext.InstitutionMethodDevelopers.Where(_ => _.Pending), + _ => _.MethodId, + _ => _.InstitutionId, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask> GetUserMethodDevelopersByMethodIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetAssociationsByOneIdAsync( + ids, + (databaseContext) => databaseContext.UserMethodDevelopers.Where(_ => !_.Pending), + _ => _.MethodId, + _ => _.UserId, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask> GetPendingUserMethodDevelopersByMethodIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetAssociationsByOneIdAsync( + ids, + (databaseContext) => databaseContext.UserMethodDevelopers.Where(_ => _.Pending), + _ => _.MethodId, + _ => _.UserId, + queryContext, + databaseContextFactory, + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Methods/MethodDeveloperConnection.cs b/backend/src/GraphQl/Methods/MethodDeveloperConnection.cs index e19d50d30..a9f2cbbb8 100644 --- a/backend/src/GraphQl/Methods/MethodDeveloperConnection.cs +++ b/backend/src/GraphQl/Methods/MethodDeveloperConnection.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using GreenDonut.Data; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Users; @@ -19,9 +20,9 @@ public sealed class MethodDeveloperConnection( QueryContext queryContext ) { - public async Task GetTotalCountAsync( - InstitutionMethodDevelopersByMethodIdDataLoader institutionMethodDevelopersDataLoader, - UserMethodDevelopersByMethodIdDataLoader userMethodDevelopersDataLoader, + public async Task GetTotalCountAsync( + IInstitutionMethodDevelopersByMethodIdDataLoader institutionMethodDevelopersDataLoader, + IUserMethodDevelopersByMethodIdDataLoader userMethodDevelopersDataLoader, CancellationToken cancellationToken ) { @@ -45,8 +46,8 @@ CancellationToken cancellationToken } public async IAsyncEnumerable GetEdgesAsync( - InstitutionMethodDevelopersByMethodIdDataLoader institutionMethodDevelopersDataLoader, - UserMethodDevelopersByMethodIdDataLoader userMethodDevelopersDataLoader, + IInstitutionMethodDevelopersByMethodIdDataLoader institutionMethodDevelopersDataLoader, + IUserMethodDevelopersByMethodIdDataLoader userMethodDevelopersDataLoader, [EnumeratorCancellation] CancellationToken cancellationToken ) { @@ -77,6 +78,7 @@ [EnumeratorCancellation] CancellationToken cancellationToken } [UseUserManager] + [Cost(1)] public Task IsAuthorizedToAddInstitutionEdgeAsync( ClaimsPrincipal claimsPrincipal, InstitutionMethodDeveloperAuthorization authorization, @@ -91,6 +93,7 @@ CancellationToken cancellationToken } [UseUserManager] + [Cost(1)] public Task IsAuthorizedToAddUserEdgeAsync( ClaimsPrincipal claimsPrincipal, InstitutionMethodDeveloperAuthorization authorization, @@ -109,7 +112,7 @@ internal sealed class InstitutionMethodDeveloperConnection( Method subject, QueryContext queryContext ) - : Connection( + : Connection( subject, x => new InstitutionMethodDeveloperEdge(x), queryContext @@ -121,7 +124,7 @@ internal sealed class UserMethodDeveloperConnection( Method subject, QueryContext queryContext ) - : Connection( + : Connection( subject, x => new UserMethodDeveloperEdge(x), queryContext @@ -134,9 +137,9 @@ public sealed class PendingMethodDeveloperConnection( QueryContext queryContext ) { - public async Task GetTotalCountAsync( - PendingInstitutionMethodDevelopersByMethodIdDataLoader pendingInstitutionMethodDevelopersDataLoader, - PendingUserMethodDevelopersByMethodIdDataLoader pendingUserMethodDevelopersDataLoader, + public async Task GetTotalCountAsync( + IPendingInstitutionMethodDevelopersByMethodIdDataLoader pendingInstitutionMethodDevelopersDataLoader, + IPendingUserMethodDevelopersByMethodIdDataLoader pendingUserMethodDevelopersDataLoader, CancellationToken cancellationToken ) { @@ -160,8 +163,8 @@ CancellationToken cancellationToken } public async IAsyncEnumerable GetEdgesAsync( - PendingInstitutionMethodDevelopersByMethodIdDataLoader pendingInstitutionMethodDevelopersDataLoader, - PendingUserMethodDevelopersByMethodIdDataLoader pendingUserMethodDevelopersDataLoader, + IPendingInstitutionMethodDevelopersByMethodIdDataLoader pendingInstitutionMethodDevelopersDataLoader, + IPendingUserMethodDevelopersByMethodIdDataLoader pendingUserMethodDevelopersDataLoader, [EnumeratorCancellation] CancellationToken cancellationToken ) { @@ -192,6 +195,7 @@ [EnumeratorCancellation] CancellationToken cancellationToken } [UseUserManager] + [Cost(1)] public Task IsAuthorizedToAddInstitutionEdgeAsync( ClaimsPrincipal claimsPrincipal, InstitutionMethodDeveloperAuthorization authorization, @@ -206,6 +210,7 @@ CancellationToken cancellationToken } [UseUserManager] + [Cost(1)] public Task IsAuthorizedToAddUserEdgeAsync( ClaimsPrincipal claimsPrincipal, InstitutionMethodDeveloperAuthorization authorization, @@ -224,7 +229,7 @@ internal sealed class PendingInstitutionMethodDeveloperConnection( Method subject, QueryContext queryContext ) - : AuthorizedConnection( + : AuthorizedConnection( subject, x => new InstitutionMethodDeveloperEdge(x), (claimsPrincipal, method, authorization, cancellationToken) => @@ -238,7 +243,7 @@ internal sealed class PendingUserMethodDeveloperConnection( Method subject, QueryContext queryContext ) - : AuthorizedConnection( + : AuthorizedConnection( subject, x => new UserMethodDeveloperEdge(x), (claimsPrincipal, method, authorization, cancellationToken) => diff --git a/backend/src/GraphQl/Methods/MethodDeveloperEdge.cs b/backend/src/GraphQl/Methods/MethodDeveloperEdge.cs index 42f7e3aaa..72ea646f5 100644 --- a/backend/src/GraphQl/Methods/MethodDeveloperEdge.cs +++ b/backend/src/GraphQl/Methods/MethodDeveloperEdge.cs @@ -2,6 +2,7 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Institutions; @@ -28,8 +29,9 @@ UserMethodDeveloperEdge edge _userMethodDeveloperEdge = edge; } + [Cost(0)] public async Task GetNodeAsync( - InstitutionByIdDataLoader institutionById, + IInstitutionByIdDataLoader institutionById, UserByIdDataLoader userById, CancellationToken cancellationToken ) @@ -46,6 +48,7 @@ CancellationToken cancellationToken } [UseUserManager] + [Cost(1)] public async Task IsAuthorizedToConfirmEdgeAsync( ClaimsPrincipal claimsPrincipal, InstitutionMethodDeveloperAuthorization institutionMethodDeveloperAuthorization, @@ -65,6 +68,7 @@ CancellationToken cancellationToken } [UseUserManager] + [Cost(1)] public async Task IsAuthorizedToRemoveEdgeAsync( ClaimsPrincipal claimsPrincipal, InstitutionMethodDeveloperAuthorization institutionMethodDeveloperAuthorization, diff --git a/backend/src/GraphQl/Methods/MethodDeveloperFilterType.cs b/backend/src/GraphQl/Methods/MethodDeveloperFilterType.cs index ccc24a309..85fb65888 100644 --- a/backend/src/GraphQl/Methods/MethodDeveloperFilterType.cs +++ b/backend/src/GraphQl/Methods/MethodDeveloperFilterType.cs @@ -1,16 +1,21 @@ using HotChocolate.Data.Filters; using Metabase.Data; +using Metabase.GraphQl.Associations; namespace Metabase.GraphQl.Methods; public sealed class MethodDeveloperFilterType - : FilterInputType + : AuditableAssociationFilterType { protected override void Configure( IFilterInputTypeDescriptor descriptor ) { - descriptor.BindFieldsExplicitly(); + base.Configure(descriptor); + descriptor.Name(nameof(MethodDeveloperFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); + // TODO Remove CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(_ => _.CreatedAt); + descriptor.Field(_ => _.UpdatedAt); // Disjunctively compose the filters following filters taking into // account the "lifting" done in `MethodDeveloperConnection`. // descriptor @@ -20,4 +25,4 @@ IFilterInputTypeDescriptor descriptor // .Field(nameof(UserMethodDeveloper.User)) // .Type(); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Methods/MethodDeveloperSortType.cs b/backend/src/GraphQl/Methods/MethodDeveloperSortType.cs new file mode 100644 index 000000000..e600f2d88 --- /dev/null +++ b/backend/src/GraphQl/Methods/MethodDeveloperSortType.cs @@ -0,0 +1,25 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data; +using Metabase.GraphQl.Associations; + +namespace Metabase.GraphQl.Methods; + +public sealed class MethodDeveloperSortType + : AuditableAssociationSortType +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.Name(nameof(MethodDeveloperSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); + // Disjunctively compose the sort following sort taking into + // account the "lifting" done in `MethodDeveloperConnection`. + // descriptor + // .Field(nameof(InstitutionMethodDeveloper.Institution)) + // .Type(); + // descriptor + // .Field(nameof(UserMethodDeveloper.User)) + // .Type(); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Methods/MethodFilterType.cs b/backend/src/GraphQl/Methods/MethodFilterType.cs index efe31dcba..ded971f07 100644 --- a/backend/src/GraphQl/Methods/MethodFilterType.cs +++ b/backend/src/GraphQl/Methods/MethodFilterType.cs @@ -5,21 +5,25 @@ namespace Metabase.GraphQl.Methods; public class MethodFilterType - : EntityFilterType + : AuditableEntityFilterType { protected override void Configure( IFilterInputTypeDescriptor descriptor ) { base.Configure(descriptor); - descriptor.Field(x => x.Name); - descriptor.Field(x => x.Description); - descriptor.Field(x => x.CalculationLocator); - descriptor.Field(x => x.Categories); - descriptor.Field(x => x.InstitutionDevelopers); - descriptor.Field(x => x.InstitutionDeveloperEdges); - descriptor.Field(x => x.UserDevelopers); - descriptor.Field(x => x.UserDeveloperEdges); - descriptor.Field(x => x.Manager); + // TODO Remove Id, CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(_ => _.Id); + descriptor.Field(_ => _.CreatedAt); + descriptor.Field(_ => _.UpdatedAt); + descriptor.Field(_ => _.Name); + descriptor.Field(_ => _.Description); + descriptor.Field(_ => _.CalculationLocator); + descriptor.Field(_ => _.Categories); + descriptor.Field(_ => _.InstitutionDevelopers); + descriptor.Field(_ => _.InstitutionDeveloperEdges); + descriptor.Field(_ => _.UserDevelopers); + descriptor.Field(_ => _.UserDeveloperEdges); + descriptor.Field(_ => _.Manager); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Methods/MethodManagerEdge.cs b/backend/src/GraphQl/Methods/MethodManagerEdge.cs index e66a458bd..7e9febb58 100644 --- a/backend/src/GraphQl/Methods/MethodManagerEdge.cs +++ b/backend/src/GraphQl/Methods/MethodManagerEdge.cs @@ -6,6 +6,6 @@ namespace Metabase.GraphQl.Methods; public sealed class MethodManagerEdge( Method association ) - : Edge(association.ManagerId) + : Edge(association.ManagerId) { } \ No newline at end of file diff --git a/backend/src/GraphQl/Methods/MethodMutations.cs b/backend/src/GraphQl/Methods/MethodMutations.cs index c881e5d68..6cd8cdcd7 100644 --- a/backend/src/GraphQl/Methods/MethodMutations.cs +++ b/backend/src/GraphQl/Methods/MethodMutations.cs @@ -46,7 +46,7 @@ CancellationToken cancellationToken if (!await context.Institutions.AsQueryable() .AnyAsync( - x => x.Id == input.ManagerId, + _ => _.Id == input.ManagerId, cancellationToken ) ) @@ -64,7 +64,7 @@ CancellationToken cancellationToken input.InstitutionDeveloperIds.Except( await context.Institutions.AsQueryable() .Where(x => input.InstitutionDeveloperIds.Contains(x.Id)) - .Select(x => x.Id) + .Select(_ => _.Id) .ToListAsync(cancellationToken) ); if (unknownInstitutionDeveloperIds.Any()) @@ -218,4 +218,4 @@ await context.Methods.AsQueryable() await context.SaveChangesAsync(cancellationToken); return new UpdateMethodPayload(method); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Methods/MethodQueries.cs b/backend/src/GraphQl/Methods/MethodQueries.cs index db41e543d..72a02d2e6 100644 --- a/backend/src/GraphQl/Methods/MethodQueries.cs +++ b/backend/src/GraphQl/Methods/MethodQueries.cs @@ -2,8 +2,9 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using GreenDonut.Data; using HotChocolate.Data; -using HotChocolate.Data.Sorting; +using HotChocolate.Resolvers; using HotChocolate.Types; using Metabase.Data; using Metabase.GraphQl.Extensions; @@ -15,27 +16,27 @@ namespace Metabase.GraphQl.Methods; public sealed class MethodQueries { [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 GetMethods( - ApplicationDbContext context, - ISortingContext sorting + public ValueTask> GetMethodsAsync( + IResolverContext resolverContext, + ApplicationDbContext databaseContext, + CancellationToken cancellationToken ) { - sorting.StabilizeOrder(); - return context.Methods.AsNoTracking(); + return databaseContext.Methods + .AsNoTracking() + .With(resolverContext.GetQueryContext(), Sorting.DefaultEntityOrder) + .ToPageAsync(resolverContext.GetPagingArguments(), cancellationToken) + .ToConnectionAsync(); } public Task GetMethodAsync( Guid id, - MethodByIdDataLoader methodById, + IMethodByIdDataLoader byId, CancellationToken cancellationToken ) { - return methodById.LoadAsync( - id, - cancellationToken - ); + return byId.LoadAsync(id, cancellationToken); } } \ No newline at end of file diff --git a/backend/src/GraphQl/Methods/MethodSortType.cs b/backend/src/GraphQl/Methods/MethodSortType.cs index 1c725d4d6..cb59b2521 100644 --- a/backend/src/GraphQl/Methods/MethodSortType.cs +++ b/backend/src/GraphQl/Methods/MethodSortType.cs @@ -4,17 +4,16 @@ namespace Metabase.GraphQl.Methods; -public sealed class MethodSortType - : EntitySortType +public class MethodSortType + : AuditableEntitySortType { protected override void Configure( ISortInputTypeDescriptor descriptor ) { base.Configure(descriptor); - descriptor.Field(x => x.Name); - descriptor.Field(x => x.Description); - descriptor.Field(x => x.CalculationLocator); - descriptor.Field(x => x.Manager); + descriptor.Field(_ => _.Name); + descriptor.Field(_ => _.Description); + descriptor.Field(_ => _.CalculationLocator); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Methods/MethodType.cs b/backend/src/GraphQl/Methods/MethodType.cs index 8f3793e29..cafb8952d 100644 --- a/backend/src/GraphQl/Methods/MethodType.cs +++ b/backend/src/GraphQl/Methods/MethodType.cs @@ -14,7 +14,7 @@ namespace Metabase.GraphQl.Methods; public sealed class MethodType - : EntityType + : EntityType { protected override void Configure( IObjectTypeDescriptor descriptor @@ -24,6 +24,7 @@ IObjectTypeDescriptor descriptor descriptor .Field(t => t.Reference) .Type() + .Cost(0) .Resolve(context => context .Parent() .Reference? @@ -44,6 +45,7 @@ IObjectTypeDescriptor descriptor .Field(t => t.Developers) .Type>>() .UseFiltering() + .UseSorting() .Resolve(context => new MethodDeveloperConnection( context.Parent(), @@ -55,6 +57,7 @@ IObjectTypeDescriptor descriptor .Type>() .Authorize(AuthorizationPolicies.WriteScopePolicy) .UseFiltering() + .UseSorting() .Resolve(context => new PendingMethodDeveloperConnection( context.Parent(), @@ -75,6 +78,7 @@ IObjectTypeDescriptor descriptor .Ignore(); descriptor .Field("isAuthorizedToUpdateNode") + .Cost(1) .ResolveWith(x => MethodResolvers.IsAuthorizedToUpdateNodeAsync(default!, default!, default!, default!)) .UseUserManager(); diff --git a/backend/src/GraphQl/Methods/PendingInstitutionMethodDevelopersByMethodIdDataLoader.cs b/backend/src/GraphQl/Methods/PendingInstitutionMethodDevelopersByMethodIdDataLoader.cs deleted file mode 100644 index b7e54160f..000000000 --- a/backend/src/GraphQl/Methods/PendingInstitutionMethodDevelopersByMethodIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Methods; - -public sealed class PendingInstitutionMethodDevelopersByMethodIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.InstitutionMethodDevelopers.AsNoTracking().Where(x => - x.Pending && ids.Contains(x.MethodId) - ).With(queryContext), - x => x.MethodId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Methods/PendingUserMethodDevelopersByMethodIdDataLoader.cs b/backend/src/GraphQl/Methods/PendingUserMethodDevelopersByMethodIdDataLoader.cs deleted file mode 100644 index 5f9e590fa..000000000 --- a/backend/src/GraphQl/Methods/PendingUserMethodDevelopersByMethodIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Methods; - -public sealed class PendingUserMethodDevelopersByMethodIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.UserMethodDevelopers.AsNoTracking().Where(x => - x.Pending && ids.Contains(x.MethodId) - ).With(queryContext), - x => x.MethodId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Methods/UserMethodDeveloperEdge.cs b/backend/src/GraphQl/Methods/UserMethodDeveloperEdge.cs index 9731ff76b..544e2e5f2 100644 --- a/backend/src/GraphQl/Methods/UserMethodDeveloperEdge.cs +++ b/backend/src/GraphQl/Methods/UserMethodDeveloperEdge.cs @@ -1,21 +1,20 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Users; -using Microsoft.AspNetCore.Identity; namespace Metabase.GraphQl.Methods; public sealed class UserMethodDeveloperEdge( UserMethodDeveloper association ) - : Edge(association.UserId) + : Edge(association.UserId) { - private readonly UserMethodDeveloper _association = association; - [UseUserManager] + [Cost(1)] public Task IsAuthorizedToConfirmEdgeAsync( ClaimsPrincipal claimsPrincipal, UserMethodDeveloperAuthorization authorization, @@ -24,12 +23,13 @@ CancellationToken cancellationToken { return authorization.IsAuthorizedToConfirm( claimsPrincipal, - _association.UserId, + association.UserId, cancellationToken ); } [UseUserManager] + [Cost(1)] public Task IsAuthorizedToRemoveEdgeAsync( ClaimsPrincipal claimsPrincipal, UserMethodDeveloperAuthorization authorization, @@ -38,7 +38,7 @@ CancellationToken cancellationToken { return authorization.IsAuthorizedToRemove( claimsPrincipal, - _association.MethodId, + association.MethodId, cancellationToken ); } diff --git a/backend/src/GraphQl/Methods/UserMethodDevelopersByMethodIdDataLoader.cs b/backend/src/GraphQl/Methods/UserMethodDevelopersByMethodIdDataLoader.cs deleted file mode 100644 index 31a617f9c..000000000 --- a/backend/src/GraphQl/Methods/UserMethodDevelopersByMethodIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Methods; - -public sealed class UserMethodDevelopersByMethodIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.UserMethodDevelopers.AsNoTracking().Where(x => - !x.Pending && ids.Contains(x.MethodId) - ).With(queryContext), - x => x.MethodId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/NodaTime/OffsetDateTimeCursorKeySerializer.cs b/backend/src/GraphQl/NodaTime/OffsetDateTimeCursorKeySerializer.cs new file mode 100644 index 000000000..c3bd39ec4 --- /dev/null +++ b/backend/src/GraphQl/NodaTime/OffsetDateTimeCursorKeySerializer.cs @@ -0,0 +1,45 @@ +using System; +using System.Buffers; +using System.Reflection; +using System.Text.Unicode; +using GreenDonut.Data.Cursors.Serializers; +using Metabase.Extensions; +using NodaTime; +using NodaTime.Text; + +namespace Metabase.GraphQl.NodaTime; + +// Inspired by https://github.com/ChilliCream/graphql-platform/blob/main/src/GreenDonut/src/GreenDonut.Data/Cursors/Serializers/DateTimeOffsetCursorKeySerializer.cs +public sealed class OffsetDateTimeCursorKeySerializer : ICursorKeySerializer +{ + private static readonly OffsetDateTimePattern s_pattern = OffsetDateTimePattern.ExtendedIso; + + public bool IsSupported(Type type) + => type == typeof(OffsetDateTime) || type == typeof(OffsetDateTime?); + + // TODO causes a runtime exception because `CompareTo` is not an instance method of `OffsetDateTime` + public MethodInfo GetCompareToMethod(Type type) + => typeof(NodaTimeExtensions).GetMethod(nameof(NodaTimeExtensions.CompareTo), [typeof(OffsetDateTime)])!; + + public object Parse(ReadOnlySpan formattedKey) + { + Span chars = stackalloc char[formattedKey.Length]; + if (Utf8.ToUtf16(formattedKey, chars, out _, out _) != OperationStatus.Done) + { + throw new FormatException("Invalid cursor format"); + } + var result = s_pattern.Parse(new string(chars)); + if (!result.Success) + { + throw new FormatException("Could not parse `OffsetDateTime` cursor"); + } + return result.Value; + } + + public bool TryFormat(object key, Span buffer, out int written) + { + var value = (OffsetDateTime)key; + var formatted = s_pattern.Format(value); + return Utf8.FromUtf16(formatted, buffer, out _, out written) == OperationStatus.Done; + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/CreateOpenIdConnectApplicationErrorCode.cs b/backend/src/GraphQl/OpenIdConnect/Applications/CreateOpenIdConnectApplicationErrorCode.cs index 09ff7b25f..0facd5b6f 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/CreateOpenIdConnectApplicationErrorCode.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/CreateOpenIdConnectApplicationErrorCode.cs @@ -9,4 +9,9 @@ public enum CreateOpenIdConnectApplicationErrorCode UNAUTHORIZED, UNKNOWN_INSTITUTION, DUPLICATE_CLIENT_ID, + ILLEGAL_CONSENT_TYPE, + ILLEGAL_SCOPE, + ILLEGAL_GRANT_TYPE, + ILLEGAL_ENDPOINT, + ILLEGAL_RESPONSE_TYPE, } \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/CreateOpenIdConnectApplicationInput.cs b/backend/src/GraphQl/OpenIdConnect/Applications/CreateOpenIdConnectApplicationInput.cs index 4e377bb2e..68d3af4fa 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/CreateOpenIdConnectApplicationInput.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/CreateOpenIdConnectApplicationInput.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Metabase.Enumerations; namespace Metabase.GraphQl.OpenIdConnect.Applications; diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/CreateOpenIdConnectApplicationPayload.cs b/backend/src/GraphQl/OpenIdConnect/Applications/CreateOpenIdConnectApplicationPayload.cs index c7830fad7..0347092de 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/CreateOpenIdConnectApplicationPayload.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/CreateOpenIdConnectApplicationPayload.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Metabase.Data.OpenIdConnect; namespace Metabase.GraphQl.OpenIdConnect.Applications; @@ -22,4 +23,11 @@ CreateOpenIdConnectApplicationError error : base(error) { } + + public CreateOpenIdConnectApplicationPayload( + IReadOnlyCollection errors + ) + : base(errors) + { + } } \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationAuthorizationEdge.cs b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationAuthorizationEdge.cs index 65c9a6b01..4a4e93ec6 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationAuthorizationEdge.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationAuthorizationEdge.cs @@ -2,9 +2,6 @@ namespace Metabase.GraphQl.OpenIdConnect.Applications; -public sealed class OpenIdConnectApplicationAuthorizationEdge( - OpenIdConnectAuthorization node -) -{ - public OpenIdConnectAuthorization Node { get; } = node; -} \ No newline at end of file +public sealed record OpenIdConnectApplicationGrantedAuthorizationEdge( + OpenIdConnectAuthorization Node +); \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationByIdDataLoader.cs b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationByIdDataLoader.cs deleted file mode 100644 index 6b3167721..000000000 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationByIdDataLoader.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using GreenDonut; -using Metabase.Data.OpenIdConnect; -using OpenIddict.Core; - -namespace Metabase.GraphQl.OpenIdConnect.Applications; - -public sealed class OpenIdConnectApplicationByIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - OpenIddictApplicationManager applicationManager) -: BatchDataLoader(batchScheduler, options) -{ - protected override async Task> LoadBatchAsync(IReadOnlyList keys, CancellationToken cancellationToken) - { - var ret = new Dictionary(); - foreach (var key in keys) - { - ret.Add(key, await applicationManager.FindByIdAsync(key.ToString(), cancellationToken: cancellationToken)); - } - return ret; - } -} \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationDataLoaders.cs b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationDataLoaders.cs new file mode 100644 index 000000000..85fc1f330 --- /dev/null +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationDataLoaders.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut; +using GreenDonut.Data; +using Metabase.Data; +using Metabase.Data.OpenIdConnect; +using Microsoft.EntityFrameworkCore; + +namespace Metabase.GraphQl.OpenIdConnect.Applications; + +public sealed class OpenIdConnectApplicationDataLoaders +: DataLoaders +{ + [DataLoader] + public static ValueTask> GetOpenIdConnectApplicationByIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetEntityByIdAsync( + ids, + databaseContext => databaseContext.OpenIdConnectApplications, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static async ValueTask> GetOpenIdConnectApplicationByClientIdAsync( + IReadOnlyList clientIds, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + await using var databaseContext = await databaseContextFactory.CreateDbContextAsync(cancellationToken); + return await databaseContext.OpenIdConnectApplications + .AsNoTrackingWithIdentityResolution() + .Where(_ => clientIds.Contains(_.ClientId ?? "")) + .With(queryContext, Sorting.DefaultEntityOrder) + .ToDictionaryAsync(_ => _.ClientId ?? "", cancellationToken); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationFilterType.cs b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationFilterType.cs index bb9817b1d..506586c26 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationFilterType.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationFilterType.cs @@ -5,20 +5,24 @@ namespace Metabase.GraphQl.OpenIdConnect.Applications; public class OpenIdConnectApplicationFilterType - : EntityFilterType + : AuditableEntityFilterType { protected override void Configure( IFilterInputTypeDescriptor descriptor ) { base.Configure(descriptor); - descriptor.Name(nameof(OpenIdConnectApplicationFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); - descriptor.Field(x => x.ApplicationType); - descriptor.Field(x => x.ClientId); - descriptor.Field(x => x.ConsentType); - descriptor.Field(x => x.DisplayName); - // descriptor.Field(x => x.PostLogoutRedirectUris); - // descriptor.Field(x => x.RedirectUris); - // descriptor.Field(x => x.Requirements); + // TODO Remove Id, CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(_ => _.Id); + descriptor.Field(_ => _.CreatedAt); + descriptor.Field(_ => _.UpdatedAt); + descriptor.Field(_ => _.ApplicationType); + descriptor.Field(_ => _.ClientId); + descriptor.Field(_ => _.ConsentType); + descriptor.Field(_ => _.DisplayName); + // descriptor.Field(_ => _.PostLogoutRedirectUris); + // descriptor.Field(_ => _.RedirectUris); + // descriptor.Field(_ => _.Requirements); + descriptor.Field(_ => _.Owner); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationAuthorizationConnection.cs b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationGrantedAuthorizationConnection.cs similarity index 72% rename from backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationAuthorizationConnection.cs rename to backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationGrantedAuthorizationConnection.cs index 7e1c2e5e4..aa3b4c0cf 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationAuthorizationConnection.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationGrantedAuthorizationConnection.cs @@ -4,16 +4,18 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using HotChocolate.CostAnalysis.Types; using Metabase.Data.OpenIdConnect; using OpenIddict.Core; namespace Metabase.GraphQl.OpenIdConnect.Applications; -public sealed class OpenIdConnectApplicationAuthorizationConnection( +public sealed class OpenIdConnectApplicationGrantedAuthorizationConnection( OpenIdConnectApplication application ) { - public async Task GetTotalCountAsync( + [Cost(0)] + public async Task GetTotalCountAsync( ClaimsPrincipal claimsPrincipal, Authorization.OpenIdConnectAuthorization authorization, OpenIddictAuthorizationManager authorizationManager, @@ -24,10 +26,11 @@ CancellationToken cancellationToken { return 0; } - return (uint)await authorizationManager.FindByApplicationIdAsync(application.Id.ToString(), cancellationToken).CountAsync(cancellationToken); + return await authorizationManager.FindByApplicationIdAsync(application.Id.ToString(), cancellationToken).CountAsync(cancellationToken); } - public async IAsyncEnumerable GetEdgesAsync( + [Cost(0)] + public async IAsyncEnumerable GetEdgesAsync( ClaimsPrincipal claimsPrincipal, Authorization.OpenIdConnectAuthorization authorization, OpenIddictAuthorizationManager authorizationManager, @@ -40,7 +43,7 @@ [EnumeratorCancellation] CancellationToken cancellationToken } await foreach (var auth in authorizationManager.FindByApplicationIdAsync(application.Id.ToString(), cancellationToken)) { - yield return new OpenIdConnectApplicationAuthorizationEdge(auth); + yield return new OpenIdConnectApplicationGrantedAuthorizationEdge(auth); } } } \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationTokenConnection.cs b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationIssuedTokenConnection.cs similarity index 72% rename from backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationTokenConnection.cs rename to backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationIssuedTokenConnection.cs index b9b906386..fe5dcb2fa 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationTokenConnection.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationIssuedTokenConnection.cs @@ -4,16 +4,18 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using HotChocolate.CostAnalysis.Types; using Metabase.Data.OpenIdConnect; using OpenIddict.Core; namespace Metabase.GraphQl.OpenIdConnect.Applications; -public sealed class OpenIdConnectApplicationTokenConnection( +public sealed class OpenIdConnectApplicationIssuedTokenConnection( OpenIdConnectApplication application ) { - public async Task GetTotalCountAsync( + [Cost(0)] + public async Task GetTotalCountAsync( ClaimsPrincipal claimsPrincipal, Authorization.OpenIdConnectAuthorization authorization, OpenIddictTokenManager tokenManager, @@ -24,10 +26,11 @@ CancellationToken cancellationToken { return 0; } - return (uint)await tokenManager.FindByApplicationIdAsync(application.Id.ToString(), cancellationToken).CountAsync(cancellationToken); + return await tokenManager.FindByApplicationIdAsync(application.Id.ToString(), cancellationToken).CountAsync(cancellationToken); } - public async IAsyncEnumerable GetEdgesAsync( + [Cost(0)] + public async IAsyncEnumerable GetEdgesAsync( ClaimsPrincipal claimsPrincipal, Authorization.OpenIdConnectAuthorization authorization, OpenIddictTokenManager tokenManager, @@ -40,7 +43,7 @@ [EnumeratorCancellation] CancellationToken cancellationToken } await foreach (var token in tokenManager.FindByApplicationIdAsync(application.Id.ToString(), cancellationToken)) { - yield return new OpenIdConnectApplicationTokenEdge(token); + yield return new OpenIdConnectApplicationIssuedTokenEdge(token); } } } \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationMutations.cs b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationMutations.cs index 14f1bce85..155bbeab3 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationMutations.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationMutations.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Security.Cryptography; @@ -50,14 +51,15 @@ CancellationToken cancellationToken ) ); } + var errors = new List(); if (!await context.Institutions.AsQueryable() .AnyAsync( - x => x.Id == input.InstitutionId, + _ => _.Id == input.InstitutionId, cancellationToken ) ) { - return new CreateOpenIdConnectApplicationPayload( + errors.Add( new CreateOpenIdConnectApplicationError( CreateOpenIdConnectApplicationErrorCode.UNKNOWN_INSTITUTION, "Unknown institution.", @@ -67,7 +69,7 @@ CancellationToken cancellationToken } if (await applicationManager.FindByClientIdAsync(input.ClientId, cancellationToken) is not null) { - return new CreateOpenIdConnectApplicationPayload( + errors.Add( new CreateOpenIdConnectApplicationError( CreateOpenIdConnectApplicationErrorCode.DUPLICATE_CLIENT_ID, "The client ID is already in use.", @@ -75,6 +77,73 @@ CancellationToken cancellationToken ) ); } + var authorizedConsentTypes = await authorization.AuthorizedConsentTypes(claimsPrincipal, cancellationToken); + if (!authorizedConsentTypes.Contains(input.ConsentType)) + { + errors.Add( + new CreateOpenIdConnectApplicationError( + CreateOpenIdConnectApplicationErrorCode.ILLEGAL_CONSENT_TYPE, + $"You may only use the consent type(s) {string.Join(", ", authorizedConsentTypes)}", + [nameof(input), nameof(input.ConsentType).FirstCharToLower()] + ) + ); + } + var unauthorizedEndpoints = input.Endpoints + .Except(await Authorization.OpenIdConnectAuthorization.AuthorizedEndpoints(claimsPrincipal, cancellationToken)) + .ToList().AsReadOnly(); + if (unauthorizedEndpoints.Count > 0) + { + errors.Add( + new CreateOpenIdConnectApplicationError( + CreateOpenIdConnectApplicationErrorCode.ILLEGAL_ENDPOINT, + $"You may not use the endpoint(s) {string.Join(", ", unauthorizedEndpoints)}", + [nameof(input), nameof(input.Endpoints).FirstCharToLower()] + ) + ); + } + var unauthorizedGrantTypes = input.GrantTypes + .Except(await authorization.AuthorizedGrantTypes(claimsPrincipal, cancellationToken)) + .ToList().AsReadOnly(); + if (unauthorizedGrantTypes.Count > 0) + { + errors.Add( + new CreateOpenIdConnectApplicationError( + CreateOpenIdConnectApplicationErrorCode.ILLEGAL_GRANT_TYPE, + $"You may not use the grant type(s) {string.Join(", ", unauthorizedGrantTypes)}", + [nameof(input), nameof(input.GrantTypes).FirstCharToLower()] + ) + ); + } + var unauthorizedResponseTypes = input.ResponseTypes + .Except(await Authorization.OpenIdConnectAuthorization.AuthorizedResponseTypes(claimsPrincipal, cancellationToken)) + .ToList().AsReadOnly(); + if (unauthorizedResponseTypes.Count > 0) + { + errors.Add( + new CreateOpenIdConnectApplicationError( + CreateOpenIdConnectApplicationErrorCode.ILLEGAL_RESPONSE_TYPE, + $"You may not use the response type(s) {string.Join(", ", unauthorizedResponseTypes)}", + [nameof(input), nameof(input.ResponseTypes).FirstCharToLower()] + ) + ); + } + var unauthorizedScopes = input.Scopes + .Except(await authorization.AuthorizedScopes(claimsPrincipal, cancellationToken)) + .ToList().AsReadOnly(); + if (unauthorizedScopes.Count > 0) + { + errors.Add( + new CreateOpenIdConnectApplicationError( + CreateOpenIdConnectApplicationErrorCode.ILLEGAL_SCOPE, + $"You may not use the scope(s) {string.Join(", ", unauthorizedScopes)}", + [nameof(input), nameof(input.Scopes).FirstCharToLower()] + ) + ); + } + if (errors.Count > 0) + { + return new CreateOpenIdConnectApplicationPayload(errors); + } var clientSecret = GenerateClientSecret(); var descriptor = new OpenIddictApplicationDescriptor { @@ -89,10 +158,10 @@ CancellationToken cancellationToken } }; descriptor.Permissions.UnionWith( - input.Endpoints.Select(x => x.ToStringEndpoint()) - .Concat(input.GrantTypes.Select(x => x.ToStringGrantType())) - .Concat(input.ResponseTypes.Select(x => x.ToStringResponseType())) - .Concat(input.Scopes.Select(x => x.ToStringScope())) + input.Endpoints.Select(_ => _.ToPermissionString()) + .Concat(input.GrantTypes.Select(_ => _.ToPermissionString())) + .Concat(input.ResponseTypes.Select(_ => _.ToPermissionString())) + .Concat(input.Scopes.ToPermissionStrings()) ); if (input.RedirectUri is not null) { @@ -186,6 +255,74 @@ CancellationToken cancellationToken ) ); } + var errors = new List(); + var authorizedConsentTypes = await authorization.AuthorizedConsentTypes(claimsPrincipal, cancellationToken); + if (!authorizedConsentTypes.Contains(input.ConsentType)) + { + errors.Add( + new UpdateOpenIdConnectApplicationError( + UpdateOpenIdConnectApplicationErrorCode.ILLEGAL_CONSENT_TYPE, + $"You may only use the consent type(s) {string.Join(", ", authorizedConsentTypes)}", + [nameof(input), nameof(input.ConsentType).FirstCharToLower()] + ) + ); + } + var unauthorizedEndpoints = input.Endpoints + .Except(await Authorization.OpenIdConnectAuthorization.AuthorizedEndpoints(claimsPrincipal, cancellationToken)) + .ToList().AsReadOnly(); + if (unauthorizedEndpoints.Count > 0) + { + errors.Add( + new UpdateOpenIdConnectApplicationError( + UpdateOpenIdConnectApplicationErrorCode.ILLEGAL_ENDPOINT, + $"You may not use the endpoint(s) {string.Join(", ", unauthorizedEndpoints)}", + [nameof(input), nameof(input.Endpoints).FirstCharToLower()] + ) + ); + } + var unauthorizedGrantTypes = input.GrantTypes + .Except(await authorization.AuthorizedGrantTypes(claimsPrincipal, cancellationToken)) + .ToList().AsReadOnly(); + if (unauthorizedGrantTypes.Count > 0) + { + errors.Add( + new UpdateOpenIdConnectApplicationError( + UpdateOpenIdConnectApplicationErrorCode.ILLEGAL_GRANT_TYPE, + $"You may not use the grant type(s) {string.Join(", ", unauthorizedGrantTypes)}", + [nameof(input), nameof(input.GrantTypes).FirstCharToLower()] + ) + ); + } + var unauthorizedResponseTypes = input.ResponseTypes + .Except(await Authorization.OpenIdConnectAuthorization.AuthorizedResponseTypes(claimsPrincipal, cancellationToken)) + .ToList().AsReadOnly(); + if (unauthorizedResponseTypes.Count > 0) + { + errors.Add( + new UpdateOpenIdConnectApplicationError( + UpdateOpenIdConnectApplicationErrorCode.ILLEGAL_RESPONSE_TYPE, + $"You may not use the response type(s) {string.Join(", ", unauthorizedResponseTypes)}", + [nameof(input), nameof(input.ResponseTypes).FirstCharToLower()] + ) + ); + } + var unauthorizedScopes = input.Scopes + .Except(await authorization.AuthorizedScopes(claimsPrincipal, cancellationToken)) + .ToList().AsReadOnly(); + if (unauthorizedScopes.Count > 0) + { + errors.Add( + new UpdateOpenIdConnectApplicationError( + UpdateOpenIdConnectApplicationErrorCode.ILLEGAL_SCOPE, + $"You may not use the scope(s) {string.Join(", ", unauthorizedScopes)}", + [nameof(input), nameof(input.Scopes).FirstCharToLower()] + ) + ); + } + if (errors.Count > 0) + { + return new UpdateOpenIdConnectApplicationPayload(errors); + } var descriptor = new OpenIddictApplicationDescriptor(); await applicationManager.PopulateAsync(descriptor, application, cancellationToken); UpdateOpenIdConnectApplicationDescriptor(input, descriptor); @@ -254,7 +391,7 @@ OpenIddictApplicationDescriptor descriptor { try { - permission.ToOpenIdConnectEndpoint(); + var ignore = permission.PermissionToOpenIdConnectEndpoint(); return true; } catch (ArgumentOutOfRangeException) @@ -266,7 +403,7 @@ OpenIddictApplicationDescriptor descriptor { try { - permission.ToOpenIdConnectGrantType(); + var ignore = permission.PermissionToOpenIdConnectGrantType(); return true; } catch (ArgumentOutOfRangeException) @@ -278,7 +415,7 @@ OpenIddictApplicationDescriptor descriptor { try { - permission.ToOpenIdConnectResponseType(); + var ignore = permission.PermissionToOpenIdConnectResponseType(); return true; } catch (ArgumentOutOfRangeException) @@ -290,7 +427,7 @@ OpenIddictApplicationDescriptor descriptor { try { - permission.ToOpenIdConnectScope(); + var ignore = permission.PermissionToOpenIdConnectScope(); return true; } catch (ArgumentOutOfRangeException) @@ -299,10 +436,10 @@ OpenIddictApplicationDescriptor descriptor } }); descriptor.Permissions.UnionWith( - input.Endpoints.Select(x => x.ToStringEndpoint()) - .Concat(input.GrantTypes.Select(x => x.ToStringGrantType())) - .Concat(input.ResponseTypes.Select(x => x.ToStringResponseType())) - .Concat(input.Scopes.Select(x => x.ToStringScope())) + input.Endpoints.Select(_ => _.ToPermissionString()) + .Concat(input.GrantTypes.Select(_ => _.ToPermissionString())) + .Concat(input.ResponseTypes.Select(_ => _.ToPermissionString())) + .Concat(input.Scopes.ToPermissionStrings()) ); } } \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationOwnerEdge.cs b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationOwnerEdge.cs index 5bc0e715b..0908328b0 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationOwnerEdge.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationOwnerEdge.cs @@ -7,6 +7,6 @@ namespace Metabase.GraphQl.OpenIdConnect.Applications; public sealed class OpenIdConnectApplicationOwnerEdge( OpenIdConnectApplication association ) - : Edge(association.OwnerId) + : Edge(association.OwnerId) { } \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationQueries.cs b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationQueries.cs index 926161c1e..1e039dd47 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationQueries.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationQueries.cs @@ -1,15 +1,19 @@ using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; +using System.Linq; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using GreenDonut.Data; using HotChocolate.Authorization; +using HotChocolate.Data; +using HotChocolate.Resolvers; using HotChocolate.Types; using Metabase.Authorization; +using Metabase.Data; using Metabase.Data.OpenIdConnect; +using Metabase.GraphQl.Extensions; using Metabase.GraphQl.Users; -using OpenIddict.Core; +using Microsoft.EntityFrameworkCore; namespace Metabase.GraphQl.OpenIdConnect.Applications; @@ -17,7 +21,7 @@ namespace Metabase.GraphQl.OpenIdConnect.Applications; public sealed class OpenIdConnectApplicationQueries { [UseUserManager] - [Authorize(Policy = AuthorizationPolicies.ManageOpenIdConnectScopePolicy)] + [Authorize(Policy = AuthorizationPolicies.AuthenticatedPolicy)] public Task GetCurrentOpenIdConnectApplicationAsync( ClaimsPrincipal claimsPrincipal, Authorization.OpenIdConnectAuthorization authorization, @@ -27,55 +31,76 @@ CancellationToken cancellationToken return authorization.SwitchUserOrApplicationAsync( claimsPrincipal, user => Task.FromResult(null), - async application => - { - if (application is not null - && !await authorization.IsAuthorizedToManageApplication(claimsPrincipal, application.Id, cancellationToken) - ) - { - return null; - } - return application; - }, + application => Task.FromResult(application), cancellationToken ); } - // TODO In all queries, instead of returning nothing, report as authentication error to client. - // TODO Make the application manager use the scoped database context. + [UsePaging] + [UseFiltering] + [UseSorting] [UseUserManager] - [Authorize(Policy = AuthorizationPolicies.ManageOpenIdConnectScopePolicy)] - public async IAsyncEnumerable GetOpenIdConnectApplicationsAsync( - OpenIddictApplicationManager applicationManager, + // The database reference implementation uses applications to restrict data + // access and checks if applications exist when restrictions are added. + // These checks should be possible even if the application cannot be + // managed. From a security perspective it may be better to provide a way + // to just check existence without getting any other information. + [Authorize(Policy = AuthorizationPolicies.WriteScopePolicy)] + public ValueTask> GetOpenIdConnectApplicationsAsync( + IResolverContext resolverContext, + ApplicationDbContext databaseContext, ClaimsPrincipal claimsPrincipal, Authorization.OpenIdConnectAuthorization authorization, - [EnumeratorCancellation] CancellationToken cancellationToken + CancellationToken cancellationToken ) { - if (!await authorization.IsAuthorizedToManageOpenIdConnect(claimsPrincipal, cancellationToken)) - { - yield break; - } - await foreach (var application in applicationManager.ListAsync(cancellationToken: cancellationToken)) - { - yield return application; - } + // if (!await authorization.IsAuthorizedToManageOpenIdConnect(claimsPrincipal, cancellationToken)) + // { + // authorization.ReportUnauthorizedError(resolverContext); + // return HotChocolate.Types.Pagination.Connection.Empty(); + // } + return databaseContext.OpenIdConnectApplications + .AsNoTracking() + .With(resolverContext.GetQueryContext(), Sorting.DefaultEntityOrder) + .ToPageAsync(resolverContext.GetPagingArguments(), cancellationToken) + .ToConnectionAsync(); } [UseUserManager] - [Authorize(Policy = AuthorizationPolicies.ManageOpenIdConnectScopePolicy)] - public async Task GetOpenIdConnectApplicationAsync( + [Authorize(Policy = AuthorizationPolicies.WriteScopePolicy)] + public Task GetOpenIdConnectApplicationAsync( Guid id, + IOpenIdConnectApplicationByIdDataLoader byId, + ClaimsPrincipal claimsPrincipal, + Authorization.OpenIdConnectAuthorization authorization, + // IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + // if (!await authorization.IsAuthorizedToManageApplication(claimsPrincipal, id, cancellationToken)) + // { + // authorization.ReportUnauthorizedError(resolverContext); + // return null; + // } + return byId.LoadAsync(id, cancellationToken); + } + + [UseUserManager] + [Authorize(Policy = AuthorizationPolicies.WriteScopePolicy)] + public Task GetOpenIdConnectApplicationByClientIdAsync( + string clientId, + IOpenIdConnectApplicationByClientIdDataLoader byId, ClaimsPrincipal claimsPrincipal, Authorization.OpenIdConnectAuthorization authorization, - OpenIddictApplicationManager applicationManager, + // IResolverContext resolverContext, CancellationToken cancellationToken ) { - if (!await authorization.IsAuthorizedToManageApplication(claimsPrincipal, id, cancellationToken)) - { - return null; - } - return await applicationManager.FindByIdAsync(id.ToString(), cancellationToken: cancellationToken); + // if (!await authorization.IsAuthorizedToManageApplication(claimsPrincipal, clientId, cancellationToken)) + // { + // authorization.ReportUnauthorizedError(resolverContext); + // return null; + // } + return byId.LoadAsync(clientId, cancellationToken); } } \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationSortType.cs b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationSortType.cs new file mode 100644 index 000000000..43290cd62 --- /dev/null +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationSortType.cs @@ -0,0 +1,22 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data.OpenIdConnect; +using Metabase.GraphQl.Entities; + +namespace Metabase.GraphQl.OpenIdConnect.Applications; + +public class OpenIdConnectApplicationSortType + : AuditableEntitySortType +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.Field(_ => _.ApplicationType); + descriptor.Field(_ => _.ClientId); + descriptor.Field(_ => _.ConsentType); + descriptor.Field(_ => _.DisplayName); + // descriptor.Field(_ => _.PostLogoutRedirectUris); + // descriptor.Field(_ => _.RedirectUris); + } +} diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationTokenEdge.cs b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationTokenEdge.cs index 5fb7b9631..1538b902d 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationTokenEdge.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationTokenEdge.cs @@ -2,9 +2,6 @@ namespace Metabase.GraphQl.OpenIdConnect.Applications; -public sealed class OpenIdConnectApplicationTokenEdge( - OpenIdConnectToken node -) -{ - public OpenIdConnectToken Node { get; } = node; -} \ No newline at end of file +public sealed record OpenIdConnectApplicationIssuedTokenEdge( + OpenIdConnectToken Node +); \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationType.cs b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationType.cs index 054286f96..513df715f 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationType.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationType.cs @@ -7,18 +7,16 @@ using System.Threading.Tasks; using HotChocolate; using HotChocolate.Types; -using Metabase.Data; using Metabase.Data.OpenIdConnect; -using Metabase.GraphQl.Extensions; -using Metabase.GraphQl.Institutions; using Metabase.GraphQl.Users; using Metabase.GraphQl.Entities; -using OpenIddict.Core; +using Metabase.GraphQl.Scalars; +using OpenIddict.Abstractions; namespace Metabase.GraphQl.OpenIdConnect.Applications; public sealed class OpenIdConnectApplicationType - : EntityType + : EntityType { protected override void Configure( IObjectTypeDescriptor descriptor @@ -35,6 +33,7 @@ IObjectTypeDescriptor descriptor descriptor .Field(application => application.ClientId) .Type>() + .Cost(0) .Resolve(context => context.Parent().ClientId ?? throw new GraphQLException("Client ID is missing.") @@ -42,6 +41,7 @@ IObjectTypeDescriptor descriptor descriptor .Field(application => application.ConsentType) .Type>>() + .Cost(0) .Resolve(context => context.Parent().ConsentType?.ToOpenIdConnectConsentType() ?? throw new GraphQLException("Consent type is missing.") @@ -52,110 +52,75 @@ IObjectTypeDescriptor descriptor descriptor .Field("endpoints") .Type>>>>() + .Cost(0) .Resolve(context => { var application = context.Parent(); - if (application.Permissions is null) - { - return []; - } - return JsonSerializer.Deserialize>(application.Permissions) - ?.FindAll(permission => - { - try - { - permission.ToOpenIdConnectEndpoint(); - return true; - } - catch (ArgumentOutOfRangeException) - { - return false; - } - }) - ?.Select(endpoint => endpoint.ToOpenIdConnectEndpoint()) - .ToList() ?? []; + return (application.Permissions is null ? [] : JsonSerializer.Deserialize>(application.Permissions)) + ?.PermissionsToOpenIdConnectEndpoints() + ?? []; }); descriptor .Field("grantTypes") .Type>>>>() + .Cost(0) .Resolve(context => { var application = context.Parent(); - if (application.Permissions is null) - { - return []; - } - return JsonSerializer.Deserialize>(application.Permissions) - ?.FindAll(permission => - { - try - { - permission.ToOpenIdConnectGrantType(); - return true; - } - catch (ArgumentOutOfRangeException) - { - return false; - } - }) - ?.Select(grantType => grantType.ToOpenIdConnectGrantType()) - .ToList() ?? []; + return (application.Permissions is null ? [] : JsonSerializer.Deserialize>(application.Permissions)) + ?.PermissionsToOpenIdConnectGrantTypes() + ?? []; }); descriptor .Field("responseTypes") .Type>>>>() + .Cost(0) .Resolve(context => { var application = context.Parent(); - if (application.Permissions is null) - { - return []; - } - return JsonSerializer.Deserialize>(application.Permissions) - ?.FindAll(permission => - { - try - { - permission.ToOpenIdConnectResponseType(); - return true; - } - catch (ArgumentOutOfRangeException) - { - return false; - } - }) - ?.Select(responseType => responseType.ToOpenIdConnectResponseType()) - .ToList() ?? []; + return (application.Permissions is null ? [] : JsonSerializer.Deserialize>(application.Permissions)) + ?.PermissionsToOpenIdConnectResponseTypes() + ?? []; }); descriptor .Field("scopes") .Type>>>>() + .Cost(0) .Resolve(context => { var application = context.Parent(); - if (application.Permissions is null) - { - return []; - } - return JsonSerializer.Deserialize>(application.Permissions) - ?.FindAll(permission => - { - try - { - permission.ToOpenIdConnectScope(); - return true; - } - catch (ArgumentOutOfRangeException) - { - return false; - } - }) - ?.Select(scope => scope.ToOpenIdConnectScope()) - .ToList() ?? []; + return (application.Permissions is null ? [] : JsonSerializer.Deserialize>(application.Permissions)) + ?.PermissionsToOpenIdConnectScopes() + ?? []; + }); + descriptor + .Field("audiences") + .Type>>>() + .Cost(0) + .Resolve(context => + { + var application = context.Parent(); + return (application.Permissions is null ? [] : JsonSerializer.Deserialize>(application.Permissions)) + ?.Where(_ => _.StartsWith(OpenIddictConstants.Permissions.Prefixes.Audience, StringComparison.InvariantCulture)) + .Select(_ => _[OpenIddictConstants.Permissions.Prefixes.Audience.Length..]) + ?? []; + }); + descriptor + .Field("resources") + .Type>>>() + .Cost(0) + .Resolve(context => + { + var application = context.Parent(); + return (application.Permissions is null ? [] : JsonSerializer.Deserialize>(application.Permissions)) + ?.Where(_ => _.StartsWith(OpenIddictConstants.Permissions.Prefixes.Resource, StringComparison.InvariantCulture)) + .Select(_ => _[OpenIddictConstants.Permissions.Prefixes.Resource.Length..]) + ?? []; }); descriptor .Field(application => application.Requirements) .Type>>>>() + .Cost(0) .Resolve(context => { var application = context.Parent(); @@ -165,21 +130,24 @@ IObjectTypeDescriptor descriptor } return JsonSerializer.Deserialize>(application.Requirements) ?.Select(requirement => requirement.ToOpenIdConnectRequirement()) - .ToList() ?? []; + .ToArray() ?? []; }); descriptor .Field(application => application.RedirectUris) .Name("redirectUri") - .Type() + .Type() + .Cost(0) .Resolve(context => ExtractUri(context.Parent().RedirectUris)); descriptor .Field(application => application.PostLogoutRedirectUris) .Name("postLogoutRedirectUri") - .Type() + .Type() + .Cost(0) .Resolve(context => ExtractUri(context.Parent().PostLogoutRedirectUris)); descriptor .Field(application => application.Owner) .Type>>() + .Cost(0) .Resolve(context => new OpenIdConnectApplicationOwnerEdge( context.Parent() @@ -190,23 +158,25 @@ IObjectTypeDescriptor descriptor .Ignore(); descriptor .Field(application => application.Authorizations) - .Type>>() + .Type>>() + .Cost(0) .Resolve(context => - new OpenIdConnectApplicationAuthorizationConnection( + new OpenIdConnectApplicationGrantedAuthorizationConnection( context.Parent() ) ); descriptor .Field(application => application.Tokens) - .Type>>() + .Type>>() .Resolve(context => - new OpenIdConnectApplicationTokenConnection( + new OpenIdConnectApplicationIssuedTokenConnection( context.Parent() ) ); descriptor .Field("isAuthorizedToManageNode") + .Cost(1) .ResolveWith(_ => ApplicationResolvers.IsAuthorizedToManageNodeAsync(default!, default!, default!, default!)) .UseUserManager(); @@ -220,7 +190,7 @@ IObjectTypeDescriptor descriptor } var uris = JsonSerializer.Deserialize>(urisJson) ?? throw new GraphQLException($"Could not deserialize `{urisJson}` into a list of strings."); - if (uris.Count == 0) + if (uris.Count is 0) { return null; } diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectConsentType.cs b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectConsentType.cs index 29bb8bf05..0fe4099b5 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectConsentType.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectConsentType.cs @@ -3,4 +3,7 @@ namespace Metabase.GraphQl.OpenIdConnect.Applications; public enum OpenIdConnectConsentType { EXPLICIT, + EXTERNAL, + IMPLICIT, + SYSTEMATIC, } \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectConsentTypeExtensions.cs b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectConsentTypeExtensions.cs index 9008f1a9f..429e1fd26 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectConsentTypeExtensions.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectConsentTypeExtensions.cs @@ -1,24 +1,33 @@ using System; +using System.Diagnostics.Contracts; using OpenIddict.Abstractions; namespace Metabase.GraphQl.OpenIdConnect.Applications; public static class OpenIdConnectConsentTypeExtensions { + [Pure] public static OpenIdConnectConsentType ToOpenIdConnectConsentType(this string consentType) { return consentType switch { OpenIddictConstants.ConsentTypes.Explicit => OpenIdConnectConsentType.EXPLICIT, + OpenIddictConstants.ConsentTypes.External => OpenIdConnectConsentType.EXTERNAL, + OpenIddictConstants.ConsentTypes.Implicit => OpenIdConnectConsentType.IMPLICIT, + OpenIddictConstants.ConsentTypes.Systematic => OpenIdConnectConsentType.SYSTEMATIC, _ => throw new ArgumentOutOfRangeException(nameof(consentType), $"Unsupported consent type `{consentType}`") }; } + [Pure] public static string ToStringConsentType(this OpenIdConnectConsentType consentType) { return consentType switch { OpenIdConnectConsentType.EXPLICIT => OpenIddictConstants.ConsentTypes.Explicit, + OpenIdConnectConsentType.EXTERNAL => OpenIddictConstants.ConsentTypes.External, + OpenIdConnectConsentType.IMPLICIT => OpenIddictConstants.ConsentTypes.Implicit, + OpenIdConnectConsentType.SYSTEMATIC => OpenIddictConstants.ConsentTypes.Systematic, _ => throw new ArgumentOutOfRangeException(nameof(consentType), $"Unsupported consent type `{consentType}`") }; } diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectEndpointExtensions.cs b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectEndpointExtensions.cs index 98f83179a..fdace1011 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectEndpointExtensions.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectEndpointExtensions.cs @@ -1,14 +1,36 @@ using System; -using Metabase.Configuration; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; using OpenIddict.Abstractions; namespace Metabase.GraphQl.OpenIdConnect.Applications; public static class OpenIdConnectEndpointExtensions { - public static OpenIdConnectEndpoint ToOpenIdConnectEndpoint(this string endpoint) + [Pure] + public static OpenIdConnectEndpoint[] PermissionsToOpenIdConnectEndpoints(this List permissions) { - return endpoint switch + return permissions.FindAll(permission => + { + try + { + var ignore = permission.PermissionToOpenIdConnectEndpoint(); + return true; + } + catch (ArgumentOutOfRangeException) + { + return false; + } + }) + .Select(endpointPermission => endpointPermission.PermissionToOpenIdConnectEndpoint()) + .ToArray(); + } + + [Pure] + public static OpenIdConnectEndpoint PermissionToOpenIdConnectEndpoint(this string endpointPermission) + { + return endpointPermission switch { OpenIddictConstants.Permissions.Endpoints.Authorization => OpenIdConnectEndpoint.AUTHORIZATION, OpenIddictConstants.Permissions.Endpoints.EndSession => OpenIdConnectEndpoint.END_SESSION, @@ -16,11 +38,12 @@ public static OpenIdConnectEndpoint ToOpenIdConnectEndpoint(this string endpoint OpenIddictConstants.Permissions.Endpoints.PushedAuthorization => OpenIdConnectEndpoint.PUSHED_AUTHORIZATION, OpenIddictConstants.Permissions.Endpoints.Revocation => OpenIdConnectEndpoint.REVOCATION, OpenIddictConstants.Permissions.Endpoints.Token => OpenIdConnectEndpoint.TOKEN, - _ => throw new ArgumentOutOfRangeException(nameof(endpoint), $"Unsupported endpoint `{endpoint}`") + _ => throw new ArgumentOutOfRangeException(nameof(endpointPermission), $"Unsupported endpoint `{endpointPermission}`") }; } - public static string ToStringEndpoint(this OpenIdConnectEndpoint endpoint) + [Pure] + public static string ToPermissionString(this OpenIdConnectEndpoint endpoint) { return endpoint switch { diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectGrantTypeExtensions.cs b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectGrantTypeExtensions.cs index bf60919a4..1efe700a9 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectGrantTypeExtensions.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectGrantTypeExtensions.cs @@ -1,24 +1,47 @@ using System; -using Metabase.Configuration; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; using OpenIddict.Abstractions; namespace Metabase.GraphQl.OpenIdConnect.Applications; public static class OpenIdConnectGrantTypeExtensions { - public static OpenIdConnectGrantType ToOpenIdConnectGrantType(this string grantType) + [Pure] + public static OpenIdConnectGrantType[] PermissionsToOpenIdConnectGrantTypes(this List permissions) { - return grantType switch + return permissions.FindAll(permission => + { + try + { + var ignore = permission.PermissionToOpenIdConnectGrantType(); + return true; + } + catch (ArgumentOutOfRangeException) + { + return false; + } + }) + .Select(grantTypePermission => grantTypePermission.PermissionToOpenIdConnectGrantType()) + .ToArray(); + } + + [Pure] + public static OpenIdConnectGrantType PermissionToOpenIdConnectGrantType(this string grantTypePermission) + { + return grantTypePermission switch { OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode => OpenIdConnectGrantType.AUTHORIZATION_CODE, OpenIddictConstants.Permissions.GrantTypes.ClientCredentials => OpenIdConnectGrantType.CLIENT_CREDENTIALS, OpenIddictConstants.Permissions.GrantTypes.RefreshToken => OpenIdConnectGrantType.REFRESH_TOKEN, OpenIddictConstants.Permissions.GrantTypes.TokenExchange => OpenIdConnectGrantType.TOKEN_EXCHANGE, - _ => throw new ArgumentOutOfRangeException(nameof(grantType), $"Unsupported grant type `{grantType}`") + _ => throw new ArgumentOutOfRangeException(nameof(grantTypePermission), $"Unsupported grant type `{grantTypePermission}`") }; } - public static string ToStringGrantType(this OpenIdConnectGrantType grantType) + [Pure] + public static string ToPermissionString(this OpenIdConnectGrantType grantType) { return grantType switch { diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectRequirementExtensions.cs b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectRequirementExtensions.cs index 0ea5271f4..4de4501cd 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectRequirementExtensions.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectRequirementExtensions.cs @@ -1,10 +1,12 @@ using System; +using System.Diagnostics.Contracts; using OpenIddict.Abstractions; namespace Metabase.GraphQl.OpenIdConnect.Applications; public static class OpenIdConnectRequirementExtensions { + [Pure] public static OpenIdConnectRequirement ToOpenIdConnectRequirement(this string requirement) { return requirement switch @@ -15,6 +17,7 @@ public static OpenIdConnectRequirement ToOpenIdConnectRequirement(this string re }; } + [Pure] public static string ToStringRequirement(this OpenIdConnectRequirement requirement) { return requirement switch diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectResponseTypeExtensions.cs b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectResponseTypeExtensions.cs index 097b28c25..d3973f2a6 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectResponseTypeExtensions.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectResponseTypeExtensions.cs @@ -1,23 +1,46 @@ using System; -using Metabase.Configuration; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; using OpenIddict.Abstractions; namespace Metabase.GraphQl.OpenIdConnect.Applications; public static class OpenIdConnectResponseTypeExtensions { - public static OpenIdConnectResponseType ToOpenIdConnectResponseType(this string responseType) + [Pure] + public static OpenIdConnectResponseType[] PermissionsToOpenIdConnectResponseTypes(this List permissions) { - return responseType switch + return permissions.FindAll(permission => + { + try + { + var ignore = permission.PermissionToOpenIdConnectResponseType(); + return true; + } + catch (ArgumentOutOfRangeException) + { + return false; + } + }) + .Select(responseTypePermission => responseTypePermission.PermissionToOpenIdConnectResponseType()) + .ToArray(); + } + + [Pure] + public static OpenIdConnectResponseType PermissionToOpenIdConnectResponseType(this string responseTypePermission) + { + return responseTypePermission switch { OpenIddictConstants.Permissions.ResponseTypes.Code => OpenIdConnectResponseType.CODE, OpenIddictConstants.Permissions.ResponseTypes.IdToken => OpenIdConnectResponseType.ID_TOKEN, OpenIddictConstants.Permissions.ResponseTypes.Token => OpenIdConnectResponseType.TOKEN, - _ => throw new ArgumentOutOfRangeException(nameof(responseType), $"Unsupported response type `{responseType}`") + _ => throw new ArgumentOutOfRangeException(nameof(responseTypePermission), $"Unsupported response type `{responseTypePermission}`") }; } - public static string ToStringResponseType(this OpenIdConnectResponseType responseType) + [Pure] + public static string ToPermissionString(this OpenIdConnectResponseType responseType) { return responseType switch { diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectScope.cs b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectScope.cs deleted file mode 100644 index a2eec4c6e..000000000 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectScope.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Metabase.GraphQl.OpenIdConnect.Applications; - -[SuppressMessage("Naming", "CA1707")] -public enum OpenIdConnectScope -{ - ADDRESS, - EMAIL, - PHONE, - PROFILE, - ROLES, - READ_API, - WRITE_API, - ADMINISTRATE_API, - VERIFY_API, - MANAGE_DATABASE_API, - MANAGE_GNU_PG_API, - MANAGE_INSTITUTION_REPRESENTATIVE_API, - MANAGE_OPEN_ID_CONNECT_API, - MANAGE_USER_API, -} \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectScopeExtensions.cs b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectScopeExtensions.cs deleted file mode 100644 index 6d016a412..000000000 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectScopeExtensions.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using OpenIddict.Abstractions; - -namespace Metabase.GraphQl.OpenIdConnect.Applications; - -public static class OpenIdConnectScopeExtensions -{ - public static OpenIdConnectScope ToOpenIdConnectScope(this string scope) - { - return scope switch - { - OpenIddictConstants.Permissions.Scopes.Address => OpenIdConnectScope.ADDRESS, - OpenIddictConstants.Permissions.Scopes.Email => OpenIdConnectScope.EMAIL, - OpenIddictConstants.Permissions.Scopes.Phone => OpenIdConnectScope.PHONE, - OpenIddictConstants.Permissions.Scopes.Profile => OpenIdConnectScope.PROFILE, - OpenIddictConstants.Permissions.Scopes.Roles => OpenIdConnectScope.ROLES, - OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.ReadApiScope => OpenIdConnectScope.READ_API, - OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.WriteApiScope => OpenIdConnectScope.WRITE_API, - OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.AdministrateApiScope => OpenIdConnectScope.ADMINISTRATE_API, - OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.VerifyApiScope => OpenIdConnectScope.VERIFY_API, - OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.ManageDatabaseApiScope => OpenIdConnectScope.MANAGE_DATABASE_API, - OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.ManageGnuPgApiScope => OpenIdConnectScope.MANAGE_GNU_PG_API, - OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.ManageInstitutionRepresentativeApiScope => OpenIdConnectScope.MANAGE_INSTITUTION_REPRESENTATIVE_API, - OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.ManageOpenIdConnectApiScope => OpenIdConnectScope.MANAGE_OPEN_ID_CONNECT_API, - OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.ManageUserApiScope => OpenIdConnectScope.MANAGE_USER_API, - _ => throw new ArgumentOutOfRangeException(nameof(scope), $"Unsupported scope `{scope}`") - }; - } - - public static string ToStringScope(this OpenIdConnectScope scope) - { - return scope switch - { - OpenIdConnectScope.ADDRESS => OpenIddictConstants.Permissions.Scopes.Address, - OpenIdConnectScope.EMAIL => OpenIddictConstants.Permissions.Scopes.Email, - OpenIdConnectScope.PHONE => OpenIddictConstants.Permissions.Scopes.Phone, - OpenIdConnectScope.PROFILE => OpenIddictConstants.Permissions.Scopes.Profile, - OpenIdConnectScope.ROLES => OpenIddictConstants.Permissions.Scopes.Roles, - OpenIdConnectScope.READ_API => OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.ReadApiScope, - OpenIdConnectScope.WRITE_API => OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.WriteApiScope, - OpenIdConnectScope.ADMINISTRATE_API => OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.AdministrateApiScope, - OpenIdConnectScope.VERIFY_API => OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.VerifyApiScope, - OpenIdConnectScope.MANAGE_DATABASE_API => OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.ManageDatabaseApiScope, - OpenIdConnectScope.MANAGE_GNU_PG_API => OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.ManageGnuPgApiScope, - OpenIdConnectScope.MANAGE_INSTITUTION_REPRESENTATIVE_API => OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.ManageInstitutionRepresentativeApiScope, - OpenIdConnectScope.MANAGE_OPEN_ID_CONNECT_API => OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.ManageOpenIdConnectApiScope, - OpenIdConnectScope.MANAGE_USER_API => OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.ManageUserApiScope, - _ => throw new ArgumentOutOfRangeException(nameof(scope), $"Unsupported scope `{scope}`") - }; - } -} \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/UpdateOpenIdConnectApplicationErrorCode.cs b/backend/src/GraphQl/OpenIdConnect/Applications/UpdateOpenIdConnectApplicationErrorCode.cs index 2870acf6f..c6050e706 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/UpdateOpenIdConnectApplicationErrorCode.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/UpdateOpenIdConnectApplicationErrorCode.cs @@ -7,5 +7,10 @@ public enum UpdateOpenIdConnectApplicationErrorCode { UNKNOWN, UNAUTHORIZED, - UNKNOWN_APPLICATION + UNKNOWN_APPLICATION, + ILLEGAL_CONSENT_TYPE, + ILLEGAL_GRANT_TYPE, + ILLEGAL_SCOPE, + ILLEGAL_RESPONSE_TYPE, + ILLEGAL_ENDPOINT } \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/UpdateOpenIdConnectApplicationInput.cs b/backend/src/GraphQl/OpenIdConnect/Applications/UpdateOpenIdConnectApplicationInput.cs index 4b2408a22..feca34294 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/UpdateOpenIdConnectApplicationInput.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/UpdateOpenIdConnectApplicationInput.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Metabase.Enumerations; namespace Metabase.GraphQl.OpenIdConnect.Applications; diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/UpdateOpenIdConnectApplicationPayload.cs b/backend/src/GraphQl/OpenIdConnect/Applications/UpdateOpenIdConnectApplicationPayload.cs index bec1d977f..27c5d6184 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/UpdateOpenIdConnectApplicationPayload.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/UpdateOpenIdConnectApplicationPayload.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Metabase.Data.OpenIdConnect; namespace Metabase.GraphQl.OpenIdConnect.Applications; @@ -18,4 +19,11 @@ UpdateOpenIdConnectApplicationError error : base(error) { } + + public UpdateOpenIdConnectApplicationPayload( + IReadOnlyList errors + ) + : base(errors) + { + } } \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Authorizations/DeleteOpenIdConnectAuthorizationPayload.cs b/backend/src/GraphQl/OpenIdConnect/Authorizations/DeleteOpenIdConnectAuthorizationPayload.cs index cf11882cc..ce6e14807 100644 --- a/backend/src/GraphQl/OpenIdConnect/Authorizations/DeleteOpenIdConnectAuthorizationPayload.cs +++ b/backend/src/GraphQl/OpenIdConnect/Authorizations/DeleteOpenIdConnectAuthorizationPayload.cs @@ -1,11 +1,13 @@ using System.Collections.Generic; +using Metabase.Data.OpenIdConnect; namespace Metabase.GraphQl.OpenIdConnect.Authorizations; public sealed class DeleteOpenIdConnectAuthorizationPayload { - public DeleteOpenIdConnectAuthorizationPayload() + public DeleteOpenIdConnectAuthorizationPayload(OpenIdConnectApplication? application) { + Application = application; } public DeleteOpenIdConnectAuthorizationPayload( @@ -15,5 +17,6 @@ DeleteOpenIdConnectAuthorizationError error Errors = [error]; } + public OpenIdConnectApplication? Application { get; } public IReadOnlyCollection? Errors { get; } } \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationByIdDataLoader.cs b/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationByIdDataLoader.cs deleted file mode 100644 index 4dc32f489..000000000 --- a/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationByIdDataLoader.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using GreenDonut; -using Metabase.Data.OpenIdConnect; -using OpenIddict.Core; - -namespace Metabase.GraphQl.OpenIdConnect.Authorizations; - -public sealed class OpenIdConnectAuthorizationByIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - OpenIddictAuthorizationManager authorizationManager) -: BatchDataLoader(batchScheduler, options) -{ - protected override async Task> LoadBatchAsync(IReadOnlyList keys, CancellationToken cancellationToken) - { - var ret = new Dictionary(); - foreach (var key in keys) - { - ret.Add(key, await authorizationManager.FindByIdAsync(key.ToString(), cancellationToken: cancellationToken)); - } - return ret; - } -} \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationDataLoaders.cs b/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationDataLoaders.cs new file mode 100644 index 000000000..f0fc4628d --- /dev/null +++ b/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationDataLoaders.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut; +using GreenDonut.Data; +using Metabase.Data; +using Metabase.Data.OpenIdConnect; +using Microsoft.EntityFrameworkCore; + +namespace Metabase.GraphQl.OpenIdConnect.Authorizations; + +public sealed class OpenIdConnectAuthorizationDataLoaders +: DataLoaders +{ + [DataLoader] + public static ValueTask> GetOpenIdConnectAuthorizationByIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetEntityByIdAsync( + ids, + databaseContext => databaseContext.OpenIdConnectAuthorizations, + queryContext, + databaseContextFactory, + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationFilterType.cs b/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationFilterType.cs index 174f46db1..4ff55557d 100644 --- a/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationFilterType.cs +++ b/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationFilterType.cs @@ -5,7 +5,7 @@ namespace Metabase.GraphQl.OpenIdConnect.Authorizations; public sealed class OpenIdConnectAuthorizationFilterType - : EntityFilterType + : AuditableEntityFilterType { protected override void Configure( IFilterInputTypeDescriptor descriptor @@ -13,11 +13,15 @@ IFilterInputTypeDescriptor descriptor { base.Configure(descriptor); descriptor.Name(nameof(OpenIdConnectAuthorizationFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); - descriptor.Field(x => x.CreationDate); - descriptor.Field(x => x.Status); - descriptor.Field(x => x.Subject); - descriptor.Field(x => x.Tokens); - descriptor.Field(x => x.Type); - // descriptor.Field(x => x.Application); + // TODO Remove Id, CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(_ => _.Id); + descriptor.Field(_ => _.CreatedAt); + descriptor.Field(_ => _.UpdatedAt); + descriptor.Field(_ => _.CreationDate).Ignore(); + descriptor.Field(_ => _.Status); + descriptor.Field(_ => _.Subject); + descriptor.Field(_ => _.Tokens); + descriptor.Field(_ => _.Type); + descriptor.Field(_ => _.Application); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationTokenConnection.cs b/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationIssuedTokenConnection.cs similarity index 60% rename from backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationTokenConnection.cs rename to backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationIssuedTokenConnection.cs index 7375b6a7a..03fb2d5ac 100644 --- a/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationTokenConnection.cs +++ b/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationIssuedTokenConnection.cs @@ -4,45 +4,46 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using HotChocolate.CostAnalysis.Types; using Metabase.Data.OpenIdConnect; using OpenIddict.Core; namespace Metabase.GraphQl.OpenIdConnect.Authorizations; -public sealed class OpenIdConnectAuthorizationTokenConnection( +public sealed class OpenIdConnectAuthorizationIssuedTokenConnection( OpenIdConnectAuthorization authorization ) { - public async Task GetTotalCountAsync( + [Cost(0)] + public async Task GetTotalCountAsync( ClaimsPrincipal claimsPrincipal, Authorization.OpenIdConnectAuthorization auth, - OpenIddictAuthorizationManager authorizationManager, OpenIddictTokenManager tokenManager, CancellationToken cancellationToken ) { - if (!await auth.IsAuthorizedToManageTokensOfAuthorization(claimsPrincipal, authorization.Id, authorizationManager, cancellationToken)) + if (!await auth.IsAuthorizedToManageTokensOfAuthorization(claimsPrincipal, authorization.Id, cancellationToken)) { return 0; } - return (uint)await tokenManager.FindByAuthorizationIdAsync(authorization.Id.ToString(), cancellationToken).CountAsync(cancellationToken); + return await tokenManager.FindByAuthorizationIdAsync(authorization.Id.ToString(), cancellationToken).CountAsync(cancellationToken); } - public async IAsyncEnumerable GetEdgesAsync( + [Cost(0)] + public async IAsyncEnumerable GetEdgesAsync( ClaimsPrincipal claimsPrincipal, Authorization.OpenIdConnectAuthorization auth, - OpenIddictAuthorizationManager authorizationManager, OpenIddictTokenManager tokenManager, [EnumeratorCancellation] CancellationToken cancellationToken ) { - if (!await auth.IsAuthorizedToManageTokensOfAuthorization(claimsPrincipal, authorization.Id, authorizationManager, cancellationToken)) + if (!await auth.IsAuthorizedToManageTokensOfAuthorization(claimsPrincipal, authorization.Id, cancellationToken)) { yield break; } await foreach (var token in tokenManager.FindByAuthorizationIdAsync(authorization.Id.ToString(), cancellationToken)) { - yield return new OpenIdConnectAuthorizationTokenEdge(token); + yield return new OpenIdConnectAuthorizationIssuedTokenEdge(token); } } } \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationMutations.cs b/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationMutations.cs index 6d991c2a7..a4c88e8d1 100644 --- a/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationMutations.cs +++ b/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationMutations.cs @@ -26,7 +26,6 @@ CancellationToken cancellationToken if (!await openIdConnectAuthorization.IsAuthorizedToManageAuthorization( claimsPrincipal, input.AuthorizationId, - authorizationManager, cancellationToken ) ) @@ -51,6 +50,6 @@ CancellationToken cancellationToken ); } await authorizationManager.DeleteAsync(authorization, cancellationToken); - return new DeleteOpenIdConnectAuthorizationPayload(); + return new DeleteOpenIdConnectAuthorizationPayload(authorization.Application); } } \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationQueries.cs b/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationQueries.cs index 8467835b4..86f2c77ed 100644 --- a/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationQueries.cs +++ b/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationQueries.cs @@ -1,53 +1,65 @@ using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; +using System.Linq; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using GreenDonut.Data; using HotChocolate.Authorization; +using HotChocolate.Data; +using HotChocolate.Resolvers; using HotChocolate.Types; +using Metabase.Data; using Metabase.Data.OpenIdConnect; +using Metabase.GraphQl.Extensions; using Metabase.GraphQl.Users; -using OpenIddict.Core; +using Microsoft.EntityFrameworkCore; namespace Metabase.GraphQl.OpenIdConnect.Authorizations; [ExtendObjectType(nameof(Query))] public sealed class OpenIdConnectAuthorizationQueries { + [UsePaging] + [UseFiltering] + [UseSorting] [UseUserManager] [Authorize(Policy = Authorization.AuthorizationPolicies.ManageOpenIdConnectScopePolicy)] - public async IAsyncEnumerable GetOpenIdConnectAuthorizationsAsync( + public async ValueTask> GetOpenIdConnectAuthorizationsAsync( + IResolverContext resolverContext, + ApplicationDbContext databaseContext, ClaimsPrincipal claimsPrincipal, - Authorization.OpenIdConnectAuthorization authorization, // TODO Make the authorization manager use the scoped database context. - OpenIddictAuthorizationManager authorizationManager, - [EnumeratorCancellation] CancellationToken cancellationToken + Authorization.OpenIdConnectAuthorization authorization, + CancellationToken cancellationToken ) { if (!await authorization.IsAuthorizedToManageOpenIdConnect(claimsPrincipal, cancellationToken)) { - yield break; - } - await foreach (var auth in authorizationManager.ListAsync(cancellationToken: cancellationToken)) - { - yield return auth; + authorization.ReportUnauthorizedError(resolverContext); + return HotChocolate.Types.Pagination.Connection.Empty(); } + return await databaseContext.OpenIdConnectAuthorizations + .AsNoTracking() + .With(resolverContext.GetQueryContext(), Sorting.DefaultEntityOrder) + .ToPageAsync(resolverContext.GetPagingArguments(), cancellationToken) + .ToConnectionAsync(); } [UseUserManager] [Authorize(Policy = Authorization.AuthorizationPolicies.ManageOpenIdConnectScopePolicy)] - public async Task GetOpenIdConnectAuthorization( + public async Task GetOpenIdConnectAuthorizationAsync( Guid id, + IOpenIdConnectAuthorizationByIdDataLoader byId, ClaimsPrincipal claimsPrincipal, Authorization.OpenIdConnectAuthorization authorization, - OpenIddictAuthorizationManager authorizationManager, + IResolverContext resolverContext, CancellationToken cancellationToken ) { - if (!await authorization.IsAuthorizedToManageAuthorization(claimsPrincipal, id, authorizationManager, cancellationToken)) + if (!await authorization.IsAuthorizedToManageAuthorization(claimsPrincipal, id, cancellationToken)) { + authorization.ReportUnauthorizedError(resolverContext); return null; } - return await authorizationManager.FindByIdAsync(id.ToString(), cancellationToken: cancellationToken); + return await byId.LoadAsync(id, cancellationToken); } } \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationSortType.cs b/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationSortType.cs new file mode 100644 index 000000000..940eddce1 --- /dev/null +++ b/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationSortType.cs @@ -0,0 +1,21 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data.OpenIdConnect; +using Metabase.GraphQl.Entities; + +namespace Metabase.GraphQl.OpenIdConnect.Authorizations; + +public sealed class OpenIdConnectAuthorizationSortType + : AuditableEntitySortType +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.Name(nameof(OpenIdConnectAuthorizationSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); + descriptor.Field(_ => _.CreationDate).Ignore(); + descriptor.Field(_ => _.Status); + descriptor.Field(_ => _.Subject); + descriptor.Field(_ => _.Type); + } +} diff --git a/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationTokenEdge.cs b/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationTokenEdge.cs index e291e9e16..b30cf88f3 100644 --- a/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationTokenEdge.cs +++ b/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationTokenEdge.cs @@ -2,9 +2,6 @@ namespace Metabase.GraphQl.OpenIdConnect.Authorizations; -public sealed class OpenIdConnectAuthorizationTokenEdge( - OpenIdConnectToken node -) -{ - public OpenIdConnectToken Node { get; } = node; -} \ No newline at end of file +public sealed record OpenIdConnectAuthorizationIssuedTokenEdge( + OpenIdConnectToken Node +); \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationType.cs b/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationType.cs index a4d686e30..d93571261 100644 --- a/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationType.cs +++ b/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationType.cs @@ -6,12 +6,16 @@ using Metabase.Data.OpenIdConnect; using Metabase.GraphQl.Users; using Metabase.GraphQl.Entities; -using OpenIddict.Core; +using System.Text.Json; +using System.Collections.Generic; +using System.Linq; +using Metabase.Data; +using Microsoft.EntityFrameworkCore; namespace Metabase.GraphQl.OpenIdConnect.Authorizations; public sealed class OpenIdConnectAuthorizationType - : EntityType + : EntityType { protected override void Configure( IObjectTypeDescriptor descriptor @@ -20,42 +24,84 @@ IObjectTypeDescriptor descriptor base.Configure(descriptor); descriptor.Field(authorization => authorization.ConcurrencyToken).Ignore(); descriptor.Field(authorization => authorization.Properties).Ignore(); - descriptor.Field(authorization => authorization.Scopes).Ignore(); + descriptor.Field(authorization => authorization.CreationDate).Ignore(); // use `CreatedAt` instead + descriptor + .Field(_ => _.Subject) + .Type>() + .Cost(1) + .ResolveWith(x => + AuthorizationResolvers.GetSubjectAsync(default!, default!, default!) + ); + descriptor + .Field(_ => _.Scopes) + .Type>>>>() + .Cost(0) + .Resolve(context => + { + var authorization = context.Parent(); + if (authorization.Scopes is null) + { + return []; + } + return JsonSerializer.Deserialize>(authorization.Scopes) + ?.Select(scope => scope.ToOpenIdConnectScope()) + .ToList() ?? []; + }); descriptor .Field(t => t.Application) - .Type>>() + .Type>() .Resolve(context => - new OpenIdConnectAuthorizationApplicationEdge( - context.Parent().Application! - ) - ); + { + // auto-included in `ApplicationDbContext` + var application = context.Parent().Application; + return application is null + ? null + : new OpenIdConnectAuthorizationApplicationEdge(application); + }); descriptor .Field(authorization => authorization.Tokens) - .Type>>() + .Type>>() .Resolve(context => - new OpenIdConnectAuthorizationTokenConnection( + new OpenIdConnectAuthorizationIssuedTokenConnection( context.Parent() ) ); descriptor - .Field("isAuthorizedToDeleteNode") - .ResolveWith(x => - AuthorizationResolvers.IsAuthorizedToDeleteNodeAsync(default!, default!, default!, default!, default!)) - .UseUserManager(); + .Field("isAuthorizedToDeleteNode") + .Cost(1) + .ResolveWith(x => + AuthorizationResolvers.IsAuthorizedToDeleteNodeAsync(default!, default!, default!, default!) + ) + .UseUserManager(); } private sealed class AuthorizationResolvers { + public static Task GetSubjectAsync( + [Parent] OpenIdConnectAuthorization authorization, + ApplicationDbContext databaseContext, + CancellationToken cancellationToken + ) + { + return IOpenIdConnectSubject.SwitchSubjectAsync( + authorization.Subject, + async (userId) => + await databaseContext.Users.AsNoTracking() + .SingleOrDefaultAsync(_ => _.Id == userId, cancellationToken), + async (clientId) => null, + async () => null + ); + } + public static Task IsAuthorizedToDeleteNodeAsync( [Parent] OpenIdConnectAuthorization authorization, ClaimsPrincipal claimsPrincipal, Authorization.OpenIdConnectAuthorization openIdConnectAuthorization, - OpenIddictAuthorizationManager authorizationManager, CancellationToken cancellationToken ) { - return openIdConnectAuthorization.IsAuthorizedToManageAuthorization(claimsPrincipal, authorization.Id, authorizationManager, cancellationToken); + return openIdConnectAuthorization.IsAuthorizedToManageAuthorization(claimsPrincipal, authorization.Id, cancellationToken); } } } \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/OpenIdConnectScope.cs b/backend/src/GraphQl/OpenIdConnect/OpenIdConnectScope.cs new file mode 100644 index 000000000..105a7e549 --- /dev/null +++ b/backend/src/GraphQl/OpenIdConnect/OpenIdConnectScope.cs @@ -0,0 +1,61 @@ +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; + +namespace Metabase.GraphQl.OpenIdConnect; + +[SuppressMessage("Naming", "CA1707")] +public enum OpenIdConnectScope +{ + // The descriptions are used through `EnumExtensions.GetDescription` in the view `Authorize.cshtml`. + + [Display(Description = "Access to your postal address information.")] + ADDRESS, + + [Display(Description = "Access to your email address.")] + EMAIL, + + [Display(Description = "Access to your phone number.")] + PHONE, + + [Display(Description = "Access to your basic profile, for example, name, gender, picture, and so forth.")] + PROFILE, + + [Display(Description = "Request a long-lived refresh token so the application can continue interacting with APIs after the original access token expired.")] + OFFLINE_ACCESS, + + [Display(Description = "Request an identity token so the application can know who you are.")] + OPEN_ID, + + [Display(Description = "Access to your user roles.")] + ROLES, + + [Display(Description = "Read-only access to resources in your name.")] + READ_API, + + [Display(Description = "Basic write access to resources in your name.")] + WRITE_API, + + [Display(Description = "Permission to perform administrative tasks in your name.")] + ADMINISTRATE_API, + + [Display(Description = "Permission to perform verification tasks in your name.")] + VERIFY_API, + + [Display(Description = "Permission to perform customer support tasks in your name.")] + SUPPORT_API, + + [Display(Description = "Authorization to manage databases in your name.")] + MANAGE_DATABASE_API, + + [Display(Description = "Authorization to manage GnuPG keys in your name.")] + MANAGE_GNU_PG_API, + + [Display(Description = "Authorization to manage institution representatives in your name.")] + MANAGE_INSTITUTION_REPRESENTATIVE_API, + + [Display(Description = "Authorization to manage OpenID Connect settings in your name.")] + MANAGE_OPEN_ID_CONNECT_API, + + [Display(Description = "Authorization to manage your user account.")] + MANAGE_USER_API, +} \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/OpenIdConnectScopeExtensions.cs b/backend/src/GraphQl/OpenIdConnect/OpenIdConnectScopeExtensions.cs new file mode 100644 index 000000000..887a649b7 --- /dev/null +++ b/backend/src/GraphQl/OpenIdConnect/OpenIdConnectScopeExtensions.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using Metabase.Extensions; +using Metabase.GraphQl.OpenIdConnect.Applications; +using OpenIddict.Abstractions; + +namespace Metabase.GraphQl.OpenIdConnect; + +public static class OpenIdConnectScopeExtensions +{ + [Pure] + public static OpenIdConnectScope ToOpenIdConnectScope(this string scope) + { + return scope switch + { + OpenIddictConstants.Scopes.Address => OpenIdConnectScope.ADDRESS, + OpenIddictConstants.Scopes.Email => OpenIdConnectScope.EMAIL, + OpenIddictConstants.Scopes.Phone => OpenIdConnectScope.PHONE, + OpenIddictConstants.Scopes.Profile => OpenIdConnectScope.PROFILE, + OpenIddictConstants.Scopes.OfflineAccess => OpenIdConnectScope.OFFLINE_ACCESS, + OpenIddictConstants.Scopes.OpenId => OpenIdConnectScope.OPEN_ID, + OpenIddictConstants.Scopes.Roles => OpenIdConnectScope.ROLES, + Data.OpenIdConnect.OpenIdConnectScope.ReadApiScope => OpenIdConnectScope.READ_API, + Data.OpenIdConnect.OpenIdConnectScope.WriteApiScope => OpenIdConnectScope.WRITE_API, + Data.OpenIdConnect.OpenIdConnectScope.AdministrateApiScope => OpenIdConnectScope.ADMINISTRATE_API, + Data.OpenIdConnect.OpenIdConnectScope.VerifyApiScope => OpenIdConnectScope.VERIFY_API, + Data.OpenIdConnect.OpenIdConnectScope.SupportApiScope => OpenIdConnectScope.SUPPORT_API, + Data.OpenIdConnect.OpenIdConnectScope.ManageDatabaseApiScope => OpenIdConnectScope.MANAGE_DATABASE_API, + Data.OpenIdConnect.OpenIdConnectScope.ManageGnuPgApiScope => OpenIdConnectScope.MANAGE_GNU_PG_API, + Data.OpenIdConnect.OpenIdConnectScope.ManageInstitutionRepresentativeApiScope => OpenIdConnectScope.MANAGE_INSTITUTION_REPRESENTATIVE_API, + Data.OpenIdConnect.OpenIdConnectScope.ManageOpenIdConnectApiScope => OpenIdConnectScope.MANAGE_OPEN_ID_CONNECT_API, + Data.OpenIdConnect.OpenIdConnectScope.ManageUserApiScope => OpenIdConnectScope.MANAGE_USER_API, + _ => throw new ArgumentOutOfRangeException(nameof(scope), $"Unsupported scope `{scope}`") + }; + } + + [Pure] + public static OpenIdConnectScope[] PermissionsToOpenIdConnectScopes(this List permissions) + { + var grantTypes = permissions.PermissionsToOpenIdConnectGrantTypes(); + return permissions.FindAll(permission => + { + try + { + var ignore = permission.PermissionToOpenIdConnectScope(); + return true; + } + catch (ArgumentOutOfRangeException) + { + return false; + } + }) + .Select(scopePermission => scopePermission.PermissionToOpenIdConnectScope()) + .If( + grantTypes.Contains(OpenIdConnectGrantType.REFRESH_TOKEN), + _ => _.Prepend(OpenIdConnectScope.OFFLINE_ACCESS) + ) + .Prepend(OpenIdConnectScope.OPEN_ID) + .ToArray(); + } + + [Pure] + public static OpenIdConnectScope PermissionToOpenIdConnectScope(this string scopePermission) + { + return scopePermission switch + { + OpenIddictConstants.Permissions.Scopes.Address => OpenIdConnectScope.ADDRESS, + OpenIddictConstants.Permissions.Scopes.Email => OpenIdConnectScope.EMAIL, + OpenIddictConstants.Permissions.Scopes.Phone => OpenIdConnectScope.PHONE, + OpenIddictConstants.Permissions.Scopes.Profile => OpenIdConnectScope.PROFILE, + // The openid and offline_access scopes are special-cased by + // OpenIddict and don't require explicit permissions according to + // https://documentation.openiddict.com/configuration/application-permissions#scope-permissions + // ... => OpenIdConnectScope.OFFLINE_ACCESS, + // ... => OpenIdConnectScope.OPEN_ID, + OpenIddictConstants.Permissions.Scopes.Roles => OpenIdConnectScope.ROLES, + OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.ReadApiScope => OpenIdConnectScope.READ_API, + OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.WriteApiScope => OpenIdConnectScope.WRITE_API, + OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.AdministrateApiScope => OpenIdConnectScope.ADMINISTRATE_API, + OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.VerifyApiScope => OpenIdConnectScope.VERIFY_API, + OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.SupportApiScope => OpenIdConnectScope.SUPPORT_API, + OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.ManageDatabaseApiScope => OpenIdConnectScope.MANAGE_DATABASE_API, + OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.ManageGnuPgApiScope => OpenIdConnectScope.MANAGE_GNU_PG_API, + OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.ManageInstitutionRepresentativeApiScope => OpenIdConnectScope.MANAGE_INSTITUTION_REPRESENTATIVE_API, + OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.ManageOpenIdConnectApiScope => OpenIdConnectScope.MANAGE_OPEN_ID_CONNECT_API, + OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.ManageUserApiScope => OpenIdConnectScope.MANAGE_USER_API, + _ => throw new ArgumentOutOfRangeException(nameof(scopePermission), $"Unsupported scope `{scopePermission}`") + }; + } + + [Pure] + public static IEnumerable ToPermissionStrings(this IEnumerable scopes) + { + return scopes + // The openid and offline_access scopes are special-cased by + // OpenIddict and don't require explicit permissions according to + // https://documentation.openiddict.com/configuration/application-permissions#scope-permissions + .Where(_ => _ is not OpenIdConnectScope.OFFLINE_ACCESS && _ is not OpenIdConnectScope.OPEN_ID) + .Select(_ => _.ToPermissionString()); + } + + [Pure] + private static string ToPermissionString(this OpenIdConnectScope scope) + { + return scope switch + { + OpenIdConnectScope.ADDRESS => OpenIddictConstants.Permissions.Scopes.Address, + OpenIdConnectScope.EMAIL => OpenIddictConstants.Permissions.Scopes.Email, + OpenIdConnectScope.PHONE => OpenIddictConstants.Permissions.Scopes.Phone, + OpenIdConnectScope.PROFILE => OpenIddictConstants.Permissions.Scopes.Profile, + // The openid and offline_access scopes are special-cased by + // OpenIddict and don't require explicit permissions according to + // https://documentation.openiddict.com/configuration/application-permissions#scope-permissions + // OpenIdConnectScope.OFFLINE_ACCESS => ..., + // OpenIdConnectScope.OPEN_ID => ..., + OpenIdConnectScope.ROLES => OpenIddictConstants.Permissions.Scopes.Roles, + OpenIdConnectScope.READ_API => OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.ReadApiScope, + OpenIdConnectScope.WRITE_API => OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.WriteApiScope, + OpenIdConnectScope.ADMINISTRATE_API => OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.AdministrateApiScope, + OpenIdConnectScope.VERIFY_API => OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.VerifyApiScope, + OpenIdConnectScope.SUPPORT_API => OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.SupportApiScope, + OpenIdConnectScope.MANAGE_DATABASE_API => OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.ManageDatabaseApiScope, + OpenIdConnectScope.MANAGE_GNU_PG_API => OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.ManageGnuPgApiScope, + OpenIdConnectScope.MANAGE_INSTITUTION_REPRESENTATIVE_API => OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.ManageInstitutionRepresentativeApiScope, + OpenIdConnectScope.MANAGE_OPEN_ID_CONNECT_API => OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.ManageOpenIdConnectApiScope, + OpenIdConnectScope.MANAGE_USER_API => OpenIddictConstants.Permissions.Prefixes.Scope + Data.OpenIdConnect.OpenIdConnectScope.ManageUserApiScope, + _ => throw new ArgumentOutOfRangeException(nameof(scope), $"Unsupported scope `{scope}`") + }; + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/OpenIdConnectSubjectType.cs b/backend/src/GraphQl/OpenIdConnect/OpenIdConnectSubjectType.cs new file mode 100644 index 000000000..0290185a0 --- /dev/null +++ b/backend/src/GraphQl/OpenIdConnect/OpenIdConnectSubjectType.cs @@ -0,0 +1,13 @@ +using HotChocolate.Types; +using Metabase.Data.OpenIdConnect; + +namespace Metabase.GraphQl.OpenIdConnect; + +public sealed class OpenIdConnectSubjectType + : UnionType +{ + protected override void Configure(IUnionTypeDescriptor descriptor) + { + descriptor.Name(nameof(IOpenIdConnectSubject)[1..]); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenByIdDataLoader.cs b/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenByIdDataLoader.cs deleted file mode 100644 index 7f6d93a48..000000000 --- a/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenByIdDataLoader.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using GreenDonut; -using Metabase.Data.OpenIdConnect; -using OpenIddict.Core; - -namespace Metabase.GraphQl.OpenIdConnect.Tokens; - -public sealed class OpenIdConnectTokenByIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - OpenIddictTokenManager tokenManager) -: BatchDataLoader(batchScheduler, options) -{ - protected override async Task> LoadBatchAsync(IReadOnlyList keys, CancellationToken cancellationToken) - { - var ret = new Dictionary(); - foreach (var key in keys) - { - ret.Add(key, await tokenManager.FindByIdAsync(key.ToString(), cancellationToken: cancellationToken)); - } - return ret; - } -} \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenDataLoaders.cs b/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenDataLoaders.cs new file mode 100644 index 000000000..a32a4e044 --- /dev/null +++ b/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenDataLoaders.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut; +using GreenDonut.Data; +using Metabase.Data; +using Metabase.Data.OpenIdConnect; +using Microsoft.EntityFrameworkCore; + +namespace Metabase.GraphQl.OpenIdConnect.Tokens; + +public sealed class OpenIdConnectTokenDataLoaders +: DataLoaders +{ + [DataLoader] + public static ValueTask> GetOpenIdConnectTokenByIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetEntityByIdAsync( + ids, + databaseContext => databaseContext.OpenIdConnectTokens, + queryContext, + databaseContextFactory, + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenFilterType.cs b/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenFilterType.cs index d9dc1d45b..d3c45fb59 100644 --- a/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenFilterType.cs +++ b/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenFilterType.cs @@ -5,7 +5,7 @@ namespace Metabase.GraphQl.OpenIdConnect.Tokens; public sealed class OpenIdConnectTokenFilterType - : EntityFilterType + : AuditableEntityFilterType { protected override void Configure( IFilterInputTypeDescriptor descriptor @@ -13,13 +13,17 @@ IFilterInputTypeDescriptor descriptor { base.Configure(descriptor); descriptor.Name(nameof(OpenIdConnectTokenFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); - descriptor.Field(x => x.CreationDate); - descriptor.Field(x => x.ExpirationDate); - descriptor.Field(x => x.RedemptionDate); - descriptor.Field(x => x.Status); - descriptor.Field(x => x.Subject); - descriptor.Field(x => x.Type); - // descriptor.Field(x => x.Authorization); - // descriptor.Field(x => x.Application); + // TODO Remove Id, CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(_ => _.Id); + descriptor.Field(_ => _.CreatedAt); + descriptor.Field(_ => _.UpdatedAt); + descriptor.Field(_ => _.CreationDate).Ignore(); + descriptor.Field(_ => _.ExpirationDate).Name(OpenIdConnectTokenType.ExpiredAtName); + descriptor.Field(_ => _.RedemptionDate).Name(OpenIdConnectTokenType.RedeemedAtName); + descriptor.Field(_ => _.Status); + descriptor.Field(_ => _.Subject); + descriptor.Field(_ => _.Type); + descriptor.Field(_ => _.Authorization); + descriptor.Field(_ => _.Application); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenMutations.cs b/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenMutations.cs index da03bb159..75fffe4e2 100644 --- a/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenMutations.cs +++ b/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenMutations.cs @@ -27,7 +27,6 @@ CancellationToken cancellationToken if (!await authorization.IsAuthorizedToManageToken( claimsPrincipal, input.TokenId, - tokenManager, cancellationToken )) { diff --git a/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenQueries.cs b/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenQueries.cs index 02cd4804f..293f37299 100644 --- a/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenQueries.cs +++ b/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenQueries.cs @@ -1,55 +1,76 @@ using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; +using System.Linq; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using GreenDonut.Data; using HotChocolate.Authorization; +using HotChocolate.Data; +using HotChocolate.Resolvers; using HotChocolate.Types; using Metabase.Authorization; +using Metabase.Data; using Metabase.Data.OpenIdConnect; +using Metabase.GraphQl.Extensions; using Metabase.GraphQl.Users; -using OpenIddict.Core; +using Microsoft.EntityFrameworkCore; +using OpenIddict.Abstractions; namespace Metabase.GraphQl.OpenIdConnect.Tokens; [ExtendObjectType(nameof(Query))] public sealed class OpenIdConnectTokenQueries { + [Authorize(Policy = AuthorizationPolicies.AuthenticatedPolicy)] + public string? GetCurrentOpenIdConnectTokenClientId( + ClaimsPrincipal claimsPrincipal + ) + { + return claimsPrincipal.GetClaim(OpenIddictConstants.Claims.ClientId) + ?? claimsPrincipal.GetClaim(OpenIddictConstants.Claims.AuthorizedParty); + } + + [UsePaging] + [UseFiltering] + [UseSorting] [UseUserManager] [Authorize(Policy = AuthorizationPolicies.ManageOpenIdConnectScopePolicy)] - public async IAsyncEnumerable GetOpenIdConnectTokensAsync( + public async ValueTask> GetOpenIdConnectTokensAsync( + IResolverContext resolverContext, + ApplicationDbContext databaseContext, ClaimsPrincipal claimsPrincipal, Authorization.OpenIdConnectAuthorization authorization, - OpenIddictTokenManager tokenManager, // TODO Make the token manager use the scoped database context. - [EnumeratorCancellation] CancellationToken cancellationToken + CancellationToken cancellationToken ) { if (!await authorization.IsAuthorizedToManageOpenIdConnect(claimsPrincipal, cancellationToken)) { - yield break; - } - await foreach (var token in tokenManager.ListAsync(cancellationToken: cancellationToken)) - { - yield return token; + authorization.ReportUnauthorizedError(resolverContext); + return HotChocolate.Types.Pagination.Connection.Empty(); } + return await databaseContext.OpenIdConnectTokens + .AsNoTracking() + .With(resolverContext.GetQueryContext(), Sorting.DefaultEntityOrder) + .ToPageAsync(resolverContext.GetPagingArguments(), cancellationToken) + .ToConnectionAsync(); } [UseUserManager] [Authorize(Policy = AuthorizationPolicies.ManageOpenIdConnectScopePolicy)] public async Task GetOpenIdConnectTokenAsync( Guid id, + IOpenIdConnectTokenByIdDataLoader byId, ClaimsPrincipal claimsPrincipal, Authorization.OpenIdConnectAuthorization authorization, - OpenIddictTokenManager tokenManager, + IResolverContext resolverContext, CancellationToken cancellationToken ) { - if (!await authorization.IsAuthorizedToManageToken(claimsPrincipal, id, tokenManager, cancellationToken)) + if (!await authorization.IsAuthorizedToManageToken(claimsPrincipal, id, cancellationToken)) { + authorization.ReportUnauthorizedError(resolverContext); return null; } - - return await tokenManager.FindByIdAsync(id.ToString(), cancellationToken: cancellationToken); + return await byId.LoadAsync(id, cancellationToken); } } \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenSortType.cs b/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenSortType.cs new file mode 100644 index 000000000..adf058e9b --- /dev/null +++ b/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenSortType.cs @@ -0,0 +1,23 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data.OpenIdConnect; +using Metabase.GraphQl.Entities; + +namespace Metabase.GraphQl.OpenIdConnect.Tokens; + +public sealed class OpenIdConnectTokenSortType + : AuditableEntitySortType +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.Name(nameof(OpenIdConnectTokenSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); + descriptor.Field(_ => _.CreationDate).Ignore(); + descriptor.Field(_ => _.ExpirationDate).Name(OpenIdConnectTokenType.ExpiredAtName); + descriptor.Field(_ => _.RedemptionDate).Name(OpenIdConnectTokenType.RedeemedAtName); + descriptor.Field(_ => _.Status); + descriptor.Field(_ => _.Subject); + descriptor.Field(_ => _.Type); + } +} diff --git a/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenType.cs b/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenType.cs index 4e426cb9b..eef544cea 100644 --- a/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenType.cs +++ b/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenType.cs @@ -6,13 +6,18 @@ using Metabase.Data.OpenIdConnect; using Metabase.GraphQl.Users; using Metabase.GraphQl.Entities; +using Metabase.Data; +using Microsoft.EntityFrameworkCore; using OpenIddict.Core; namespace Metabase.GraphQl.OpenIdConnect.Tokens; public sealed class OpenIdConnectTokenType - : EntityType + : EntityType { + internal const string ExpiredAtName = "expiredAt"; + internal const string RedeemedAtName = "redeemedAt"; + protected override void Configure( IObjectTypeDescriptor descriptor ) @@ -22,41 +27,80 @@ IObjectTypeDescriptor descriptor descriptor.Field(token => token.ReferenceId).Ignore(); descriptor.Field(token => token.Payload).Ignore(); descriptor.Field(token => token.ConcurrencyToken).Ignore(); + descriptor.Field(token => token.CreationDate).Ignore(); // use `CreatedAt` instead + descriptor + .Field(_ => _.ExpirationDate) + .Name(ExpiredAtName); + descriptor + .Field(_ => _.RedemptionDate) + .Name(RedeemedAtName); + descriptor + .Field(_ => _.Subject) + .Type>() + .Cost(1) + .ResolveWith(x => + TokenResolvers.GetSubjectAsync(default!, default!, default!, default!) + ); descriptor .Field(t => t.Application) - .Type>>() + .Type>() .Resolve(context => - new OpenIdConnectTokenApplicationEdge( - context.Parent().Application! - ) - ); + { + // auto-included in `ApplicationDbContext` + var application = context.Parent().Application; + return application is null + ? null + : new OpenIdConnectTokenApplicationEdge(application); + }); descriptor .Field(t => t.Authorization) - .Type>>() + .Type>() .Resolve(context => - new OpenIdConnectTokenAuthorizationEdge( - context.Parent().Authorization! - ) - ); + { + // auto-included in `ApplicationDbContext` + var authorization = context.Parent().Authorization; + return authorization is null + ? null + : new OpenIdConnectTokenAuthorizationEdge(authorization); + }); descriptor - .Field("isAuthorizedToRevokeNode") - .ResolveWith(x => - TokenResolvers.IsAuthorizedToRevokeNodeAsync(default!, default!, default!, default!, default!)) - .UseUserManager(); + .Field("isAuthorizedToRevokeNode") + .Cost(1) + .ResolveWith(x => + TokenResolvers.IsAuthorizedToRevokeNodeAsync(default!, default!, default!, default!) + ) + .UseUserManager(); } private sealed class TokenResolvers { + public static Task GetSubjectAsync( + [Parent] OpenIdConnectToken token, + OpenIddictApplicationManager applicationManager, + ApplicationDbContext databaseContext, + CancellationToken cancellationToken + ) + { + return IOpenIdConnectSubject.SwitchSubjectAsync( + token.Subject, + async (userId) => + await databaseContext.Users.AsNoTracking() + .SingleOrDefaultAsync(_ => _.Id == userId, cancellationToken), + async (clientId) => + await applicationManager.FindByClientIdAsync(clientId, cancellationToken), + async () => null + ); + } + public static Task IsAuthorizedToRevokeNodeAsync( [Parent] OpenIdConnectToken token, ClaimsPrincipal claimsPrincipal, Authorization.OpenIdConnectAuthorization authorization, - OpenIddictTokenManager tokenManager, CancellationToken cancellationToken ) { - return authorization.IsAuthorizedToManageToken(claimsPrincipal, token.Id, tokenManager, cancellationToken); + return authorization.IsAuthorizedToManageToken(claimsPrincipal, token.Id, cancellationToken); } } } \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Tokens/RevokeOpenIdConnectTokenPayload.cs b/backend/src/GraphQl/OpenIdConnect/Tokens/RevokeOpenIdConnectTokenPayload.cs index b210ebaa6..99fdc8199 100644 --- a/backend/src/GraphQl/OpenIdConnect/Tokens/RevokeOpenIdConnectTokenPayload.cs +++ b/backend/src/GraphQl/OpenIdConnect/Tokens/RevokeOpenIdConnectTokenPayload.cs @@ -5,8 +5,6 @@ namespace Metabase.GraphQl.OpenIdConnect.Tokens; public sealed class RevokeOpenIdConnectTokenPayload { - public OpenIdConnectToken? Token { get; } - public RevokeOpenIdConnectTokenPayload(OpenIdConnectToken token) { Token = token; @@ -19,5 +17,6 @@ RevokeOpenIdConnectTokenError error Errors = [error]; } + public OpenIdConnectToken? Token { get; } public IReadOnlyCollection? Errors { get; } } \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/OpticalComponentSubtype.cs b/backend/src/GraphQl/OpticalDataX/OpticalComponentSubtype.cs similarity index 82% rename from backend/src/GraphQl/DataX/OpticalComponentSubtype.cs rename to backend/src/GraphQl/OpticalDataX/OpticalComponentSubtype.cs index 161f72f28..685d20ce5 100644 --- a/backend/src/GraphQl/DataX/OpticalComponentSubtype.cs +++ b/backend/src/GraphQl/OpticalDataX/OpticalComponentSubtype.cs @@ -1,30 +1,32 @@ using System.Diagnostics.CodeAnalysis; -namespace Metabase.GraphQl.DataX; +namespace Metabase.GraphQl.OpticalDataX; [SuppressMessage("Naming", "CA1707")] public enum OpticalComponentSubtype { - MONOLITHIC, - LAMINATE, - INTERLAYER, - EMBEDDED_COATING, + ACID_ETCHED_GLASS, + APPLIED_FILM, + CELLULAR_SHADE, + CHROMOGENIC, COATED, COATING, - APPLIED_FILM, - FILM, - VENETIAN_BLIND, DIFFUSING_SHADE, - ROLLER_SHADE, - WOVEN_SHADE, - VERTICAL_LOUVER, + EMBEDDED_COATING, + FILM, + FRITTED_GLASS, + INTERLAYER, + LAMINATE, + MONOLITHIC, PERFORATED_SCREEN, - CELLULAR_SHADE, PLEATED_SHADE, + ROLLER_SHADE, ROMAN_SHADE, - SHADE_MATERIAL, - FRITTED_GLASS, - ACID_ETCHED_GLASS, SANDBLASTED_GLASS, - CHROMOGENIC + SHADE_MATERIAL, + SOLAR_SCREEN, + UNKNOWN, + VENETIAN_BLIND, + VERTICAL_LOUVER, + WOVEN_SHADE } \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/OpticalComponentSubtypePropositionInput.cs b/backend/src/GraphQl/OpticalDataX/OpticalComponentSubtypePropositionInput.cs similarity index 56% rename from backend/src/GraphQl/DataX/OpticalComponentSubtypePropositionInput.cs rename to backend/src/GraphQl/OpticalDataX/OpticalComponentSubtypePropositionInput.cs index 4181e3163..dd2ca5f17 100644 --- a/backend/src/GraphQl/DataX/OpticalComponentSubtypePropositionInput.cs +++ b/backend/src/GraphQl/OpticalDataX/OpticalComponentSubtypePropositionInput.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; -namespace Metabase.GraphQl.DataX; +namespace Metabase.GraphQl.OpticalDataX; public sealed record OpticalComponentSubtypePropositionInput( OpticalComponentSubtype? EqualTo, OpticalComponentSubtype? NotEqualTo, - IReadOnlyList? In, - IReadOnlyList? NotIn + IReadOnlyList? In, + IReadOnlyList? NotIn ); \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/OpticalComponentType.cs b/backend/src/GraphQl/OpticalDataX/OpticalComponentType.cs similarity index 77% rename from backend/src/GraphQl/DataX/OpticalComponentType.cs rename to backend/src/GraphQl/OpticalDataX/OpticalComponentType.cs index 2130cae74..6d0392f59 100644 --- a/backend/src/GraphQl/DataX/OpticalComponentType.cs +++ b/backend/src/GraphQl/OpticalDataX/OpticalComponentType.cs @@ -1,6 +1,6 @@ using System.Diagnostics.CodeAnalysis; -namespace Metabase.GraphQl.DataX; +namespace Metabase.GraphQl.OpticalDataX; [SuppressMessage("Naming", "CA1707")] public enum OpticalComponentType diff --git a/backend/src/GraphQl/DataX/OpticalComponentTypePropositionInput.cs b/backend/src/GraphQl/OpticalDataX/OpticalComponentTypePropositionInput.cs similarity index 55% rename from backend/src/GraphQl/DataX/OpticalComponentTypePropositionInput.cs rename to backend/src/GraphQl/OpticalDataX/OpticalComponentTypePropositionInput.cs index 6c95d03da..2078eedb4 100644 --- a/backend/src/GraphQl/DataX/OpticalComponentTypePropositionInput.cs +++ b/backend/src/GraphQl/OpticalDataX/OpticalComponentTypePropositionInput.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; -namespace Metabase.GraphQl.DataX; +namespace Metabase.GraphQl.OpticalDataX; public sealed record OpticalComponentTypePropositionInput( OpticalComponentType? EqualTo, OpticalComponentType? NotEqualTo, - IReadOnlyList? In, - IReadOnlyList? NotIn + IReadOnlyList? In, + IReadOnlyList? NotIn ); \ No newline at end of file diff --git a/backend/src/GraphQl/OpticalDataX/OpticalData.cs b/backend/src/GraphQl/OpticalDataX/OpticalData.cs new file mode 100644 index 000000000..1c42f3774 --- /dev/null +++ b/backend/src/GraphQl/OpticalDataX/OpticalData.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate.Resolvers; +using HotChocolate.Types.Relay; +using Metabase.Data; +using Metabase.GraphQl.DataX; +using Metabase.GraphQl.Requests; +using NodaTime; + +namespace Metabase.GraphQl.OpticalDataX; + +[Node(IdField = nameof(Id))] +public sealed record OpticalData( + string Id, + Guid Uuid, + OffsetDateTime Timestamp, + string Locale, + Guid DatabaseId, + Guid ComponentId, + string? Name, + string? Description, + IReadOnlyList Warnings, + Guid CreatorId, + OffsetDateTime CreatedAt, + OpticalComponentType? Type, + OpticalComponentSubtype? Subtype, + CoatedSide CoatedSide, + AppliedMethod AppliedMethod, + IReadOnlyList Resources, + GetHttpsResourceTree ResourceTree, + IReadOnlyList Approvals, + // ResponseApproval Approval, + IReadOnlyList NearnormalHemisphericalVisibleTransmittances, + IReadOnlyList NearnormalHemisphericalVisibleReflectances, + IReadOnlyList NearnormalHemisphericalSolarTransmittances, + IReadOnlyList NearnormalHemisphericalSolarReflectances, + IReadOnlyList InfraredEmittances, + IReadOnlyList ColorRenderingIndices, + IReadOnlyList CielabColors +) +: DataX.Data( + Id, + Uuid, + Timestamp, + Locale, + DatabaseId, + ComponentId, + Name, + Description, + Warnings, + CreatorId, + CreatedAt, + AppliedMethod, + Resources, + ResourceTree, + Approvals + ) +{ + public override DataKind Kind => DataKind.OPTICAL_DATA; + + [NodeResolver] + public static Task GetAsync( + string id, + DataQueries dataQueries, + ApplicationDbContext databaseContext, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + return DataX.Data.FetchNodeAsync( + id, + (database, uuid, locale) => dataQueries.GetOpticalDataAsync( + database, + uuid, + locale, + resolverContext, + cancellationToken + ), + databaseContext, + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/OpticalDataConnection.cs b/backend/src/GraphQl/OpticalDataX/OpticalDataConnection.cs similarity index 52% rename from backend/src/GraphQl/DataX/OpticalDataConnection.cs rename to backend/src/GraphQl/OpticalDataX/OpticalDataConnection.cs index 61f82e621..d5c72a13e 100644 --- a/backend/src/GraphQl/DataX/OpticalDataConnection.cs +++ b/backend/src/GraphQl/OpticalDataX/OpticalDataConnection.cs @@ -1,15 +1,14 @@ -using System; using System.Collections.Generic; -using System.Linq; using HotChocolate.Types.Pagination; +using Metabase.GraphQl.DataX; -namespace Metabase.GraphQl.DataX; +namespace Metabase.GraphQl.OpticalDataX; public sealed record OpticalDataConnection( - IReadOnlyList Edges, - uint TotalCount, + IReadOnlyList? Edges, + int TotalCount, ConnectionPageInfo PageInfo -) : DataConnectionBase( +) : DataConnection( Edges, TotalCount, PageInfo diff --git a/backend/src/GraphQl/DataX/OpticalDataEdge.cs b/backend/src/GraphQl/OpticalDataX/OpticalDataEdge.cs similarity index 51% rename from backend/src/GraphQl/DataX/OpticalDataEdge.cs rename to backend/src/GraphQl/OpticalDataX/OpticalDataEdge.cs index d864d96f2..2e52fa12c 100644 --- a/backend/src/GraphQl/DataX/OpticalDataEdge.cs +++ b/backend/src/GraphQl/OpticalDataX/OpticalDataEdge.cs @@ -1,11 +1,11 @@ -using System; +using Metabase.GraphQl.DataX; -namespace Metabase.GraphQl.DataX; +namespace Metabase.GraphQl.OpticalDataX; public sealed record OpticalDataEdge( string Cursor, OpticalData Node -) : DataEdgeBase( +) : DataEdge( Cursor, Node ); \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/OpticalDataPropositionInput.cs b/backend/src/GraphQl/OpticalDataX/OpticalDataPropositionInput.cs similarity index 78% rename from backend/src/GraphQl/DataX/OpticalDataPropositionInput.cs rename to backend/src/GraphQl/OpticalDataX/OpticalDataPropositionInput.cs index 47c818191..9a3d2154c 100644 --- a/backend/src/GraphQl/DataX/OpticalDataPropositionInput.cs +++ b/backend/src/GraphQl/OpticalDataX/OpticalDataPropositionInput.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; +using Metabase.GraphQl.DataX; -namespace Metabase.GraphQl.DataX; +namespace Metabase.GraphQl.OpticalDataX; public sealed record OpticalDataPropositionInput( UuidPropositionInput? ComponentId, @@ -18,4 +19,8 @@ public sealed record OpticalDataPropositionInput( FloatsPropositionInput? InfraredEmittances, FloatsPropositionInput? ColorRenderingIndices, CielabColorsPropositionInput? CielabColors -); \ No newline at end of file +) +{ + public static readonly OpticalDataPropositionInput Empty = + new(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); +}; \ No newline at end of file diff --git a/backend/src/GraphQl/OpticalDataX/OpticalDataQueries.cs b/backend/src/GraphQl/OpticalDataX/OpticalDataQueries.cs new file mode 100644 index 000000000..d7acff48b --- /dev/null +++ b/backend/src/GraphQl/OpticalDataX/OpticalDataQueries.cs @@ -0,0 +1,105 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.CostAnalysis.Types; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using Metabase.Data; +using Metabase.GraphQl.Scalars; +using Metabase.GraphQl.Requests; +using Microsoft.EntityFrameworkCore; + +namespace Metabase.GraphQl.OpticalDataX; + +[ExtendObjectType(nameof(Query))] +public sealed class OpticalDataQueries +{ + public async Task GetOpticalDataAsync( + Guid databaseId, + Guid id, + [GraphQLType] string? locale, + DataQueries dataQueries, + ApplicationDbContext databaseContext, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + var database = await databaseContext.Databases.AsNoTracking() + .Where(_ => _.Id == databaseId) + .SingleOrDefaultAsync(cancellationToken); + if (database is null) + { + return null; + } + return await dataQueries.GetOpticalDataAsync( + database, + id, + locale, + resolverContext, + cancellationToken + ); + } + + [ListSize( + AssumedSize = (int)GraphQlConstants.MaximumPageSize - 1, + SlicingArguments = ["first", "last"], + SlicingArgumentDefaultValue = (int)GraphQlConstants.MaximumPageSize - 1, + SizedFields = ["edges", "nodes"], + RequireOneSlicingArgument = false + )] + public Task GetAllOpticalDataAsync( + OpticalDataPropositionInput? where, + [GraphQLType] string? locale, + int? first, + string? after, + int? last, + string? before, + DataQueries dataQueries, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + return dataQueries.GetAllDataAsync( + first, + after, + last, + before, + (edges, totalCount, pageInfo) => new OpticalDataConnection(edges, totalCount, pageInfo), + (node, cursor) => new OpticalDataEdge(cursor, node), + (database, first, after, last, before) => dataQueries.GetAllOpticalDataAsync( + database, + where, + locale, + first, + after, + last, + before, + resolverContext, + cancellationToken + ), + cancellationToken + ); + } + + public Task HasOpticalDataAsync( + OpticalDataPropositionInput? where, + [GraphQLType] string? locale, + DataQueries dataQueries, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + return dataQueries.HasDataAsync( + (database) => dataQueries.HasOpticalDataAsync( + database, + where, + locale, + resolverContext, + cancellationToken + ), + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/PaginatedConnection.cs b/backend/src/GraphQl/PaginatedConnection.cs new file mode 100644 index 000000000..f8351514b --- /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 Metabase.Data; +using Metabase.GraphQl.Extensions; + +namespace Metabase.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 000000000..36766fee0 --- /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 Metabase.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/PaginationHelpers.cs b/backend/src/GraphQl/PaginationHelpers.cs new file mode 100644 index 000000000..708bfbb1e --- /dev/null +++ b/backend/src/GraphQl/PaginationHelpers.cs @@ -0,0 +1,17 @@ +using System; +using Metabase.Extensions; + +namespace Metabase.GraphQl; + +public static class PaginationHelpers +{ + public static string ConstructCursor(Guid id) + { + return id.ToString("D").Base64Encode(); + } + + public static string ConstructCursor(Guid id1, Guid id2) + { + return $"{id1:D}:{id2:D}".Base64Encode(); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicData.cs b/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicData.cs new file mode 100644 index 000000000..03e2aaa82 --- /dev/null +++ b/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicData.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate.Resolvers; +using HotChocolate.Types.Relay; +using Metabase.Data; +using Metabase.GraphQl.DataX; +using Metabase.GraphQl.Requests; +using NodaTime; + +namespace Metabase.GraphQl.PhotovoltaicDataX; + +[Node(IdField = nameof(Id))] +public sealed record PhotovoltaicData( + string Id, + Guid Uuid, + OffsetDateTime Timestamp, + string Locale, + Guid DatabaseId, + Guid ComponentId, + string? Name, + string? Description, + IReadOnlyList Warnings, + Guid CreatorId, + OffsetDateTime CreatedAt, + AppliedMethod AppliedMethod, + IReadOnlyList Resources, + GetHttpsResourceTree ResourceTree, + IReadOnlyList Approvals +// ResponseApproval Approval +) +: DataX.Data( + Id, + Uuid, + Timestamp, + Locale, + DatabaseId, + ComponentId, + Name, + Description, + Warnings, + CreatorId, + CreatedAt, + AppliedMethod, + Resources, + ResourceTree, + Approvals +) +{ + public override DataKind Kind { get => DataKind.PHOTOVOLTAIC_DATA; } + + [NodeResolver] + public static Task GetAsync( + string id, + DataQueries dataQueries, + ApplicationDbContext databaseContext, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + return DataX.Data.FetchNodeAsync( + id, + (database, uuid, locale) => dataQueries.GetPhotovoltaicDataAsync( + database, + uuid, + locale, + resolverContext, + cancellationToken + ), + databaseContext, + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/PhotovoltaicDataConnection.cs b/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataConnection.cs similarity index 51% rename from backend/src/GraphQl/DataX/PhotovoltaicDataConnection.cs rename to backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataConnection.cs index 70db869cc..2504310f4 100644 --- a/backend/src/GraphQl/DataX/PhotovoltaicDataConnection.cs +++ b/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataConnection.cs @@ -1,14 +1,14 @@ -using System; using System.Collections.Generic; using HotChocolate.Types.Pagination; +using Metabase.GraphQl.DataX; -namespace Metabase.GraphQl.DataX; +namespace Metabase.GraphQl.PhotovoltaicDataX; public sealed record PhotovoltaicDataConnection( - IReadOnlyList Edges, - uint TotalCount, + IReadOnlyList? Edges, + int TotalCount, ConnectionPageInfo PageInfo -) : DataConnectionBase( +) : DataConnection( Edges, TotalCount, PageInfo diff --git a/backend/src/GraphQl/DataX/PhotovoltaicDataEdge.cs b/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataEdge.cs similarity index 50% rename from backend/src/GraphQl/DataX/PhotovoltaicDataEdge.cs rename to backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataEdge.cs index d23d5e4e0..07f40eb67 100644 --- a/backend/src/GraphQl/DataX/PhotovoltaicDataEdge.cs +++ b/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataEdge.cs @@ -1,9 +1,11 @@ -namespace Metabase.GraphQl.DataX; +using Metabase.GraphQl.DataX; + +namespace Metabase.GraphQl.PhotovoltaicDataX; public sealed record PhotovoltaicDataEdge( string Cursor, PhotovoltaicData Node -) : DataEdgeBase( +) : DataEdge( Cursor, Node ); \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/PhotovoltaicDataPropositionInput.cs b/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataPropositionInput.cs similarity index 63% rename from backend/src/GraphQl/DataX/PhotovoltaicDataPropositionInput.cs rename to backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataPropositionInput.cs index f0d573649..cf2ee7017 100644 --- a/backend/src/GraphQl/DataX/PhotovoltaicDataPropositionInput.cs +++ b/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataPropositionInput.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; +using Metabase.GraphQl.DataX; -namespace Metabase.GraphQl.DataX; +namespace Metabase.GraphQl.PhotovoltaicDataX; public sealed record PhotovoltaicDataPropositionInput( UuidPropositionInput? ComponentId, @@ -8,4 +9,8 @@ public sealed record PhotovoltaicDataPropositionInput( PhotovoltaicDataPropositionInput? Not, IReadOnlyList? Or, GetHttpsResourcesPropositionInput? Resources -); \ No newline at end of file +) +{ + public static readonly PhotovoltaicDataPropositionInput Empty = + new(null, null, null, null, null); +}; \ No newline at end of file diff --git a/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataQueries.cs b/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataQueries.cs new file mode 100644 index 000000000..9a21d436c --- /dev/null +++ b/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataQueries.cs @@ -0,0 +1,105 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate; +using HotChocolate.CostAnalysis.Types; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using Metabase.Data; +using Metabase.GraphQl.Scalars; +using Metabase.GraphQl.Requests; +using Microsoft.EntityFrameworkCore; + +namespace Metabase.GraphQl.PhotovoltaicDataX; + +[ExtendObjectType(nameof(Query))] +public sealed class PhotovoltaicDataQueries +{ + public async Task GetPhotovoltaicDataAsync( + Guid databaseId, + Guid id, + [GraphQLType] string? locale, + DataQueries dataQueries, + ApplicationDbContext databaseContext, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + var database = await databaseContext.Databases.AsNoTracking() + .Where(_ => _.Id == databaseId) + .SingleOrDefaultAsync(cancellationToken); + if (database is null) + { + return null; + } + return await dataQueries.GetPhotovoltaicDataAsync( + database, + id, + locale, + resolverContext, + cancellationToken + ); + } + + [ListSize( + AssumedSize = (int)GraphQlConstants.MaximumPageSize - 1, + SlicingArguments = ["first", "last"], + SlicingArgumentDefaultValue = (int)GraphQlConstants.MaximumPageSize - 1, + SizedFields = ["edges", "nodes"], + RequireOneSlicingArgument = false + )] + public Task GetAllPhotovoltaicDataAsync( + PhotovoltaicDataPropositionInput? where, + [GraphQLType] string? locale, + int? first, + string? after, + int? last, + string? before, + DataQueries dataQueries, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + return dataQueries.GetAllDataAsync( + first, + after, + last, + before, + (edges, totalCount, pageInfo) => new PhotovoltaicDataConnection(edges, totalCount, pageInfo), + (node, cursor) => new PhotovoltaicDataEdge(cursor, node), + (database, first, after, last, before) => dataQueries.GetAllPhotovoltaicDataAsync( + database, + where, + locale, + first, + after, + last, + before, + resolverContext, + cancellationToken + ), + cancellationToken + ); + } + + public Task HasPhotovoltaicDataAsync( + PhotovoltaicDataPropositionInput? where, + [GraphQLType] string? locale, + DataQueries dataQueries, + IResolverContext resolverContext, + CancellationToken cancellationToken + ) + { + return dataQueries.HasDataAsync( + (database) => dataQueries.HasPhotovoltaicDataAsync( + database, + where, + locale, + resolverContext, + cancellationToken + ), + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Publications/PublicationInput.cs b/backend/src/GraphQl/Publications/PublicationInput.cs index 1afbb6db5..f34037aae 100644 --- a/backend/src/GraphQl/Publications/PublicationInput.cs +++ b/backend/src/GraphQl/Publications/PublicationInput.cs @@ -1,5 +1,7 @@ using System; +using HotChocolate; using Metabase.Data; +using Metabase.GraphQl.Scalars; namespace Metabase.GraphQl.Publications; @@ -8,9 +10,9 @@ public sealed record PublicationInput( string? Abstract, string? Section, string[]? Authors, - string? Doi, - string? ArXiv, - string? Urn, + [property: GraphQLType] string? Doi, + [property: GraphQLType] string? ArXiv, + [property: GraphQLType] string? Urn, Uri? WebAddress ) { diff --git a/backend/src/GraphQl/References/ReferenceInput.cs b/backend/src/GraphQl/References/ReferenceInput.cs index d85564671..710dc8f09 100644 --- a/backend/src/GraphQl/References/ReferenceInput.cs +++ b/backend/src/GraphQl/References/ReferenceInput.cs @@ -1,10 +1,12 @@ using System; +using HotChocolate.Types; using Metabase.Data; using Metabase.GraphQl.Publications; using Metabase.GraphQl.Standards; namespace Metabase.GraphQl.References; +[OneOf] public sealed record ReferenceInput( StandardInput? Standard, PublicationInput? Publication diff --git a/backend/src/GraphQl/Requests/DataQueries.cs b/backend/src/GraphQl/Requests/DataQueries.cs new file mode 100644 index 000000000..cc8a6c6b8 --- /dev/null +++ b/backend/src/GraphQl/Requests/DataQueries.cs @@ -0,0 +1,1137 @@ +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Text.Json.Serialization; +using System.Linq; +using GraphQL; +using HotChocolate; +using HotChocolate.Resolvers; +using HotChocolate.Types.Pagination; +using Metabase.Data; +using Metabase.GraphQl.DataX; +using Metabase.GraphQl.CalorimetricDataX; +using Metabase.GraphQl.GeometricDataX; +using Metabase.GraphQl.HygrothermalDataX; +using Metabase.GraphQl.LifeCycleDataX; +using Metabase.GraphQl.OpticalDataX; +using Metabase.GraphQl.PhotovoltaicDataX; +using Metabase.Json; +using Metabase.GraphQl.Scalars; +using Metabase.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.EntityFrameworkCore; + +namespace Metabase.GraphQl.Requests; + +public static partial class Log +{ + [LoggerMessage( + Level = LogLevel.Warning, + Message = "Failed with errors {Errors} to query the database {Locator} for {Request}.")] + public static partial void FailedWithErrors( + this ILogger logger, + string Errors, + Uri Locator, + string Request + ); +} + +public sealed class DataQueries( + IDbContextFactory databaseContextFactory, + QueryingDatabases queryingDatabases, + GraphQlRequestHelper graphQlRequestHelper, + ILogger logger +) +{ + private static readonly string[] s_calorimetricDataFileNames = + [ + "DataFields.graphql", + "CalorimetricDataFields.graphql", + "CalorimetricData.graphql" + ]; + + private static readonly string[] s_geometricDataFileNames = + [ + "DataFields.graphql", + "GeometricDataFields.graphql", + "GeometricData.graphql" + ]; + + private static readonly string[] s_hygrothermalDataFileNames = + [ + "DataFields.graphql", + "HygrothermalDataFields.graphql", + "HygrothermalData.graphql" + ]; + + private static readonly string[] s_lifeCycleDataFileNames = + [ + "DataFields.graphql", + "LifeCycleDataFields.graphql", + "LifeCycleData.graphql" + ]; + + private static readonly string[] s_opticalDataFileNames = + [ + "DataFields.graphql", + "OpticalDataFields.graphql", + "OpticalData.graphql" + ]; + + private static readonly string[] s_photovoltaicDataFileNames = + [ + "DataFields.graphql", + "PhotovoltaicDataFields.graphql", + "PhotovoltaicData.graphql" + ]; + + private static readonly string[] s_allCalorimetricDataFileNames = + [ + "DataFields.graphql", + "CalorimetricDataFields.graphql", + "PageInfoFields.graphql", + "AllCalorimetricData.graphql" + ]; + + private static readonly string[] s_allGeometricDataFileNames = + [ + "DataFields.graphql", + "GeometricDataFields.graphql", + "PageInfoFields.graphql", + "AllGeometricData.graphql" + ]; + + private static readonly string[] s_allHygrothermalDataFileNames = + [ + "DataFields.graphql", + "HygrothermalDataFields.graphql", + "PageInfoFields.graphql", + "AllHygrothermalData.graphql" + ]; + + private static readonly string[] s_allLifeCycleDataFileNames = + [ + "DataFields.graphql", + "LifeCycleDataFields.graphql", + "PageInfoFields.graphql", + "AllLifeCycleData.graphql" + ]; + + private static readonly string[] s_allOpticalDataFileNames = + [ + "DataFields.graphql", + "OpticalDataFields.graphql", + "PageInfoFields.graphql", + "AllOpticalData.graphql" + ]; + + private static readonly string[] s_allPhotovoltaicDataFileNames = + [ + "DataFields.graphql", + "PhotovoltaicDataFields.graphql", + "PageInfoFields.graphql", + "AllPhotovoltaicData.graphql" + ]; + + private static readonly string[] s_hasCalorimetricDataFileNames = + [ + "HasCalorimetricData.graphql" + ]; + + private static readonly string[] s_hasGeometricDataFileNames = + [ + "HasGeometricData.graphql" + ]; + + private static readonly string[] s_hasHygrothermalDataFileNames = + [ + "HasHygrothermalData.graphql" + ]; + + private static readonly string[] s_hasLifeCycleDataFileNames = + [ + "HasLifeCycleData.graphql" + ]; + + private static readonly string[] s_hasOpticalDataFileNames = + [ + "HasOpticalData.graphql" + ]; + + private static readonly string[] s_hasPhotovoltaicDataFileNames = + [ + "HasPhotovoltaicData.graphql" + ]; + + private sealed record NeighboringCursors( + [property: JsonPropertyName("l")] string? Before, + [property: JsonPropertyName("r")] string? After + ); + + /// The cursor of an edge of an edge list resulting from interleaving edge + /// lists of various databases. The database the edge belongs to is + /// identified by the database ID (`DatabaseId`). The cursor of the edge + /// within its database (hereafter referred to be "local cursor") is + /// `Cursors[DatabaseId].Before` and `Cursors[DatabaseId].After` (both + /// values are identical in this case). For each other database with ID + /// `OtherId`, if for that database there is an edge in the edge list + /// before the current edge, then its cursor is `Cursors[OtherId].Before`, + /// otherwise the value is `null`, and if there is an edge after the + /// current edge, then its cursor is `Cursors[OtherId].After`, otherwise + /// `null`. + /// + /// Database X: [1, 2, 3, 4, 5] (local cursors) + /// Database Y: [a, b, c] (local cursors) + /// Interleaved: [1, a, 2, b, 3, c, 4, 5] (short hand) + /// [ (long form) + /// [(X, { X: (1, 1), Y: (null, a) })] (0) + /// [(Y, { X: (1, 2), Y: (a, a) })] (1) + /// [(X, { X: (2, 2), Y: (a, b) })] (2) + /// [(X, { X: (2, 3), Y: (b, b) })] (3) + /// [(X, { X: (3, 3), Y: (b, c) })] (4) + /// [(X, { X: (3, 4), Y: (c, c) })] (5) + /// [(X, { X: (4, 4), Y: (c, null) })] (6) + /// [(X, { X: (5, 5), Y: (c, null) })] (7) + /// ] + /// + /// The values of `Before` and `After` are used for queries with `after` or + /// `before` pagination argument. For example, to query for edges after + /// `[(X, { X: (3, 4), Y: (c, c) })]` (5), database `X` is asked for edges + /// after `3` (the `Before` cursor) and `Y` for edges after `c` (the + /// `Before` cursor), and to query for edges before it, database `X` is + /// asked for edges before `4` (the `After` cursor) and `Y` for edges + /// before `c` (the `After` cursor). + private sealed record CompoundCursor + { + [JsonPropertyName("d")] + public required Guid DatabaseId { get; set; } + + [JsonPropertyName("c")] + public required Dictionary Cursors { get; set; } + }; + + private static string SerializeCompoundCursor(CompoundCursor cursor) + { + return JsonSerializer.Serialize(cursor, JsonSerializerSettings.Compact).Base64Encode(); + } + + private static CompoundCursor? DeserializeCompoundCursor(string? cursors) + { + if (cursors is null) + { + return null; + } + return JsonSerializer.Deserialize( + cursors.Base64Decode(), + JsonSerializerSettings.Compact + ); + } + + private enum PaginationDirection + { + FORWARD, + BACKWARD + } + + /// + /// The maximum and default page size is 99, that is 1 less than the + /// maximum page size of product-data databases. + /// + public async Task GetAllDataAsync( + int? first, + string? after, + int? last, + string? before, + Func, int, ConnectionPageInfo, TDataConnection> createDataConnection, + Func createDataEdge, + Func> getAllDataAsync, + CancellationToken cancellationToken + ) + where TDataConnection : DataConnection + where TDataEdge : DataEdge + { + await using var databaseContext = await databaseContextFactory.CreateDbContextAsync(cancellationToken); + var paginationDirection = (first, last) switch + { + (_, null) => PaginationDirection.FORWARD, + (null, _) => PaginationDirection.BACKWARD, + _ => PaginationDirection.FORWARD, + }; + var compoundAfter = DeserializeCompoundCursor(after); + var compoundBefore = DeserializeCompoundCursor(before); + var databases = await databaseContext.Databases.AsNoTracking() + .Where(_ => _.VerificationState == Enumerations.DatabaseVerificationState.VERIFIED) + // on follow-up requests include only the previously included databases + .If( + compoundAfter is not null || compoundBefore is not null, + queryable => queryable.Where(_ => + (compoundAfter ?? compoundBefore ?? default!).Cursors.Keys.Contains(_.Id) + ) + ) + // order databases to stabilize compound cursors and the order of interleaved edges + .OrderBy(_ => _.CreatedAt) + .ToListAsync(cancellationToken); + if (databases.Count is 0) + { + return createDataConnection([], 0, new ConnectionPageInfo(false, false, null, null)); + } + // reorder and rotate the databases get the first edge after or last edge before and so forth of the interleaved edges in the correct order + var beginAfterDatabaseId = + paginationDirection is PaginationDirection.FORWARD + ? compoundAfter?.DatabaseId + : compoundBefore?.DatabaseId; + var rotatedDatabases = databases + .IfList(paginationDirection is PaginationDirection.BACKWARD, _ => _.ToReversed()) + .IfList(beginAfterDatabaseId is not null, _ => _.Rotate(_ => _.Id == beginAfterDatabaseId!)); + // fetch data from the databases concurrently leaving the order intact. + // Ask for 1 more node after and/or before the last or first requested + // node; it/they is/are needed to construct the compound cursor of the + // last or first node. Do not ask for more nodes though than the + // maximum page size of the databases. + var connections = await Task.WhenAll( + rotatedDatabases.Select((database) => + getAllDataAsync( + database, + first is null ? null : Math.Min((int)first + 1, (int)GraphQlConstants.MaximumPageSize), + compoundAfter?.Cursors.GetValueOrDefault(database.Id)?.Before, + last is null ? null : Math.Min((int)last + 1, (int)GraphQlConstants.MaximumPageSize), + compoundBefore?.Cursors.GetValueOrDefault(database.Id)?.After + ) + ) + ); + var databaseToConnection = rotatedDatabases + .Zip(connections, (database, connection) => connection is null ? null : new { databaseId = database.Id, connection }) + .NotNull() + .ToDictionary(_ => _.databaseId, _ => _.connection); + // adapt the after and before cursors such that they can serve as seed for the next or previous edge + compoundAfter?.Cursors[compoundAfter.DatabaseId] = new( + compoundAfter.Cursors.GetValueOrDefault(compoundAfter.DatabaseId)?.Before, + databaseToConnection.GetValueOrDefault(compoundAfter.DatabaseId)?.Edges?.GetFirstOrDefault()?.Cursor + ); + compoundBefore?.Cursors[compoundBefore.DatabaseId] = new( + databaseToConnection.GetValueOrDefault(compoundBefore.DatabaseId)?.Edges?.GetLastOrDefault()?.Cursor, + compoundBefore.Cursors.GetValueOrDefault(compoundBefore.DatabaseId)?.After + ); + // interleave edges and replace their cursors with compound cursors (see `CompoundCursor`) + var edges = paginationDirection is PaginationDirection.FORWARD + ? databaseToConnection + .Select(_ => + // pair each edge with its right neighbor padding at the + // end, where the first entry in each tuple is the edge and + // the second its right neighbor `(edge, neighbor)`. For + // example [1, 2, 3] becomes [(1, 2), (2, 3), (3, null)] + _.Value.Edges?.Zip( + _.Value.Edges.Skip(1).Append(null), + (current, after) => new { current, after } + ) + .Select((neighboringEdges) => (neighboringEdges, databaseId: _.Key)) + ?? [] + ) + // interleave edges of various databases (from the left or + // left-aligned or padded right with `null`s) + // Database X: [1, 2, 3, 4, 5] (local cursors) + // Database Y: [a, b, c] (local cursors) + // Interleaved: [1, a, 2, b, 3, c, 4, 5] (short hand) + // (long form) [(1, 2), (a, b), (2, 3), (b, c), (3, 4), (c, null), (4, 5), (5, null)] + .Interleave() + // give each edge a compound cursor with enough information for + // using it as `after` and `before` in paginated queries: + // [ (long form) + // [(X, { X: (1, 1), Y: (null, a) })] (0) + // [(Y, { X: (1, 2), Y: (a, a) })] (1) + // [(X, { X: (2, 2), Y: (a, b) })] (2) + // [(Y, { X: (2, 3), Y: (b, b) })] (3) + // [(X, { X: (3, 3), Y: (b, c) })] (4) + // [(Y, { X: (3, 4), Y: (c, c) })] (5) + // [(X, { X: (4, 4), Y: (c, null) })] (6) + // [(X, { X: (5, 5), Y: (c, null) })] (7) + // ] + // if `after` were [(Y, { X: (0, 1), Y: (#, #) })], then `null` + // in the cursor of (0) would become `#`: + // [(X, { X: (1, 1), Y: (#, a) })] (0) + .Scan( + compoundAfter ?? new() + { + Cursors = databases.ToDictionary( + _ => _.Id, + _ => new NeighboringCursors( + null, + databaseToConnection.GetValueOrDefault(_.Id)?.Edges?.GetFirstOrDefault()?.Cursor + ) + ), + DatabaseId = rotatedDatabases[^1].Id + }, + (compoundCursor, _) => + { + // adapt the cursor for the current edge + compoundCursor.Cursors[_.databaseId] = new(_.neighboringEdges.current.Cursor, _.neighboringEdges.current.Cursor); + compoundCursor.DatabaseId = _.databaseId; + var currentEdgeCursor = SerializeCompoundCursor(compoundCursor); + // adapt the cursor for the edge coming after + compoundCursor.Cursors[_.databaseId] = new(_.neighboringEdges.current.Cursor, _.neighboringEdges.after?.Cursor); + return (compoundCursor, createDataEdge(_.neighboringEdges.current.Node, currentEdgeCursor)); + } + ) + .ToList() + : databaseToConnection + .Select(_ => + // pair each edge with its left neighbor padding at the + // beginning, where the second entry in each tuple is the + // edge and the first its left neighbor `(neighbor, edge)`. + // For example [1, 2, 3] becomes [(null, 1), (1, 2), (2, 3)]. + // By reversing it before interleaving and scanning it with + // the edges of other databases, the edges are iterated in + // reverse, that is, from the end to the beginning. + _.Value.Edges?.Prepend(null).SkipLast(1).Zip( + _.Value.Edges, + (before, current) => new { before, current } + ) + .Reverse() + .Select((neighboringEdges) => (neighboringEdges, databaseId: _.Key)) + ?? [] + ) + // interleave edges of various databases (from the right or + // right-aligned or padded left with `null`s) + // Database X: [1, 2, 3, 4, 5] (local cursors) + // Database Y: [a, b, c] (local cursors) + // Interleaved: [1, 2, a, 3, b, 4, c, 5] (short hand) + // (long form) [(null, 1), (1, 2), (null, a), (2, 3), (a, b), (3, 4), (b, c), (4, 5)] + .Interleave() + // give each edge a compound cursor with enough information for + // using it as `after` and `before` in paginated queries + // [ (long form) + // [(X, { X: (1, 1), Y: (null, a) })] (0) + // [(X, { X: (2, 2), Y: (null, a) })] (1) + // [(Y, { X: (2, 3), Y: (a, a) })] (2) + // [(X, { X: (3, 3), Y: (a, b) })] (3) + // [(Y, { X: (3, 4), Y: (b, b) })] (4) + // [(X, { X: (4, 4), Y: (b, c) })] (5) + // [(Y, { X: (4, 5), Y: (c, c) })] (6) + // [(X, { X: (5, 5), Y: (c, null) })] (7) + // ] + // if `before` were [(Y, { X: (5, 6), Y: (d, d) })], then `null` + // in the cursor of (7) would become `d`: + // [(X, { X: (5, 5), Y: (c, d) })] (7) + .Scan( + compoundBefore ?? new() + { + Cursors = databases.ToDictionary( + _ => _.Id, + _ => new NeighboringCursors( + databaseToConnection.GetValueOrDefault(_.Id)?.Edges?.GetFirstOrDefault()?.Cursor, + null + ) + ), + DatabaseId = rotatedDatabases[^1].Id + }, + (compoundCursor, _) => + { + // adapt the cursor for the current edge + compoundCursor.Cursors[_.databaseId] = new(_.neighboringEdges.current.Cursor, _.neighboringEdges.current.Cursor); + compoundCursor.DatabaseId = _.databaseId; + var currentEdgeCursor = SerializeCompoundCursor(compoundCursor); + // adapt the cursor for the edge coming before + compoundCursor.Cursors[_.databaseId] = new(_.neighboringEdges.before?.Cursor, _.neighboringEdges.current.Cursor); + return (compoundCursor, createDataEdge(_.neighboringEdges.current.Node, currentEdgeCursor)); + } + ) + // undo the reversal of the edges above + .Reverse() + .ToList(); + // clamp the edges taking only the first `first` and the last `last` + // (or the maximum page size minus 1). Before we fetched 1 more than + // `first` and/or `last` (for compound cursor construction) and at most + // `GraphQlConstants.MaximumPageSize` (to not exceed the max allowed) + // nodes from databases. The additional node(s) do(es) not have a + // usable compound cursor and must not be included in the final result. + // Therefore, we cap at max minus 1, which is 99, instead of at 100. + var cappedFirst = Math.Min(first ?? (int)GraphQlConstants.MaximumPageSize - 1, (int)GraphQlConstants.MaximumPageSize - 1); + var cappedLast = Math.Min(last ?? (int)GraphQlConstants.MaximumPageSize - 1, (int)GraphQlConstants.MaximumPageSize - 1); + var clampedEdges = + ((first, last) switch + { + (_, null) => edges.Take(cappedFirst), + (null, _) => edges.TakeLast(cappedLast), + _ => edges.Take(cappedFirst).TakeLast(cappedLast) + }) + .ToList() + .AsReadOnly(); + // compute total cound and page info + var totalCount = connections.Sum(_ => _?.TotalCount ?? 0); + var pageInfo = new ConnectionPageInfo( + // there is a next page if edges were removed from the end + hasNextPage: (clampedEdges.Count > 0 && clampedEdges[^1] != edges[^1]) || connections.Any(_ => _?.PageInfo.HasNextPage ?? false), + // there is a previous page if edges were removed from the beginning + hasPreviousPage: (clampedEdges.Count > 0 && clampedEdges[0] != edges[0]) || connections.Any(_ => _?.PageInfo.HasPreviousPage ?? false), + startCursor: clampedEdges.Count is 0 ? null : clampedEdges[0].Cursor, + endCursor: clampedEdges.Count is 0 ? null : clampedEdges[^1].Cursor + ); + return createDataConnection(clampedEdges, totalCount, pageInfo); + } + + public async Task HasDataAsync( + Func> hasDataAsync, + CancellationToken cancellationToken + ) + { + await using var databaseContext = await databaseContextFactory.CreateDbContextAsync(cancellationToken); + var databases = await databaseContext.Databases.AsNoTracking().ToListAsync(cancellationToken); + var hasData = await Task.WhenAll( + databases.Select((database) => + hasDataAsync(database) + ) + ); + return hasData.Any(_ => _ ?? false); + } + + public async Task GetDataAsync( + Database database, + Guid id, + DataKind kind, + [GraphQLType] string? locale, + IResolverContext? resolverContext, + CancellationToken cancellationToken + ) + { + return kind switch + { + DataKind.CALORIMETRIC_DATA => await GetCalorimetricDataAsync(database, id, locale, resolverContext, cancellationToken), + DataKind.GEOMETRIC_DATA => await GetGeometricDataAsync(database, id, locale, resolverContext, cancellationToken), + DataKind.HYGROTHERMAL_DATA => await GetHygrothermalDataAsync(database, id, locale, resolverContext, cancellationToken), + DataKind.LIFE_CYCLE_DATA => await GetLifeCycleDataAsync(database, id, locale, resolverContext, cancellationToken), + DataKind.OPTICAL_DATA => await GetOpticalDataAsync(database, id, locale, resolverContext, cancellationToken), + DataKind.PHOTOVOLTAIC_DATA => await GetPhotovoltaicDataAsync(database, id, locale, resolverContext, cancellationToken), + _ => throw new ArgumentOutOfRangeException($"The data kind {kind} is not supported.") + }; + } + + public async Task HasDataAsync( + Database database, + DataKind kind, + DataPropositionInput? where, + [GraphQLType] string? locale, + IResolverContext? resolverContext, + CancellationToken cancellationToken + ) + { + return kind switch + { + DataKind.CALORIMETRIC_DATA => await HasCalorimetricDataAsync(database, where?.ToCalorimetricInput(), locale, resolverContext, cancellationToken), + DataKind.GEOMETRIC_DATA => await HasGeometricDataAsync(database, where?.ToGeometricInput(), locale, resolverContext, cancellationToken), + DataKind.HYGROTHERMAL_DATA => await HasHygrothermalDataAsync(database, where?.ToHygrothermalInput(), locale, resolverContext, cancellationToken), + DataKind.LIFE_CYCLE_DATA => await HasLifeCycleDataAsync(database, where?.ToLifeCycleInput(), locale, resolverContext, cancellationToken), + DataKind.OPTICAL_DATA => await HasOpticalDataAsync(database, where?.ToOpticalInput(), locale, resolverContext, cancellationToken), + DataKind.PHOTOVOLTAIC_DATA => await HasPhotovoltaicDataAsync(database, where?.ToPhotovoltaiInput(), locale, resolverContext, cancellationToken), + _ => throw new ArgumentOutOfRangeException($"The data kind {kind} is not supported.") + }; + } + + public async Task GetCalorimetricDataAsync( + Database database, + Guid id, + [GraphQLType] string? locale, + IResolverContext? resolverContext, + CancellationToken cancellationToken + ) + { + return (await QueryDatabase( + database, + new GraphQLRequest( + await QueryingDatabases.ConstructQuery( + s_calorimetricDataFileNames + ), + new + { + id, + locale + }, + nameof(CalorimetricData) + ), + resolverContext, + cancellationToken + ) + )?.CalorimetricData; + } + + public async Task GetGeometricDataAsync( + Database database, + Guid id, + [GraphQLType] string? locale, + IResolverContext? resolverContext, + CancellationToken cancellationToken + ) + { + return (await QueryDatabase( + database, + new GraphQLRequest( + await QueryingDatabases.ConstructQuery( + s_geometricDataFileNames + ), + new + { + id, + locale + }, + nameof(GeometricData) + ), + resolverContext, + cancellationToken + ) + )?.GeometricData; + } + + public async Task GetHygrothermalDataAsync( + Database database, + Guid id, + [GraphQLType] string? locale, + IResolverContext? resolverContext, + CancellationToken cancellationToken + ) + { + return (await QueryDatabase( + database, + new GraphQLRequest( + await QueryingDatabases.ConstructQuery( + s_hygrothermalDataFileNames + ), + new + { + id, + locale + }, + nameof(HygrothermalData) + ), + resolverContext, + cancellationToken + ) + )?.HygrothermalData; + } + + public async Task GetLifeCycleDataAsync( + Database database, + Guid id, + [GraphQLType] string? locale, + IResolverContext? resolverContext, + CancellationToken cancellationToken + ) + { + return (await QueryDatabase( + database, + new GraphQLRequest( + await QueryingDatabases.ConstructQuery( + s_lifeCycleDataFileNames + ), + new + { + id, + locale + }, + nameof(LifeCycleData) + ), + resolverContext, + cancellationToken + ) + )?.LifeCycleData; + } + + + public async Task GetOpticalDataAsync( + Database database, + Guid id, + [GraphQLType] string? locale, + IResolverContext? resolverContext, + CancellationToken cancellationToken + ) + { + return (await QueryDatabase( + database, + new GraphQLRequest( + await QueryingDatabases.ConstructQuery( + s_opticalDataFileNames + ), + new + { + id, + locale + }, + nameof(OpticalData) + ), + resolverContext, + cancellationToken + ) + )?.OpticalData; + } + + public async Task GetPhotovoltaicDataAsync( + Database database, + Guid id, + [GraphQLType] string? locale, + IResolverContext? resolverContext, + CancellationToken cancellationToken + ) + { + return (await QueryDatabase( + database, + new GraphQLRequest( + await QueryingDatabases.ConstructQuery( + s_photovoltaicDataFileNames + ), + new + { + id, + locale + }, + nameof(PhotovoltaicData) + ), + resolverContext, + cancellationToken + ) + )?.PhotovoltaicData; + } + + public async Task GetAllCalorimetricDataAsync( + Database database, + CalorimetricDataPropositionInput? where, + [GraphQLType] string? locale, + int? first, + string? after, + int? last, + string? before, + IResolverContext? resolverContext, + CancellationToken cancellationToken + ) + { + return (await QueryDatabase( + database, + new GraphQLRequest( + await QueryingDatabases.ConstructQuery( + s_allCalorimetricDataFileNames + ), + new + { + where, + locale, + first, + after, + last, + before + }, + "AllCalorimetricData" + ), + resolverContext, + cancellationToken + ) + )?.AllCalorimetricData; + } + + public async Task GetAllGeometricDataAsync( + Database database, + GeometricDataPropositionInput? where, + [GraphQLType] string? locale, + int? first, + string? after, + int? last, + string? before, + IResolverContext? resolverContext, + CancellationToken cancellationToken + ) + { + return (await QueryDatabase( + database, + new GraphQLRequest( + await QueryingDatabases.ConstructQuery( + s_allGeometricDataFileNames), + new + { + where, + locale, + first, + after, + last, + before + }, + "AllGeometricData" + ), + resolverContext, + cancellationToken + ) + )?.AllGeometricData; + } + + public async Task GetAllHygrothermalDataAsync( + Database database, + HygrothermalDataPropositionInput? where, + [GraphQLType] string? locale, + int? first, + string? after, + int? last, + string? before, + IResolverContext? resolverContext, + CancellationToken cancellationToken + ) + { + return (await QueryDatabase( + database, + new GraphQLRequest( + await QueryingDatabases.ConstructQuery( + s_allHygrothermalDataFileNames + ), + new + { + where, + locale, + first, + after, + last, + before + }, + "AllHygrothermalData" + ), + resolverContext, + cancellationToken + ) + )?.AllHygrothermalData; + } + + public async Task GetAllLifeCycleDataAsync( + Database database, + LifeCycleDataPropositionInput? where, + [GraphQLType] string? locale, + int? first, + string? after, + int? last, + string? before, + IResolverContext? resolverContext, + CancellationToken cancellationToken + ) + { + return (await QueryDatabase( + database, + new GraphQLRequest( + await QueryingDatabases.ConstructQuery( + s_allLifeCycleDataFileNames + ), + new + { + where, + locale, + first, + after, + last, + before + }, + "AllLifeCycleData" + ), + resolverContext, + cancellationToken + ) + )?.AllLifeCycleData; + } + + public async Task GetAllOpticalDataAsync( + Database database, + OpticalDataPropositionInput? where, + [GraphQLType] string? locale, + int? first, + string? after, + int? last, + string? before, + IResolverContext? resolverContext, + CancellationToken cancellationToken + ) + { + return (await QueryDatabase( + database, + new GraphQLRequest( + await QueryingDatabases.ConstructQuery( + s_allOpticalDataFileNames), + new + { + where, + locale, + first, + after, + last, + before + }, + "AllOpticalData" + ), + resolverContext, + cancellationToken + ) + )?.AllOpticalData; + } + + public async Task GetAllPhotovoltaicDataAsync( + Database database, + PhotovoltaicDataPropositionInput? where, + [GraphQLType] string? locale, + int? first, + string? after, + int? last, + string? before, + IResolverContext? resolverContext, + CancellationToken cancellationToken + ) + { + return (await QueryDatabase( + database, + new GraphQLRequest( + await QueryingDatabases.ConstructQuery( + s_allPhotovoltaicDataFileNames + ), + new + { + where, + locale, + first, + after, + last, + before + }, + "AllPhotovoltaicData" + ), + resolverContext, + cancellationToken + ) + )?.AllPhotovoltaicData; + } + + public async Task HasCalorimetricDataAsync( + Database database, + CalorimetricDataPropositionInput? where, + [GraphQLType] string? locale, + IResolverContext? resolverContext, + CancellationToken cancellationToken + ) + { + return (await QueryDatabase( + database, + new GraphQLRequest( + await QueryingDatabases.ConstructQuery( + s_hasCalorimetricDataFileNames + ), + new + { + where, + locale + }, + "HasCalorimetricData" + ), + resolverContext, + cancellationToken + ) + )?.HasCalorimetricData; + } + + public async Task HasGeometricDataAsync( + Database database, + GeometricDataPropositionInput? where, + [GraphQLType] string? locale, + IResolverContext? resolverContext, + CancellationToken cancellationToken + ) + { + return (await QueryDatabase( + database, + new GraphQLRequest( + await QueryingDatabases.ConstructQuery( + s_hasGeometricDataFileNames + ), + new + { + where, + locale + }, + "HasGeometricData" + ), + resolverContext, + cancellationToken + ) + )?.HasGeometricData; + } + + public async Task HasHygrothermalDataAsync( + Database database, + HygrothermalDataPropositionInput? where, + [GraphQLType] string? locale, + IResolverContext? resolverContext, + CancellationToken cancellationToken + ) + { + return (await QueryDatabase( + database, + new GraphQLRequest( + await QueryingDatabases.ConstructQuery( + s_hasHygrothermalDataFileNames + ), + new + { + where, + locale + }, + "HasHygrothermalData" + ), + resolverContext, + cancellationToken + ) + )?.HasHygrothermalData; + } + + public async Task HasLifeCycleDataAsync( + Database database, + LifeCycleDataPropositionInput? where, + [GraphQLType] string? locale, + IResolverContext? resolverContext, + CancellationToken cancellationToken + ) + { + return (await QueryDatabase( + database, + new GraphQLRequest( + await QueryingDatabases.ConstructQuery( + s_hasLifeCycleDataFileNames + ), + new + { + where, + locale + }, + "HasLifeCycleData" + ), + resolverContext, + cancellationToken + ) + )?.HasLifeCycleData; + } + + public async Task HasOpticalDataAsync( + Database database, + OpticalDataPropositionInput? where, + [GraphQLType] string? locale, + IResolverContext? resolverContext, + CancellationToken cancellationToken + ) + { + return (await QueryDatabase( + database, + new GraphQLRequest( + await QueryingDatabases.ConstructQuery( + s_hasOpticalDataFileNames + ), + new + { + where, + locale + }, + "HasOpticalData" + ), + resolverContext, + cancellationToken + ) + )?.HasOpticalData; + } + + public async Task HasPhotovoltaicDataAsync( + Database database, + PhotovoltaicDataPropositionInput? where, + [GraphQLType] string? locale, + IResolverContext? resolverContext, + CancellationToken cancellationToken + ) + { + return (await QueryDatabase( + database, + new GraphQLRequest( + await QueryingDatabases.ConstructQuery( + s_hasPhotovoltaicDataFileNames + ), + new + { + where, + locale + }, + "HasPhotovoltaicData" + ), + resolverContext, + cancellationToken + ) + )?.HasPhotovoltaicData; + } + + private Task QueryDatabase( + Database database, + GraphQLRequest request, + IResolverContext? resolverContext, + CancellationToken cancellationToken + ) + where TGraphQlResponse : class + { + return graphQlRequestHelper.TransformExceptionsAsync( + async () => + { + var deserializedGraphQlResponse = + await queryingDatabases.QueryDatabase( + database, + request, + cancellationToken + ); + if (deserializedGraphQlResponse.Errors?.Length > 0) + { + logger.FailedWithErrors( + JsonSerializer.Serialize(deserializedGraphQlResponse.Errors), + database.Locator, + JsonSerializer.Serialize(request, JsonSerializerSettings.GraphQl) + ); + foreach (var error in deserializedGraphQlResponse.Errors) + { + var errorBuilder = ErrorBuilder.New() + .SetCode("DATABASE_QUERY_ERROR") + // .SetPath(error.Path) // TODO Add the error path. Just using `error.Path` does not work as it contains non-"GraphQlName"s according to HotChocolate sometimes. + .SetMessage( + $"The GraphQL response received from the database {database.Locator} for the request {JsonSerializer.Serialize(request, JsonSerializerSettings.GraphQl)} reported the error {error.Message}."); + if (error.Extensions is not null) + { + foreach (var (key, value) in error.Extensions) + { + errorBuilder.SetExtension(key, value); + } + } + // TODO Add `error.Locations` to `errorBuilder`. + if (resolverContext is null) + { + throw new GraphQLException(errorBuilder.Build()); + } + resolverContext.ReportError(errorBuilder.Build()); + } + } + return deserializedGraphQlResponse.Data; + }, + database.Locator, + request, + resolverContext + ); + } + + private sealed record OpticalDataData(OpticalData OpticalData); + private sealed record HygrothermalDataData(HygrothermalData HygrothermalData); + private sealed record LifeCycleDataData(LifeCycleData LifeCycleData); + private sealed record CalorimetricDataData(CalorimetricData CalorimetricData); + private sealed record PhotovoltaicDataData(PhotovoltaicData PhotovoltaicData); + private sealed record GeometricDataData(GeometricData GeometricData); + private sealed record AllOpticalDataData(OpticalDataConnection? AllOpticalData); + private sealed record AllHygrothermalDataData(HygrothermalDataConnection? AllHygrothermalData); + private sealed record AllLifeCycleDataData(LifeCycleDataConnection? AllLifeCycleData); + private sealed record AllCalorimetricDataData(CalorimetricDataConnection? AllCalorimetricData); + private sealed record AllGeometricDataData(GeometricDataConnection? AllGeometricData); + private sealed record AllPhotovoltaicDataData(PhotovoltaicDataConnection? AllPhotovoltaicData); + private sealed record HasOpticalDataData(bool HasOpticalData); + private sealed record HasCalorimetricDataData(bool HasCalorimetricData); + private sealed record HasGeometricDataData(bool HasGeometricData); + private sealed record HasHygrothermalDataData(bool HasHygrothermalData); + private sealed record HasLifeCycleDataData(bool HasLifeCycleData); + private sealed record HasPhotovoltaicDataData(bool HasPhotovoltaicData); +} \ No newline at end of file diff --git a/backend/src/GraphQl/Requests/GraphQlRequestHelper.cs b/backend/src/GraphQl/Requests/GraphQlRequestHelper.cs new file mode 100644 index 000000000..ca1dedd52 --- /dev/null +++ b/backend/src/GraphQl/Requests/GraphQlRequestHelper.cs @@ -0,0 +1,137 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using GraphQL; +using HotChocolate; +using HotChocolate.Resolvers; +using Metabase.Json; +using Microsoft.Extensions.Logging; + +namespace Metabase.GraphQl.Requests; + +public static partial class Log +{ + [LoggerMessage( + Level = LogLevel.Error, + Message = "Failed with status code {StatusCode} to request {Locator} for {Request}.")] + public static partial void FailedWithStatusCode( + this ILogger logger, + Exception exception, + HttpStatusCode? StatusCode, + Uri Locator, + string Request + ); + + [LoggerMessage( + Level = LogLevel.Error, + Message = "Failed to deserialize GraphQL response of request to {Locator} for {Request}. The details given are: Zero-based number of bytes read within the current line before the exception are {BytePositionInLine}, zero-based number of lines read before the exception are {LineNumber}, message that describes the current exception is '{Message}', path within the JSON where the exception was encountered is {Path}.")] + public static partial void FailedToDeserialize( + this ILogger logger, + Exception exception, + Uri Locator, + string Request, + long? BytePositionInLine, + long? LineNumber, + string Message, + string? Path + ); + + [LoggerMessage( + Level = LogLevel.Error, + Message = "Failed to request {Locator} for {Request} or failed to deserialize the response.")] + public static partial void FailedToRequestOrDeserialize( + this ILogger logger, + Exception exception, + Uri Locator, + string Request + ); +} + +public sealed class GraphQlRequestHelper( + ILogger logger +) +{ + public async Task TransformExceptionsAsync( + Func> action, + Uri databaseLocator, + GraphQLRequest request, + IResolverContext? resolverContext + ) + where T : class + { + try + { + return await action(); + } + catch (HttpRequestException exception) + { + logger.FailedWithStatusCode( + exception, + exception.StatusCode, + databaseLocator, + JsonSerializer.Serialize(request, JsonSerializerSettings.GraphQl) + ); + var error = + ErrorBuilder.New() + .SetCode("EXTERNAL_GRAPHQL_REQUEST_FAILED") + .SetPath(resolverContext?.Path) + .SetMessage($"Failed with status code '{exception.StatusCode}' to request the endpoint '{databaseLocator}' for {JsonSerializer.Serialize(request, JsonSerializerSettings.GraphQl)}.") + .SetException(exception) + .Build(); + if (resolverContext is null) + { + throw new GraphQLException(error); + } + resolverContext.ReportError(error); + return null; + } + catch (JsonException exception) + { + logger.FailedToDeserialize( + exception, + databaseLocator, + JsonSerializer.Serialize(request, JsonSerializerSettings.GraphQl), + exception.BytePositionInLine, + exception.LineNumber, + exception.Message, + exception.Path + ); + var error = + ErrorBuilder.New() + .SetCode("JSON_DESERIALIZATION_FAILED") + .SetPath(resolverContext?.Path) // TODO Add the error path. I would do it as follows as a workaround, however splitting the path at '.' is wrong in general: .SetPath(resolverContext.Path.ToList().Concat(e.Path?.Split('.') ?? []).ToList()) + .SetMessage($"Failed to deserialize the GraphQL response of the request to the endpoint '{databaseLocator}' for {JsonSerializer.Serialize(request, JsonSerializerSettings.GraphQl)}. The details given are: Zero-based number of bytes read within the current line before the exception are '{exception.BytePositionInLine}', zero-based number of lines read before the exception are '{exception.LineNumber}', message that describes the current exception is \"{exception.Message}\", path within the JSON where the exception was encountered is '{exception.Path}'.") + .SetException(exception) + .Build(); + if (resolverContext is null) + { + throw new GraphQLException(error); + } + resolverContext.ReportError(error); + return null; + } + catch (Exception exception) + { + logger.FailedToRequestOrDeserialize( + exception, + databaseLocator, + JsonSerializer.Serialize(request, JsonSerializerSettings.GraphQl) + ); + var error = + ErrorBuilder.New() + .SetCode("DATABASE_REQUEST_FAILED") + .SetPath(resolverContext?.Path) + .SetMessage($"Failed to request {databaseLocator} for {JsonSerializer.Serialize(request, JsonSerializerSettings.GraphQl)} or failed to deserialize the response.") + .SetException(exception) + .Build(); + if (resolverContext is null) + { + throw new GraphQLException(error); + } + resolverContext.ReportError(error); + return null; + } + } +} \ No newline at end of file diff --git a/backend/src/Services/QueryingDatabases.cs b/backend/src/GraphQl/Requests/QueryingDatabases.cs similarity index 79% rename from backend/src/Services/QueryingDatabases.cs rename to backend/src/GraphQl/Requests/QueryingDatabases.cs index 822ff8ffe..6d6f9b3c2 100644 --- a/backend/src/Services/QueryingDatabases.cs +++ b/backend/src/GraphQl/Requests/QueryingDatabases.cs @@ -21,7 +21,7 @@ using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Abstractions.OpenIddictExceptions; -namespace Metabase.Services; +namespace Metabase.GraphQl.Requests; public static partial class Log { @@ -130,8 +130,7 @@ public async QueryDatabase( Database database, GraphQLRequest request, - CancellationToken cancellationToken, - string? apiToken = null + CancellationToken cancellationToken ) where TGraphQlResponse : class { @@ -146,53 +145,24 @@ public async // // ) // .AsGraphQLHttpResponse(); - using var httpClient = httpClientFactory.CreateClient(DatabaseHttpClient); - // Set the authorization header to a given API token or the bearer token - // from the original HTTP request. Note that we cannot pass the API - // token as well as the bearer token in one request, neither with - // multiple HTTP authorization headers nor with one header with - // a comma-separated list of authentication scheme and value pairs. - // Both go against RFC 7230/7235, even though some web servers accept - // multiple schemes. For details see - // https://stackoverflow.com/questions/29282578/multiple-http-authorization-headers - if (apiToken is not null) - { - httpClient.SetToken("Token", apiToken); - } - else - { - // Create and set restricted access token that can be used to - // identify the authenticated user. - var accessToken = await CreateRestrictedAccessTokenForDatabaseAsync( - database, - cancellationToken - ); - if (accessToken is not null) - { - httpClient.SetBearerToken(accessToken); - } - } + using var httpClient = await CreateHttpClientAsync(database, cancellationToken); // For some reason `httpClient.PostAsJsonAsync` without `MakeJsonHttpContent` but with `SerializerOptions` results in `BadRequest` status code. It has to do with `JsonContent.Create` used within `PostAsJsonAsync` --- we also cannot use `JsonContent.Create` in `MakeJsonHttpContent`. What is happening here? using var jsonHttpContent = MakeJsonHttpContent(request); - jsonHttpContent.Headers.Add( - HeaderNames.Origin, - appSettings.Uri.AbsoluteUri - ); using var httpResponseMessage = await httpClient.PostAsync( database.Locator, jsonHttpContent, cancellationToken ); - if (httpResponseMessage.StatusCode != HttpStatusCode.OK) + if (httpResponseMessage.StatusCode is not HttpStatusCode.OK) { throw new HttpRequestException( $"The status code is not {HttpStatusCode.OK} but {httpResponseMessage.StatusCode}.", null, - httpResponseMessage.StatusCode); + httpResponseMessage.StatusCode + ); } - // We could use `httpResponseMessage.Content.ReadFromJsonAsync>` which would make debugging more difficult though, https://docs.microsoft.com/en-us/dotnet/api/system.net.http.json.httpcontentjsonextensions.readfromjsonasync?view=net-5.0#System_Net_Http_Json_HttpContentJsonExtensions_ReadFromJsonAsync__1_System_Net_Http_HttpContent_System_Text_Json_JsonSerializerOptions_System_Threading_CancellationToken_ - using var graphQlResponseStream = + await using var graphQlResponseStream = await httpResponseMessage.Content .ReadAsStreamAsync(cancellationToken); // For debugging, the following lines of code write the response to standard output. @@ -202,10 +172,53 @@ await JsonSerializer.DeserializeAsync>( graphQlResponseStream, JsonSerializerSettings.GraphQl, cancellationToken - ) ?? throw new JsonException("Failed to deserialize the GraphQL response."); + ) + ?? throw new JsonException("Failed to deserialize the GraphQL response."); return deserializedGraphQlResponse; } + internal async Task CreateHttpClientAsync( + Database database, + CancellationToken cancellationToken + ) + { + var httpClient = httpClientFactory.CreateClient(QueryingDatabases.DatabaseHttpClient); + httpClient.DefaultRequestHeaders.Add( + HeaderNames.Origin, + appSettings.Uri.AbsoluteUri + ); + // Set the authorization header to a restricted bearer token from the + // original HTTP request. The restricted token can be used to identify + // the authenticated user. + var accessToken = await CreateRestrictedAccessTokenForDatabaseAsync( + database, + cancellationToken + ); + if (accessToken is not null) + { + httpClient.SetBearerToken(accessToken); + } + switch (database.OperatorId) + { + case var id when id == new Guid(DataConstants.LbnlInstitutionUuid): + // Pass the API token instead of the access token. Note that we + // cannot pass the API token as well as the bearer token in one + // request, neither with multiple HTTP authorization headers + // nor with one header with a comma-separated list of + // authentication scheme and value pairs. Both go against RFC + // 7230/7235, even though some web servers accept multiple + // schemes. For details see + // https://stackoverflow.com/questions/29282578/multiple-http-authorization-headers + httpClient.SetToken("Token", appSettings.Igsdb.ApiToken); + break; + case var id when id == new Guid(DataConstants.EpeaInstitutionUuid): + httpClient.DefaultRequestHeaders.Add("x-user-id", appSettings.Epea.UserId); + httpClient.DefaultRequestHeaders.Add("x-api-key", appSettings.Epea.ApiKey); + break; + } + return httpClient; + } + // private GraphQLHttpClient CreateGraphQlClient( // Data.Database database // ) diff --git a/backend/src/GraphQl/Scalars/ArXivType.cs b/backend/src/GraphQl/Scalars/ArXivType.cs new file mode 100644 index 000000000..987bdcc04 --- /dev/null +++ b/backend/src/GraphQl/Scalars/ArXivType.cs @@ -0,0 +1,31 @@ +using System.Text.RegularExpressions; +using HotChocolate.Types; + +namespace Metabase.GraphQl.Scalars; + +public sealed class ArXivType( + string name, + string? description = null, + BindingBehavior bind = BindingBehavior.Explicit +) +: RegexType( + name, + ValidationPattern, + description, + RegexOptions.Compiled | RegexOptions.IgnoreCase, + bind +) +{ + private const string ValidationPattern = "^(arXiv:)[0-z./]*$"; + + public ArXivType() + : this( + nameof(ArXivType)[..^"Type".Length], + """ + arXiv identifier, that is, a string of the format + [Understanding the arXiv identifier](https://info.arxiv.org/help/arxiv_identifier.html). + """ + ) + { + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Scalars/DoiType.cs b/backend/src/GraphQl/Scalars/DoiType.cs new file mode 100644 index 000000000..8e8620258 --- /dev/null +++ b/backend/src/GraphQl/Scalars/DoiType.cs @@ -0,0 +1,40 @@ +using System.Text.RegularExpressions; +using HotChocolate.Types; + +namespace Metabase.GraphQl.Scalars; + +public sealed class DoiType( + string name, + string? description = null, + BindingBehavior bind = BindingBehavior.Explicit +) +: RegexType( + name, + ValidationPattern, + description, + RegexOptions.Compiled | RegexOptions.IgnoreCase, + bind +) +{ + private const string ValidationPattern = "^(10[.][0-z/.]*)$"; + + public DoiType() + : this( + nameof(DoiType)[..^"Type".Length], + """ + Digital Object Identifier (DOI) name, that is, a string compliant with [ISO + 26324:2025](https://www.iso.org/standard/88862.html). + + See also + [What is a DOI?](https://www.doi.org/the-identifier/what-is-a-doi/). In short, + a DOI name is a digital identifier of an object, any object — physical, + digital, or abstract. DOIs solve a common problem: keeping track of things. + Things can be matter, material, content, or activities. Designed to be used by + humans as well as machines, DOIs identify objects persistently. They allow + things to be uniquely identified and accessed reliably. You know what you have, + where it is, and others can track it too. + """ + ) + { + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Scalars/GraphQlQueryType.cs b/backend/src/GraphQl/Scalars/GraphQlQueryType.cs new file mode 100644 index 000000000..b47aa7b25 --- /dev/null +++ b/backend/src/GraphQl/Scalars/GraphQlQueryType.cs @@ -0,0 +1,30 @@ +using HotChocolate.Types; + +namespace Metabase.GraphQl.Scalars; + +public sealed class GraphQlQueryType( + string name, + string? description = null, + BindingBehavior bind = BindingBehavior.Explicit +) +: StringType( + name, + description, + bind +) +{ + public GraphQlQueryType() + : this( + nameof(GraphQlQueryType)[..^"Type".Length], + """ + [September 2025 GraphQL](https://spec.graphql.org/September2025/) + compliant + [query](https://spec.graphql.org/September2025/#sec-Language.Operations) + string without + [variables](http://spec.graphql.org/September2025/#sec-Language.Variables) + and without unnecessary white-space for the present schema. + """ + ) + { + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Scalars/LocaleType.cs b/backend/src/GraphQl/Scalars/LocaleType.cs new file mode 100644 index 000000000..794770edf --- /dev/null +++ b/backend/src/GraphQl/Scalars/LocaleType.cs @@ -0,0 +1,45 @@ +using System.Text.RegularExpressions; +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 Metabase.GraphQl.Scalars; + +public sealed class LocaleType( + string name, + string? description = null, + BindingBehavior bind = BindingBehavior.Explicit +) +: RegexType( + name, + ValidationPattern, + description, + RegexOptions.Compiled | RegexOptions.IgnoreCase, + bind +) +{ + private const string ValidationPattern = + "^[a-zA-Z0-9]+(-[a-zA-Z0-9]+)?(-[a-zA-Z0-9]+)?$"; + + public LocaleType() + : this( + nameof(LocaleType)[..^"Type".Length], + """ + [BCP 47](https://tools.ietf.org/html/bcp47) + compliant + [Language Tag](https://tools.ietf.org/html/bcp47#section-2) + string like `"de-AT"`, `"sr-Latn-RS"`, `"en-US"`, or `"en-GB"`, where the language + part is essentially an + [ISO 639 Language Code](https://www.iso.org/iso-639-language-codes.html), + the script an + [ISO 15924:2004 Script Code](https://www.iso.org/standard/29546.html), + and the region an + [ISO 3166-1 Country Code](https://www.iso.org/iso-3166-country-codes.html). + Note that the + [Internet Assigned Numbers Authority (IANA)](https://www.iana.org) + maintains the + [Language Subtag Registry](https://www.iana.org/assignments/lang-subtags-templates/lang-subtags-templates.xhtml). + """ + ) + { + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Scalars/MyUriType.cs b/backend/src/GraphQl/Scalars/MyUriType.cs new file mode 100644 index 000000000..0633b6e86 --- /dev/null +++ b/backend/src/GraphQl/Scalars/MyUriType.cs @@ -0,0 +1,109 @@ +using System; +using System.Text.Json; +using HotChocolate.Features; +using HotChocolate.Language; +using HotChocolate.Text.Json; +using HotChocolate.Types; +using Microsoft.Extensions.DependencyInjection; + +namespace Metabase.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 000000000..9bc42972c --- /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 Metabase.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 000000000..b7fe329bd --- /dev/null +++ b/backend/src/GraphQl/Sorting.cs @@ -0,0 +1,18 @@ +using GreenDonut.Data; +using Metabase.Data; + +namespace Metabase.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/UserMethodDevelopers/AddUserMethodDeveloperPayload.cs b/backend/src/GraphQl/UserMethodDevelopers/AddUserMethodDeveloperPayload.cs index 8a3dcc892..fbc888cbe 100644 --- a/backend/src/GraphQl/UserMethodDevelopers/AddUserMethodDeveloperPayload.cs +++ b/backend/src/GraphQl/UserMethodDevelopers/AddUserMethodDeveloperPayload.cs @@ -11,7 +11,11 @@ public AddUserMethodDeveloperPayload( UserMethodDeveloper userMethodDeveloper ) { - DevelopedMethodEdge = new UserDevelopedMethodEdge(userMethodDeveloper); + DevelopedMethodEdge = new UserDevelopedMethodEdge( + userMethodDeveloper, + PaginationHelpers.ConstructCursor(userMethodDeveloper.MethodId, userMethodDeveloper.UserId) + + ); MethodDeveloperEdge = new UserMethodDeveloperEdge(userMethodDeveloper); } diff --git a/backend/src/GraphQl/UserMethodDevelopers/ConfirmUserMethodDeveloperPayload.cs b/backend/src/GraphQl/UserMethodDevelopers/ConfirmUserMethodDeveloperPayload.cs index b544691fc..83f860be7 100644 --- a/backend/src/GraphQl/UserMethodDevelopers/ConfirmUserMethodDeveloperPayload.cs +++ b/backend/src/GraphQl/UserMethodDevelopers/ConfirmUserMethodDeveloperPayload.cs @@ -11,7 +11,10 @@ public ConfirmUserMethodDeveloperPayload( UserMethodDeveloper userMethodDeveloper ) { - DevelopedMethodEdge = new UserDevelopedMethodEdge(userMethodDeveloper); + DevelopedMethodEdge = new UserDevelopedMethodEdge( + userMethodDeveloper, + PaginationHelpers.ConstructCursor(userMethodDeveloper.MethodId, userMethodDeveloper.UserId) + ); MethodDeveloperEdge = new UserMethodDeveloperEdge(userMethodDeveloper); } diff --git a/backend/src/GraphQl/UserMethodDevelopers/RemoveUserMethodDeveloperPayload.cs b/backend/src/GraphQl/UserMethodDevelopers/RemoveUserMethodDeveloperPayload.cs index 1898be427..3fb77511a 100644 --- a/backend/src/GraphQl/UserMethodDevelopers/RemoveUserMethodDeveloperPayload.cs +++ b/backend/src/GraphQl/UserMethodDevelopers/RemoveUserMethodDeveloperPayload.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Metabase.Data; using Metabase.GraphQl.Methods; using Metabase.GraphQl.Users; @@ -7,12 +9,13 @@ namespace Metabase.GraphQl.UserMethodDevelopers; public sealed class RemoveUserMethodDeveloperPayload { + private readonly UserMethodDeveloper? _association; + public RemoveUserMethodDeveloperPayload( UserMethodDeveloper userMethodDeveloper ) { - DevelopedMethodEdge = new UserDevelopedMethodEdge(userMethodDeveloper); - MethodDeveloperEdge = new UserMethodDeveloperEdge(userMethodDeveloper); + _association = userMethodDeveloper; } public RemoveUserMethodDeveloperPayload( @@ -29,7 +32,29 @@ RemoveUserMethodDeveloperError error { } - public UserDevelopedMethodEdge? DevelopedMethodEdge { get; } - public UserMethodDeveloperEdge? MethodDeveloperEdge { get; } public IReadOnlyCollection? Errors { get; } + + public async Task GetMethodAsync( + IMethodByIdDataLoader byId, + CancellationToken cancellationToken + ) + { + if (_association is null) + { + return null; + } + return await byId.LoadAsync(_association.MethodId, cancellationToken); + } + + public async Task GetUserAsync( + IUserByIdDataLoader byId, + CancellationToken cancellationToken + ) + { + if (_association is null) + { + return null; + } + return await byId.LoadAsync(_association.UserId, cancellationToken); + } } \ No newline at end of file diff --git a/backend/src/GraphQl/UserMethodDevelopers/UserMethodDeveloperFilterType.cs b/backend/src/GraphQl/UserMethodDevelopers/UserMethodDeveloperFilterType.cs index fb499f743..aed0b81e7 100644 --- a/backend/src/GraphQl/UserMethodDevelopers/UserMethodDeveloperFilterType.cs +++ b/backend/src/GraphQl/UserMethodDevelopers/UserMethodDeveloperFilterType.cs @@ -1,17 +1,21 @@ using HotChocolate.Data.Filters; using Metabase.Data; +using Metabase.GraphQl.Associations; namespace Metabase.GraphQl.UserMethodDevelopers; public abstract class UserMethodDeveloperFilterType - : FilterInputType + : AuditableAssociationFilterType { protected override void Configure( IFilterInputTypeDescriptor descriptor ) { - descriptor.BindFieldsExplicitly(); - descriptor.Field(x => x.Method); - descriptor.Field(x => x.User); + base.Configure(descriptor); + // TODO Remove CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(_ => _.CreatedAt); + descriptor.Field(_ => _.UpdatedAt); + descriptor.Field(_ => _.Method); + descriptor.Field(_ => _.User); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/UserMethodDevelopers/UserMethodDeveloperSortType.cs b/backend/src/GraphQl/UserMethodDevelopers/UserMethodDeveloperSortType.cs index 7c8b7ce69..f57269358 100644 --- a/backend/src/GraphQl/UserMethodDevelopers/UserMethodDeveloperSortType.cs +++ b/backend/src/GraphQl/UserMethodDevelopers/UserMethodDeveloperSortType.cs @@ -1,17 +1,17 @@ using HotChocolate.Data.Sorting; using Metabase.Data; +using Metabase.GraphQl.Associations; namespace Metabase.GraphQl.UserMethodDevelopers; -public sealed class UserMethodDeveloperSortType - : SortInputType +public abstract class UserMethodDeveloperSortType + : AuditableAssociationSortType { protected override void Configure( ISortInputTypeDescriptor descriptor ) { - descriptor.BindFieldsExplicitly(); - descriptor.Field(x => x.Method); - descriptor.Field(x => x.User); + base.Configure(descriptor); + descriptor.Name(nameof(UserMethodDeveloperSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); } } \ No newline at end of file diff --git a/backend/src/GraphQl/Users/ChangeUserEmailInput.cs b/backend/src/GraphQl/Users/ChangeUserEmailInput.cs index a5a1625f1..57e66b900 100644 --- a/backend/src/GraphQl/Users/ChangeUserEmailInput.cs +++ b/backend/src/GraphQl/Users/ChangeUserEmailInput.cs @@ -1,5 +1,8 @@ +using HotChocolate; +using HotChocolate.Types; + namespace Metabase.GraphQl.Users; public sealed record ChangeUserEmailInput( - string NewEmail + [property: GraphQLType>] string NewEmail ); \ No newline at end of file diff --git a/backend/src/GraphQl/Users/ConfirmUserEmailChangeInput.cs b/backend/src/GraphQl/Users/ConfirmUserEmailChangeInput.cs index fd7c4ae82..bec2e74f4 100644 --- a/backend/src/GraphQl/Users/ConfirmUserEmailChangeInput.cs +++ b/backend/src/GraphQl/Users/ConfirmUserEmailChangeInput.cs @@ -1,7 +1,10 @@ +using HotChocolate; +using HotChocolate.Types; + namespace Metabase.GraphQl.Users; public sealed record ConfirmUserEmailChangeInput( - string CurrentEmail, - string NewEmail, + [property: GraphQLType>] string CurrentEmail, + [property: GraphQLType>] string NewEmail, string ConfirmationCode -); \ No newline at end of file +); diff --git a/backend/src/GraphQl/Users/ConfirmUserEmailInput.cs b/backend/src/GraphQl/Users/ConfirmUserEmailInput.cs index 7057bc03e..053aa534c 100644 --- a/backend/src/GraphQl/Users/ConfirmUserEmailInput.cs +++ b/backend/src/GraphQl/Users/ConfirmUserEmailInput.cs @@ -1,6 +1,9 @@ +using HotChocolate; +using HotChocolate.Types; + namespace Metabase.GraphQl.Users; public sealed record ConfirmUserEmailInput( - string Email, + [property: GraphQLType>] string Email, string ConfirmationCode -); \ No newline at end of file +); diff --git a/backend/src/GraphQl/Users/GnuPgKeyFingerprintsByUserIdDataLoader.cs b/backend/src/GraphQl/Users/GnuPgKeyFingerprintsByUserIdDataLoader.cs deleted file mode 100644 index df06f7d92..000000000 --- a/backend/src/GraphQl/Users/GnuPgKeyFingerprintsByUserIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Users; - -public sealed class GnuPgKeyFingerprintsByUserIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.GnuPgKeyFingerprints.AsNoTracking().Where(x => - ids.Contains(x.UserId) - ).With(queryContext), - x => x.UserId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Users/LoginUserInput.cs b/backend/src/GraphQl/Users/LoginUserInput.cs index e3f2a0801..77c4937e9 100644 --- a/backend/src/GraphQl/Users/LoginUserInput.cs +++ b/backend/src/GraphQl/Users/LoginUserInput.cs @@ -1,6 +1,9 @@ +using HotChocolate; +using HotChocolate.Types; + namespace Metabase.GraphQl.Users; public sealed record LoginUserInput( - string Email, + [property: GraphQLType>] string Email, string Password -); \ No newline at end of file +); diff --git a/backend/src/GraphQl/Users/PendingUserDevelopedMethodsByUserIdDataLoader.cs b/backend/src/GraphQl/Users/PendingUserDevelopedMethodsByUserIdDataLoader.cs deleted file mode 100644 index f3ea2f2e6..000000000 --- a/backend/src/GraphQl/Users/PendingUserDevelopedMethodsByUserIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Users; - -public sealed class PendingUserDevelopedMethodsByUserIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.UserMethodDevelopers.AsNoTracking().Where(x => - x.Pending && ids.Contains(x.UserId) - ).With(queryContext), - x => x.UserId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Users/PendingUserRepresentedInstitutionsByUserIdDataLoader.cs b/backend/src/GraphQl/Users/PendingUserRepresentedInstitutionsByUserIdDataLoader.cs deleted file mode 100644 index 5ff187cb1..000000000 --- a/backend/src/GraphQl/Users/PendingUserRepresentedInstitutionsByUserIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Users; - -public sealed class PendingUserRepresentedInstitutionsByUserIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.InstitutionRepresentatives.AsNoTracking().Where(x => - x.Pending && ids.Contains(x.UserId) - ).With(queryContext), - x => x.UserId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Users/RegisterUserInput.cs b/backend/src/GraphQl/Users/RegisterUserInput.cs index 1e449371a..94c4c8f97 100644 --- a/backend/src/GraphQl/Users/RegisterUserInput.cs +++ b/backend/src/GraphQl/Users/RegisterUserInput.cs @@ -1,11 +1,13 @@ using System; +using HotChocolate; +using HotChocolate.Types; namespace Metabase.GraphQl.Users; public sealed record RegisterUserInput( string Name, - string Email, + [property: GraphQLType>] string Email, string Password, string PasswordConfirmation, Uri? ReturnTo -); \ No newline at end of file +); diff --git a/backend/src/GraphQl/Users/RemoveUserRoleErrorCode.cs b/backend/src/GraphQl/Users/RemoveUserRoleErrorCode.cs index 6e8567b86..bce048268 100644 --- a/backend/src/GraphQl/Users/RemoveUserRoleErrorCode.cs +++ b/backend/src/GraphQl/Users/RemoveUserRoleErrorCode.cs @@ -7,5 +7,6 @@ public enum RemoveUserRoleErrorCode { UNKNOWN, UNKNOWN_USER, + LAST_USER, UNAUTHORIZED } \ No newline at end of file diff --git a/backend/src/GraphQl/Users/RequestUserPasswordResetInput.cs b/backend/src/GraphQl/Users/RequestUserPasswordResetInput.cs index 150fe7407..24e1de474 100644 --- a/backend/src/GraphQl/Users/RequestUserPasswordResetInput.cs +++ b/backend/src/GraphQl/Users/RequestUserPasswordResetInput.cs @@ -1,8 +1,10 @@ using System; +using HotChocolate; +using HotChocolate.Types; namespace Metabase.GraphQl.Users; public sealed record RequestUserPasswordResetInput( - string Email, + [property: GraphQLType>] string Email, Uri? ReturnTo -); \ No newline at end of file +); diff --git a/backend/src/GraphQl/Users/ResendUserEmailConfirmationInput.cs b/backend/src/GraphQl/Users/ResendUserEmailConfirmationInput.cs index 9dd08e69b..a09bc2ef4 100644 --- a/backend/src/GraphQl/Users/ResendUserEmailConfirmationInput.cs +++ b/backend/src/GraphQl/Users/ResendUserEmailConfirmationInput.cs @@ -1,5 +1,8 @@ +using HotChocolate; +using HotChocolate.Types; + namespace Metabase.GraphQl.Users; public sealed record ResendUserEmailConfirmationInput( - string Email -); \ No newline at end of file + [property: GraphQLType>] string Email +); diff --git a/backend/src/GraphQl/Users/ResetUserPasswordInput.cs b/backend/src/GraphQl/Users/ResetUserPasswordInput.cs index 26081f7fe..58c7eb7d2 100644 --- a/backend/src/GraphQl/Users/ResetUserPasswordInput.cs +++ b/backend/src/GraphQl/Users/ResetUserPasswordInput.cs @@ -1,8 +1,11 @@ +using HotChocolate; +using HotChocolate.Types; + namespace Metabase.GraphQl.Users; public sealed record ResetUserPasswordInput( - string Email, + [property: GraphQLType>] string Email, string Password, string PasswordConfirmation, string ResetCode -); \ No newline at end of file +); diff --git a/backend/src/GraphQl/Users/SetUserPhoneNumberInput.cs b/backend/src/GraphQl/Users/SetUserPhoneNumberInput.cs index 3c992d145..766f3ac29 100644 --- a/backend/src/GraphQl/Users/SetUserPhoneNumberInput.cs +++ b/backend/src/GraphQl/Users/SetUserPhoneNumberInput.cs @@ -1,5 +1,8 @@ +using HotChocolate; +using HotChocolate.Types; + namespace Metabase.GraphQl.Users; public sealed record SetUserPhoneNumberInput( - string PhoneNumber + [property: GraphQLType>] string PhoneNumber ); \ No newline at end of file diff --git a/backend/src/GraphQl/Users/UseSignInManagerAttribute.cs b/backend/src/GraphQl/Users/UseSignInManagerAttribute.cs index 307d21305..9af8aefa2 100644 --- a/backend/src/GraphQl/Users/UseSignInManagerAttribute.cs +++ b/backend/src/GraphQl/Users/UseSignInManagerAttribute.cs @@ -9,7 +9,7 @@ public sealed class UseSignInManagerAttribute : ObjectFieldDescriptorAttribute protected override void OnConfigure( IDescriptorContext context, IObjectFieldDescriptor descriptor, - MemberInfo member + MemberInfo? member ) { descriptor.UseSignInManager(); diff --git a/backend/src/GraphQl/Users/UseUserManagerAttribute.cs b/backend/src/GraphQl/Users/UseUserManagerAttribute.cs index e3a28d06b..df89bdd14 100644 --- a/backend/src/GraphQl/Users/UseUserManagerAttribute.cs +++ b/backend/src/GraphQl/Users/UseUserManagerAttribute.cs @@ -9,7 +9,7 @@ public sealed class UseUserManagerAttribute : ObjectFieldDescriptorAttribute protected override void OnConfigure( IDescriptorContext context, IObjectFieldDescriptor descriptor, - MemberInfo member + MemberInfo? member ) { descriptor.UseUserManager(); diff --git a/backend/src/GraphQl/Users/UserByIdDataLoader.cs b/backend/src/GraphQl/Users/UserByIdDataLoader.cs deleted file mode 100644 index fa1781de0..000000000 --- a/backend/src/GraphQl/Users/UserByIdDataLoader.cs +++ /dev/null @@ -1,20 +0,0 @@ -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.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 000000000..0ea945395 --- /dev/null +++ b/backend/src/GraphQl/Users/UserDataLoaders.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut; +using GreenDonut.Data; +using Metabase.Data; +using Microsoft.EntityFrameworkCore; + +namespace Metabase.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 + ); + } + + [DataLoader] + public static ValueTask> GetGnuPgKeyFingerprintsByUserIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetManyByOneIdAsync( + ids, + (databaseContext) => databaseContext.GnuPgKeyFingerprints, + _ => _.UserId, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask> GetUserRepresentedInstitutionsByUserIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetAssociationsByOneIdAsync( + ids, + (databaseContext) => databaseContext.InstitutionRepresentatives.Where(_ => !_.Pending), + _ => _.UserId, + _ => _.InstitutionId, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask> GetPendingUserRepresentedInstitutionsByUserIdAsync( + IReadOnlyList ids, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetAssociationsByOneIdAsync( + ids, + (databaseContext) => databaseContext.InstitutionRepresentatives.Where(_ => _.Pending), + _ => _.UserId, + _ => _.InstitutionId, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask>> GetUserDevelopedMethodsByUserIdAsync( + IReadOnlyList ids, + PagingArguments pagingArguments, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetAssociationsByOneIdAsync( + ids, + (databaseContext) => databaseContext.UserMethodDevelopers.Where(_ => !_.Pending), + _ => _.UserId, + _ => _.MethodId, + pagingArguments, + queryContext, + databaseContextFactory, + cancellationToken + ); + } + + [DataLoader] + public static ValueTask>> GetPendingUserDevelopedMethodsByUserIdAsync( + IReadOnlyList ids, + PagingArguments pagingArguments, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + { + return GetAssociationsByOneIdAsync( + ids, + (databaseContext) => databaseContext.UserMethodDevelopers.Where(_ => _.Pending), + _ => _.UserId, + _ => _.MethodId, + pagingArguments, + queryContext, + databaseContextFactory, + cancellationToken + ); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Users/UserDevelopedMethodConnection.cs b/backend/src/GraphQl/Users/UserDevelopedMethodConnection.cs index e54a1bcd7..271d009f0 100644 --- a/backend/src/GraphQl/Users/UserDevelopedMethodConnection.cs +++ b/backend/src/GraphQl/Users/UserDevelopedMethodConnection.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using GreenDonut.Data; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; @@ -9,11 +10,13 @@ namespace Metabase.GraphQl.Users; public sealed class UserDevelopedMethodConnection( User subject, + PagingArguments pagingArguments, QueryContext queryContext ) - : Connection( + : PaginatedConnection( subject, - x => new UserDevelopedMethodEdge(x), + (association, cursor) => new UserDevelopedMethodEdge(association, cursor), + pagingArguments, queryContext ) { @@ -21,17 +24,20 @@ QueryContext queryContext public sealed class PendingUserDevelopedMethodConnection( User subject, + PagingArguments pagingArguments, QueryContext queryContext ) - : AuthorizedConnection( + : AuthorizedPaginatedConnection( subject, - x => new UserDevelopedMethodEdge(x), - (claimsPrincipal, institution, authorization, cancellationToken) => - authorization.IsAuthorizedToConfirm(claimsPrincipal, institution.Id, cancellationToken), + (association, cursor) => new UserDevelopedMethodEdge(association, cursor), + (claimsPrincipal, authorization, cancellationToken) => + authorization.IsAuthorizedToConfirm(claimsPrincipal, subject.Id, cancellationToken), + pagingArguments, queryContext ) { [UseUserManager] + [Cost(1)] public Task IsAuthorizedToConfirmEdgesAsync( ClaimsPrincipal claimsPrincipal, UserMethodDeveloperAuthorization authorization, diff --git a/backend/src/GraphQl/Users/UserDevelopedMethodEdge.cs b/backend/src/GraphQl/Users/UserDevelopedMethodEdge.cs index 8e6539d47..7d8bb935b 100644 --- a/backend/src/GraphQl/Users/UserDevelopedMethodEdge.cs +++ b/backend/src/GraphQl/Users/UserDevelopedMethodEdge.cs @@ -1,11 +1,49 @@ +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate.CostAnalysis.Types; +using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Methods; namespace Metabase.GraphQl.Users; public sealed class UserDevelopedMethodEdge( - UserMethodDeveloper association - ) - : Edge(association.MethodId) + UserMethodDeveloper association, + string cursor +) +: PaginatedEdge( + association.MethodId, + cursor +) { + [UseUserManager] + [Cost(1)] + public Task IsAuthorizedToConfirmEdgeAsync( + ClaimsPrincipal claimsPrincipal, + UserMethodDeveloperAuthorization authorization, + CancellationToken cancellationToken + ) + { + return authorization.IsAuthorizedToConfirm( + claimsPrincipal, + association.UserId, + cancellationToken + ); + } + + [UseUserManager] + [Cost(1)] + public Task IsAuthorizedToRemoveEdgeAsync( + ClaimsPrincipal claimsPrincipal, + UserMethodDeveloperAuthorization authorization, + CancellationToken cancellationToken + ) + { + return authorization.IsAuthorizedToRemove( + claimsPrincipal, + association.MethodId, + cancellationToken + ); + } } \ No newline at end of file diff --git a/backend/src/GraphQl/Users/UserDevelopedMethodFilterType.cs b/backend/src/GraphQl/Users/UserDevelopedMethodFilterType.cs index ca47a4d4c..7ecc2fcc5 100644 --- a/backend/src/GraphQl/Users/UserDevelopedMethodFilterType.cs +++ b/backend/src/GraphQl/Users/UserDevelopedMethodFilterType.cs @@ -13,6 +13,6 @@ IFilterInputTypeDescriptor descriptor { base.Configure(descriptor); descriptor.Name(nameof(UserDevelopedMethodFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); - descriptor.Field(x => x.User).Ignore(); + descriptor.Field(_ => _.User).Ignore(); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Users/UserDevelopedMethodSortType.cs b/backend/src/GraphQl/Users/UserDevelopedMethodSortType.cs new file mode 100644 index 000000000..25151a358 --- /dev/null +++ b/backend/src/GraphQl/Users/UserDevelopedMethodSortType.cs @@ -0,0 +1,17 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data; +using Metabase.GraphQl.UserMethodDevelopers; + +namespace Metabase.GraphQl.Users; + +public sealed class UserDevelopedMethodSortType + : UserMethodDeveloperSortType +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.Name(nameof(UserDevelopedMethodSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Users/UserDevelopedMethodsByUserIdDataLoader.cs b/backend/src/GraphQl/Users/UserDevelopedMethodsByUserIdDataLoader.cs deleted file mode 100644 index 8c5f4c839..000000000 --- a/backend/src/GraphQl/Users/UserDevelopedMethodsByUserIdDataLoader.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Linq; -using GreenDonut; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Users; - -public sealed class UserDevelopedMethodsByUserIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.UserMethodDevelopers.AsNoTracking().Where(x => - !x.Pending && ids.Contains(x.UserId) - ).With(queryContext), - x => x.UserId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Users/UserFilterType.cs b/backend/src/GraphQl/Users/UserFilterType.cs index 57a5c5cb3..c46e31deb 100644 --- a/backend/src/GraphQl/Users/UserFilterType.cs +++ b/backend/src/GraphQl/Users/UserFilterType.cs @@ -5,21 +5,26 @@ namespace Metabase.GraphQl.Users; public sealed class UserFilterType - : EntityFilterType + : AuditableEntityFilterType { protected override void Configure( IFilterInputTypeDescriptor descriptor ) { base.Configure(descriptor); + descriptor.Name(nameof(UserFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); + // TODO Remove Id, CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(_ => _.Id); + descriptor.Field(_ => _.CreatedAt); + descriptor.Field(_ => _.UpdatedAt); // TODO The commented fields below should be filterable by OpenId Connect Clients and application users with the proper scopes and rights. If they are filterable in general, it is a way to figure out that information even if it is not returned by GraphQL. - // descriptor.Field(x => x.Name); - // descriptor.Field(x => x.Email); - // descriptor.Field(x => x.PostalAddress); - // descriptor.Field(x => x.WebsiteLocator); - descriptor.Field(x => x.DevelopedMethods); - descriptor.Field(x => x.DevelopedMethodEdges); - descriptor.Field(x => x.RepresentedInstitutions); - descriptor.Field(x => x.RepresentedInstitutionEdges); + descriptor.Field(_ => _.Name); + // descriptor.Field(_ => _.Email); + // descriptor.Field(_ => _.PostalAddress); + // descriptor.Field(_ => _.WebsiteLocator); + descriptor.Field(_ => _.DevelopedMethods); + descriptor.Field(_ => _.DevelopedMethodEdges); + descriptor.Field(_ => _.RepresentedInstitutions); + descriptor.Field(_ => _.RepresentedInstitutionEdges); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Users/UserGnuPgKeyFingerprintConnection.cs b/backend/src/GraphQl/Users/UserGnuPgKeyFingerprintConnection.cs index ef646dab0..02f28bd42 100644 --- a/backend/src/GraphQl/Users/UserGnuPgKeyFingerprintConnection.cs +++ b/backend/src/GraphQl/Users/UserGnuPgKeyFingerprintConnection.cs @@ -9,8 +9,8 @@ QueryContext queryContext ) : Connection< User, GnuPgKeyFingerprint, - GnuPgKeyFingerprintsByUserIdDataLoader, - UserGnuPgKeyFingerprintEdge + UserGnuPgKeyFingerprintEdge, + IGnuPgKeyFingerprintsByUserIdDataLoader > ( user, diff --git a/backend/src/GraphQl/Users/UserGnuPgKeyFingerprintFilterType.cs b/backend/src/GraphQl/Users/UserGnuPgKeyFingerprintFilterType.cs index 8cbfd3832..036ae3d1b 100644 --- a/backend/src/GraphQl/Users/UserGnuPgKeyFingerprintFilterType.cs +++ b/backend/src/GraphQl/Users/UserGnuPgKeyFingerprintFilterType.cs @@ -1,5 +1,4 @@ using HotChocolate.Data.Filters; -using Metabase.Configuration; using Metabase.Data; using Metabase.GraphQl.GnuPgKeyFingerprints; @@ -14,6 +13,6 @@ IFilterInputTypeDescriptor descriptor { base.Configure(descriptor); descriptor.Name(nameof(UserGnuPgKeyFingerprintFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); - descriptor.Field(x => x.User).Ignore(); + descriptor.Field(_ => _.User).Ignore(); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Users/UserGnuPgKeyFingerprintSortType.cs b/backend/src/GraphQl/Users/UserGnuPgKeyFingerprintSortType.cs new file mode 100644 index 000000000..31d4fd346 --- /dev/null +++ b/backend/src/GraphQl/Users/UserGnuPgKeyFingerprintSortType.cs @@ -0,0 +1,17 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data; +using Metabase.GraphQl.GnuPgKeyFingerprints; + +namespace Metabase.GraphQl.Users; + +public sealed class UserGnuPgKeyFingerprintSortType + : GnuPgKeyFingerprintSortType +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.Name(nameof(UserGnuPgKeyFingerprintSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Users/UserMutations.cs b/backend/src/GraphQl/Users/UserMutations.cs index a5bf4017e..5b0de32f2 100644 --- a/backend/src/GraphQl/Users/UserMutations.cs +++ b/backend/src/GraphQl/Users/UserMutations.cs @@ -1167,7 +1167,7 @@ await userManager.VerifyTwoFactorTokenAsync( }; } - if (await userManager.CountRecoveryCodesAsync(user) == 0) + if (await userManager.CountRecoveryCodesAsync(user) is 0) { var recoveryCodes = await userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); @@ -1644,7 +1644,7 @@ CancellationToken cancellationToken var user = await context.Users.AsQueryable() .SingleOrDefaultAsync( - x => x.Id == input.UserId, + _ => _.Id == input.UserId, cancellationToken ); if (user is null) @@ -1691,6 +1691,7 @@ public async Task RemoveUserRoleAsync( ClaimsPrincipal claimsPrincipal, UserAuthorization authorization, UserManager userManager, + RoleManager roleManager, ApplicationDbContext context, CancellationToken cancellationToken ) @@ -1708,7 +1709,7 @@ CancellationToken cancellationToken var user = await context.Users.AsQueryable() .SingleOrDefaultAsync( - x => x.Id == input.UserId, + _ => _.Id == input.UserId, cancellationToken ); if (user is null) @@ -1722,6 +1723,21 @@ CancellationToken cancellationToken ); } + var role = (await roleManager.FindByNameAsync(Role.EnumToName(input.Role)))!; + if (!await context.UserRoles.AsQueryable() + .Where(_ => _.RoleId == role.Id && _.UserId != user.Id) + .AnyAsync(cancellationToken) + ) + { + return new RemoveUserRolePayload( + new RemoveUserRoleError( + RemoveUserRoleErrorCode.LAST_USER, + "Cannot remove last user in role.", + [nameof(input), nameof(input.Role).FirstCharToLower()] + ) + ); + } + var identityResult = await userManager.RemoveFromRoleAsync(user, Role.EnumToName(input.Role)); if (!identityResult.Succeeded) { @@ -1828,4 +1844,4 @@ public sealed record GettingAuthenticatorKeyFailure : LoadSharedKeyAndQrCodeUriP public sealed record GettingEmailFailure : LoadSharedKeyAndQrCodeUriPayload; } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Users/UserQueries.cs b/backend/src/GraphQl/Users/UserQueries.cs index 7aaa5f2aa..d9c71e750 100644 --- a/backend/src/GraphQl/Users/UserQueries.cs +++ b/backend/src/GraphQl/Users/UserQueries.cs @@ -3,9 +3,10 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using GreenDonut.Data; using HotChocolate.Authorization; using HotChocolate.Data; -using HotChocolate.Data.Sorting; +using HotChocolate.Resolvers; using HotChocolate.Types; using Metabase.Authorization; using Metabase.Data; @@ -34,27 +35,27 @@ CancellationToken cancellationToken } [UsePaging] - /* [UseProjection] // fails without an explicit error message in the logs */ [UseFiltering] [UseSorting] - public IQueryable GetUsers( - ApplicationDbContext context, - ISortingContext sorting + public ValueTask> GetUsersAsync( + IResolverContext resolverContext, + ApplicationDbContext databaseContext, + CancellationToken cancellationToken ) { - sorting.StabilizeOrder(); - return context.Users.AsNoTracking(); + return databaseContext.Users + .AsNoTracking() + .With(resolverContext.GetQueryContext(), Sorting.DefaultEntityOrder) + .ToPageAsync(resolverContext.GetPagingArguments(), cancellationToken) + .ToConnectionAsync(); } public Task GetUserAsync( Guid id, - UserByIdDataLoader userById, + IUserByIdDataLoader byId, CancellationToken cancellationToken ) { - return userById.LoadAsync( - id, - cancellationToken - ); + return byId.LoadAsync(id, cancellationToken); } } \ No newline at end of file diff --git a/backend/src/GraphQl/Users/UserRepresentedInstitutionConnection.cs b/backend/src/GraphQl/Users/UserRepresentedInstitutionConnection.cs index 523704b8f..b9e18282a 100644 --- a/backend/src/GraphQl/Users/UserRepresentedInstitutionConnection.cs +++ b/backend/src/GraphQl/Users/UserRepresentedInstitutionConnection.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using GreenDonut.Data; +using HotChocolate.CostAnalysis.Types; using Metabase.Authorization; using Metabase.Data; @@ -14,8 +15,8 @@ QueryContext queryContext : Connection< User, InstitutionRepresentative, - UserRepresentedInstitutionsByUserIdDataLoader, - UserRepresentedInstitutionEdge + UserRepresentedInstitutionEdge, + UserRepresentedInstitutionsByUserIdDataLoader >( subject, x => new UserRepresentedInstitutionEdge(x), @@ -31,8 +32,8 @@ QueryContext queryContext : AuthorizedConnection< User, InstitutionRepresentative, - PendingUserRepresentedInstitutionsByUserIdDataLoader, UserRepresentedInstitutionEdge, + PendingUserRepresentedInstitutionsByUserIdDataLoader, InstitutionRepresentativeAuthorization >( subject, @@ -43,6 +44,7 @@ QueryContext queryContext ) { [UseUserManager] + [Cost(1)] public Task IsAuthorizedToConfirmEdgesAsync( ClaimsPrincipal claimsPrincipal, InstitutionRepresentativeAuthorization authorization, diff --git a/backend/src/GraphQl/Users/UserRepresentedInstitutionEdge.cs b/backend/src/GraphQl/Users/UserRepresentedInstitutionEdge.cs index 0e128fb1e..9dd7c36aa 100644 --- a/backend/src/GraphQl/Users/UserRepresentedInstitutionEdge.cs +++ b/backend/src/GraphQl/Users/UserRepresentedInstitutionEdge.cs @@ -1,3 +1,8 @@ +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate.CostAnalysis.Types; +using Metabase.Authorization; using Metabase.Data; using Metabase.Enumerations; using Metabase.GraphQl.Institutions; @@ -7,7 +12,22 @@ namespace Metabase.GraphQl.Users; public sealed class UserRepresentedInstitutionEdge( InstitutionRepresentative association ) - : Edge(association.InstitutionId) + : Edge(association.InstitutionId) { public InstitutionRepresentativeRole Role { get; } = association.Role; + + [UseUserManager] + [Cost(1)] + public Task IsAuthorizedToRemoveEdgeAsync( + ClaimsPrincipal claimsPrincipal, + InstitutionRepresentativeAuthorization authorization, + CancellationToken cancellationToken + ) + { + return authorization.IsAuthorizedToManage( + claimsPrincipal, + association.InstitutionId, + cancellationToken + ); + } } \ No newline at end of file diff --git a/backend/src/GraphQl/Users/UserRepresentedInstitutionFilterType.cs b/backend/src/GraphQl/Users/UserRepresentedInstitutionFilterType.cs index b41ca5e05..5807c6c65 100644 --- a/backend/src/GraphQl/Users/UserRepresentedInstitutionFilterType.cs +++ b/backend/src/GraphQl/Users/UserRepresentedInstitutionFilterType.cs @@ -1,5 +1,4 @@ using HotChocolate.Data.Filters; -using Metabase.Configuration; using Metabase.Data; using Metabase.GraphQl.InstitutionRepresentatives; @@ -14,6 +13,6 @@ IFilterInputTypeDescriptor descriptor { base.Configure(descriptor); descriptor.Name(nameof(UserRepresentedInstitutionFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); - descriptor.Field(x => x.User).Ignore(); + descriptor.Field(_ => _.User).Ignore(); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Users/UserRepresentedInstitutionSortType.cs b/backend/src/GraphQl/Users/UserRepresentedInstitutionSortType.cs new file mode 100644 index 000000000..d6aa69140 --- /dev/null +++ b/backend/src/GraphQl/Users/UserRepresentedInstitutionSortType.cs @@ -0,0 +1,17 @@ +using HotChocolate.Data.Sorting; +using Metabase.Data; +using Metabase.GraphQl.InstitutionRepresentatives; + +namespace Metabase.GraphQl.Users; + +public sealed class UserRepresentedInstitutionSortType + : InstitutionRepresentativeSortType +{ + protected override void Configure( + ISortInputTypeDescriptor descriptor + ) + { + base.Configure(descriptor); + descriptor.Name(nameof(UserRepresentedInstitutionSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Users/UserRepresentedInstitutionsByUserIdDataLoader.cs b/backend/src/GraphQl/Users/UserRepresentedInstitutionsByUserIdDataLoader.cs deleted file mode 100644 index 7e8e00888..000000000 --- a/backend/src/GraphQl/Users/UserRepresentedInstitutionsByUserIdDataLoader.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Linq; -using GreenDonut; -using GreenDonut.Data; -using Metabase.Data; -using Metabase.GraphQl.Entities; -using Microsoft.EntityFrameworkCore; - -namespace Metabase.GraphQl.Users; - -public sealed class UserRepresentedInstitutionsByUserIdDataLoader( - IBatchScheduler batchScheduler, - DataLoaderOptions options, - IDbContextFactory dbContextFactory - ) - : AssociationsByAssociateIdDataLoader( - batchScheduler, - options, - dbContextFactory, - (dbContext, ids, queryContext) => - dbContext.InstitutionRepresentatives.AsNoTracking().Where(x => - !x.Pending && ids.Contains(x.UserId) - ).With(queryContext), - x => x.UserId - ) -{ -} \ No newline at end of file diff --git a/backend/src/GraphQl/Users/UserSortType.cs b/backend/src/GraphQl/Users/UserSortType.cs index e05ccb294..359d9425c 100644 --- a/backend/src/GraphQl/Users/UserSortType.cs +++ b/backend/src/GraphQl/Users/UserSortType.cs @@ -5,17 +5,18 @@ namespace Metabase.GraphQl.Users; public sealed class UserSortType - : EntitySortType + : AuditableEntitySortType { protected override void Configure( ISortInputTypeDescriptor descriptor ) { base.Configure(descriptor); - // TODO The commented fiels below should be sortable by OpenId Connect Clients and application users with the proper scopes and rights. If they are filterable in general, it is a way to figure out that information even if it is not returned by GraphQL. - // descriptor.Field(x => x.Name); - // descriptor.Field(x => x.Email); - // descriptor.Field(x => x.PostalAddress); - // descriptor.Field(x => x.WebsiteLocator); + descriptor.Name(nameof(UserSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); + // TODO The commented fiels below should be sortable by OpenId Connect Clients and application users with the proper scopes and rights. If they are sortable in general, it is a way to figure out that information even if it is not returned by GraphQL. + descriptor.Field(_ => _.Name); + // descriptor.Field(_ => _.Email); + // descriptor.Field(_ => _.PostalAddress); + // descriptor.Field(_ => _.WebsiteLocator); } -} \ No newline at end of file +} diff --git a/backend/src/GraphQl/Users/UserType.cs b/backend/src/GraphQl/Users/UserType.cs index 0389e481c..b30fce68b 100644 --- a/backend/src/GraphQl/Users/UserType.cs +++ b/backend/src/GraphQl/Users/UserType.cs @@ -16,6 +16,7 @@ using Metabase.Extensions; using Metabase.GraphQl.Entities; using Metabase.GraphQl.Extensions; +using Metabase.GraphQl.OpenIdConnect.Applications; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using OpenIddict.Abstractions; @@ -24,7 +25,7 @@ namespace Metabase.GraphQl.Users; public sealed class UserType - : EntityType + : EntityType { private static async Task Authorize( IResolverContext context, @@ -136,17 +137,20 @@ IObjectTypeDescriptor descriptor descriptor .Field(t => t.Name) // .Type>() + .Cost(0) .Resolve(async context => // Instead of returning `null`, we return a string because otherwise the // corresponding GraphQL field would need to be nullable and because the type `User` // implements `IStakeholder`, the stakeholder name would also need to be nullable. - await Authorize(context, user => user.Name, Scopes.Profile) ?? - "" + await Authorize(context, user => user.Name, Scopes.Profile) + ?? context.Parent().Name.Split(null as char[], StringSplitOptions.RemoveEmptyEntries).GetFirstOrDefault() + ?? "" ) .UseUserManager(); descriptor .Field("contact") - .Type>>() + .Type>() + .Cost(0) .Resolve(async context => new ContactInformation( await Authorize(context, user => user.PhoneNumber, Scopes.Phone), @@ -160,6 +164,7 @@ await Authorize(context, user => user.WebsiteLocator, Scopes.Profile) .UseUserManager(); descriptor .Field("twoFactorAuthentication") + .Cost(0) .ResolveWith(t => UserResolvers.GetTwoFactorAuthenticationAsync(default!, default!, default!, default!, default!, default!)) .UseUserManager() @@ -167,6 +172,7 @@ await Authorize(context, user => user.WebsiteLocator, Scopes.Profile) descriptor .Field("hasPassword") .Type() + .Cost(0) .Resolve(context => AuthorizeAsync( context, @@ -177,6 +183,7 @@ await Authorize(context, user => user.WebsiteLocator, Scopes.Profile) .UseUserManager(); descriptor .Field("roles") + .Cost(0) .Resolve(context => AuthorizeAsync( context, @@ -185,35 +192,73 @@ await Authorize(context, user => user.WebsiteLocator, Scopes.Profile) ) ) .UseUserManager(); + descriptor + .Field("authorizedOpenIdConnectConsentTypes") + .Cost(1) + .ResolveWith(x => + UserResolvers.GetAuthorizedOpenIdConnectConsentTypesAsync(default!, default!, default!)) + .UseUserManager(); + descriptor + .Field("authorizedOpenIdConnectEndpoints") + .Cost(1) + .ResolveWith(x => + UserResolvers.GetAuthorizedOpenIdConnectEndpointsAsync(default!, default!, default!)) + .UseUserManager(); + descriptor + .Field("authorizedOpenIdConnectGrantTypes") + .Cost(1) + .ResolveWith(x => + UserResolvers.GetAuthorizedOpenIdConnectGrantTypesAsync(default!, default!, default!)) + .UseUserManager(); + descriptor + .Field("authorizedOpenIdConnectResponseTypes") + .Cost(1) + .ResolveWith(x => + UserResolvers.GetAuthorizedOpenIdConnectResponseTypesAsync(default!, default!)) + .UseUserManager(); + descriptor + .Field("authorizedOpenIdConnectScopes") + .Cost(1) + .ResolveWith(x => + UserResolvers.GetAuthorizedOpenIdConnectScopesAsync(default!, default!, default!)) + .UseUserManager(); descriptor .Field("rolesCurrentUserCanAdd") + .Cost(1) .ResolveWith(x => UserResolvers.GetRolesCurrentUserCanAddOrRemoveAsync(default!, default!, default!)) .UseUserManager(); descriptor .Field("rolesCurrentUserCanRemove") + .Cost(1) .ResolveWith(x => UserResolvers.GetRolesCurrentUserCanAddOrRemoveAsync(default!, default!, default!)) .UseUserManager(); descriptor .Field("isAuthorizedToDeleteUser") + .Cost(1) .ResolveWith(x => UserResolvers.IsAuthorizedToDeleteUserAsync(default!, default!, default!)) .UseUserManager(); descriptor .Field("isAuthorizedToManageOpenIdConnect") + .Cost(1) .ResolveWith(x => UserResolvers.IsAuthorizedToManageOpenIdConnect(default!, default!, default!)) .UseUserManager(); descriptor .Field("isAuthorizedToAddApprovals") + .Cost(1) .ResolveWith(x => UserResolvers.IsAuthorizedToAddApprovals(default!, default!, default!)) .UseUserManager(); descriptor .Field(t => t.DevelopedMethods) .Type>>() + .AddPagingArguments() .UseFiltering() + .UseSorting() .Resolve(context => new UserDevelopedMethodConnection( context.Parent(), + context.GetPagingArguments(), context.GetQueryContext() ) ); @@ -221,10 +266,13 @@ await Authorize(context, user => user.WebsiteLocator, Scopes.Profile) .Field($"{GraphQlConstants.PendingPrefix}{nameof(User.DevelopedMethods)}") .Type>() .Authorize(AuthorizationPolicies.WriteScopePolicy) + .AddPagingArguments() .UseFiltering() + .UseSorting() .Resolve(context => new PendingUserDevelopedMethodConnection( context.Parent(), + context.GetPagingArguments(), context.GetQueryContext() ) ); @@ -232,6 +280,7 @@ await Authorize(context, user => user.WebsiteLocator, Scopes.Profile) .Field(t => t.RepresentedInstitutions) .Type>>() .UseFiltering() + .UseSorting() .Resolve(context => new UserRepresentedInstitutionConnection( context.Parent(), @@ -243,6 +292,7 @@ await Authorize(context, user => user.WebsiteLocator, Scopes.Profile) .Type>() .Authorize(AuthorizationPolicies.WriteScopePolicy) .UseFiltering() + .UseSorting() .Resolve(context => new PendingUserRepresentedInstitutionConnection( context.Parent(), @@ -253,6 +303,7 @@ await Authorize(context, user => user.WebsiteLocator, Scopes.Profile) .Field(t => t.GnuPgKeyFingerprints) .Type>>() .UseFiltering() + .UseSorting() .Resolve(context => new UserGnuPgKeyFingerprintConnection( context.Parent(), @@ -262,6 +313,7 @@ await Authorize(context, user => user.WebsiteLocator, Scopes.Profile) descriptor .Field("has" + nameof(GnuPgKeyFingerprint)) .UseFiltering() + .UseSorting() .ResolveWith(x => UserResolvers.HasGnuPgKeyFingerprintsAsync(default!, default!, default!, default!)); } @@ -350,5 +402,49 @@ [EnumeratorCancellation] CancellationToken cancellationToken } } } + + public static Task> GetAuthorizedOpenIdConnectConsentTypesAsync( + ClaimsPrincipal claimsPrincipal, + Authorization.OpenIdConnectAuthorization authorization, + CancellationToken cancellationToken + ) + { + return authorization.AuthorizedConsentTypes(claimsPrincipal, cancellationToken); + } + + public static Task> GetAuthorizedOpenIdConnectEndpointsAsync( + ClaimsPrincipal claimsPrincipal, + Authorization.OpenIdConnectAuthorization authorization, + CancellationToken cancellationToken + ) + { + return Authorization.OpenIdConnectAuthorization.AuthorizedEndpoints(claimsPrincipal, cancellationToken); + } + + public static Task> GetAuthorizedOpenIdConnectGrantTypesAsync( + ClaimsPrincipal claimsPrincipal, + Authorization.OpenIdConnectAuthorization authorization, + CancellationToken cancellationToken + ) + { + return authorization.AuthorizedGrantTypes(claimsPrincipal, cancellationToken); + } + + public static Task> GetAuthorizedOpenIdConnectResponseTypesAsync( + ClaimsPrincipal claimsPrincipal, + CancellationToken cancellationToken + ) + { + return Authorization.OpenIdConnectAuthorization.AuthorizedResponseTypes(claimsPrincipal, cancellationToken); + } + + public static Task> GetAuthorizedOpenIdConnectScopesAsync( + ClaimsPrincipal claimsPrincipal, + Authorization.OpenIdConnectAuthorization authorization, + CancellationToken cancellationToken + ) + { + return authorization.AuthorizedScopes(claimsPrincipal, cancellationToken); + } } } \ No newline at end of file diff --git a/backend/src/Jobs/JwtSigningAndEncryptionCertificateRotationJob.cs b/backend/src/Jobs/JwtSigningAndEncryptionCertificateRotationJob.cs index ceceedf69..d2aa3c195 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 Metabase.Authentication; +using Metabase.Extensions; using Microsoft.Extensions.Logging; +using NodaTime; using Quartz; namespace Metabase.Jobs; @@ -48,6 +49,7 @@ Exception exception } public sealed class JwtSigningAndEncryptionCertificateRotationJob( + IClock clock, ILogger logger ) : IJob @@ -75,7 +77,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); @@ -97,7 +99,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) @@ -114,7 +116,7 @@ public static X509Certificate2 CreateSigningCertificate(string distinguishedName { try { - return CreateSigningCertificate(distinguishedName); + return CreateSigningCertificate(distinguishedName, clock); } catch (Exception exception) { @@ -123,7 +125,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); @@ -139,7 +141,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) @@ -156,7 +158,7 @@ public static X509Certificate2 CreateEncryptionCertificate(string distinguishedN { try { - return CreateEncryptionCertificate(distinguishedName); + return CreateEncryptionCertificate(distinguishedName, clock); } catch (Exception exception) { @@ -202,10 +204,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/Json/JsonSerializerSettings.cs b/backend/src/Json/JsonSerializerSettings.cs index 6972e4d27..acbd884c2 100644 --- a/backend/src/Json/JsonSerializerSettings.cs +++ b/backend/src/Json/JsonSerializerSettings.cs @@ -63,4 +63,11 @@ public static class JsonSerializerSettings PropertyNamingPolicy = JsonNamingPolicy.CamelCase, UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, }; + + public static readonly JsonSerializerOptions Compact = + new(s_common) + { + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; } \ No newline at end of file diff --git a/backend/src/Metabase.csproj b/backend/src/Metabase.csproj index 4fba0111b..4d4d53c0c 100644 --- a/backend/src/Metabase.csproj +++ b/backend/src/Metabase.csproj @@ -13,60 +13,62 @@ - - + - - - - - - - - - + + + + + + + + + + + - - - - - - - + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - - + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - + diff --git a/backend/src/Migrations/20210414155531_InitialCreate.cs b/backend/src/Migrations/20210414155531_InitialCreate.cs index dc7fe6f7d..47c95dd52 100644 --- a/backend/src/Migrations/20210414155531_InitialCreate.cs +++ b/backend/src/Migrations/20210414155531_InitialCreate.cs @@ -39,7 +39,7 @@ protected override void Up(MigrationBuilder migrationBuilder) }, constraints: table => { - table.PrimaryKey("PK_component", x => x.Id); + table.PrimaryKey("PK_component", _ => _.Id); }); migrationBuilder.CreateTable( @@ -58,7 +58,7 @@ protected override void Up(MigrationBuilder migrationBuilder) }, constraints: table => { - table.PrimaryKey("PK_institution", x => x.Id); + table.PrimaryKey("PK_institution", _ => _.Id); }); migrationBuilder.CreateTable( @@ -94,7 +94,7 @@ protected override void Up(MigrationBuilder migrationBuilder) }, constraints: table => { - table.PrimaryKey("PK_method", x => x.Id); + table.PrimaryKey("PK_method", _ => _.Id); }); migrationBuilder.CreateTable( @@ -118,7 +118,7 @@ protected override void Up(MigrationBuilder migrationBuilder) }, constraints: table => { - table.PrimaryKey("PK_OpenIddictApplications", x => x.Id); + table.PrimaryKey("PK_OpenIddictApplications", _ => _.Id); }); migrationBuilder.CreateTable( @@ -138,7 +138,7 @@ protected override void Up(MigrationBuilder migrationBuilder) }, constraints: table => { - table.PrimaryKey("PK_OpenIddictScopes", x => x.Id); + table.PrimaryKey("PK_OpenIddictScopes", _ => _.Id); }); migrationBuilder.CreateTable( @@ -153,7 +153,7 @@ protected override void Up(MigrationBuilder migrationBuilder) }, constraints: table => { - table.PrimaryKey("PK_role", x => x.Id); + table.PrimaryKey("PK_role", _ => _.Id); }); migrationBuilder.CreateTable( @@ -183,7 +183,7 @@ protected override void Up(MigrationBuilder migrationBuilder) }, constraints: table => { - table.PrimaryKey("PK_user", x => x.Id); + table.PrimaryKey("PK_user", _ => _.Id); }); migrationBuilder.CreateTable( @@ -201,14 +201,14 @@ protected override void Up(MigrationBuilder migrationBuilder) table.PrimaryKey("PK_component_concretization_and_generalization", x => new { x.GeneralComponentId, x.ConcreteComponentId }); table.ForeignKey( name: "FK_component_concretization_and_generalization_component_Concr~", - column: x => x.ConcreteComponentId, + column: _ => _.ConcreteComponentId, principalSchema: "metabase", principalTable: "component", principalColumn: "Id", onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "FK_component_concretization_and_generalization_component_Gener~", - column: x => x.GeneralComponentId, + column: _ => _.GeneralComponentId, principalSchema: "metabase", principalTable: "component", principalColumn: "Id", @@ -228,14 +228,14 @@ protected override void Up(MigrationBuilder migrationBuilder) table.PrimaryKey("PK_component_manufacturer", x => new { x.ComponentId, x.InstitutionId }); table.ForeignKey( name: "FK_component_manufacturer_component_ComponentId", - column: x => x.ComponentId, + column: _ => _.ComponentId, principalSchema: "metabase", principalTable: "component", principalColumn: "Id", onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "FK_component_manufacturer_institution_InstitutionId", - column: x => x.InstitutionId, + column: _ => _.InstitutionId, principalSchema: "metabase", principalTable: "institution", principalColumn: "Id", @@ -256,10 +256,10 @@ protected override void Up(MigrationBuilder migrationBuilder) }, constraints: table => { - table.PrimaryKey("PK_database", x => x.Id); + table.PrimaryKey("PK_database", _ => _.Id); table.ForeignKey( name: "FK_database_institution_OperatorId", - column: x => x.OperatorId, + column: _ => _.OperatorId, principalSchema: "metabase", principalTable: "institution", principalColumn: "Id", @@ -299,10 +299,10 @@ protected override void Up(MigrationBuilder migrationBuilder) }, constraints: table => { - table.PrimaryKey("PK_DataFormats", x => x.Id); + table.PrimaryKey("PK_DataFormats", _ => _.Id); table.ForeignKey( name: "FK_DataFormats_institution_ManagerId", - column: x => x.ManagerId, + column: _ => _.ManagerId, principalSchema: "metabase", principalTable: "institution", principalColumn: "Id", @@ -324,14 +324,14 @@ protected override void Up(MigrationBuilder migrationBuilder) table.PrimaryKey("PK_institution_method_developer", x => new { x.InstitutionId, x.MethodId }); table.ForeignKey( name: "FK_institution_method_developer_institution_InstitutionId", - column: x => x.InstitutionId, + column: _ => _.InstitutionId, principalSchema: "metabase", principalTable: "institution", principalColumn: "Id", onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "FK_institution_method_developer_method_MethodId", - column: x => x.MethodId, + column: _ => _.MethodId, principalSchema: "metabase", principalTable: "method", principalColumn: "Id", @@ -355,10 +355,10 @@ protected override void Up(MigrationBuilder migrationBuilder) }, constraints: table => { - table.PrimaryKey("PK_OpenIddictAuthorizations", x => x.Id); + table.PrimaryKey("PK_OpenIddictAuthorizations", _ => _.Id); table.ForeignKey( name: "FK_OpenIddictAuthorizations_OpenIddictApplications_Application~", - column: x => x.ApplicationId, + column: _ => _.ApplicationId, principalSchema: "metabase", principalTable: "OpenIddictApplications", principalColumn: "Id", @@ -378,10 +378,10 @@ protected override void Up(MigrationBuilder migrationBuilder) }, constraints: table => { - table.PrimaryKey("PK_role_claim", x => x.Id); + table.PrimaryKey("PK_role_claim", _ => _.Id); table.ForeignKey( name: "FK_role_claim_role_RoleId", - column: x => x.RoleId, + column: _ => _.RoleId, principalSchema: "metabase", principalTable: "role", principalColumn: "Id", @@ -402,14 +402,14 @@ protected override void Up(MigrationBuilder migrationBuilder) table.PrimaryKey("PK_institution_representative", x => new { x.InstitutionId, x.UserId }); table.ForeignKey( name: "FK_institution_representative_institution_InstitutionId", - column: x => x.InstitutionId, + column: _ => _.InstitutionId, principalSchema: "metabase", principalTable: "institution", principalColumn: "Id", onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "FK_institution_representative_user_UserId", - column: x => x.UserId, + column: _ => _.UserId, principalSchema: "metabase", principalTable: "user", principalColumn: "Id", @@ -429,10 +429,10 @@ protected override void Up(MigrationBuilder migrationBuilder) }, constraints: table => { - table.PrimaryKey("PK_user_claim", x => x.Id); + table.PrimaryKey("PK_user_claim", _ => _.Id); table.ForeignKey( name: "FK_user_claim_user_UserId", - column: x => x.UserId, + column: _ => _.UserId, principalSchema: "metabase", principalTable: "user", principalColumn: "Id", @@ -454,7 +454,7 @@ protected override void Up(MigrationBuilder migrationBuilder) table.PrimaryKey("PK_user_login", x => new { x.LoginProvider, x.ProviderKey }); table.ForeignKey( name: "FK_user_login_user_UserId", - column: x => x.UserId, + column: _ => _.UserId, principalSchema: "metabase", principalTable: "user", principalColumn: "Id", @@ -476,14 +476,14 @@ protected override void Up(MigrationBuilder migrationBuilder) table.PrimaryKey("PK_user_method_developer", x => new { x.UserId, x.MethodId }); table.ForeignKey( name: "FK_user_method_developer_method_MethodId", - column: x => x.MethodId, + column: _ => _.MethodId, principalSchema: "metabase", principalTable: "method", principalColumn: "Id", onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "FK_user_method_developer_user_UserId", - column: x => x.UserId, + column: _ => _.UserId, principalSchema: "metabase", principalTable: "user", principalColumn: "Id", @@ -503,14 +503,14 @@ protected override void Up(MigrationBuilder migrationBuilder) table.PrimaryKey("PK_user_role", x => new { x.UserId, x.RoleId }); table.ForeignKey( name: "FK_user_role_role_RoleId", - column: x => x.RoleId, + column: _ => _.RoleId, principalSchema: "metabase", principalTable: "role", principalColumn: "Id", onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "FK_user_role_user_UserId", - column: x => x.UserId, + column: _ => _.UserId, principalSchema: "metabase", principalTable: "user", principalColumn: "Id", @@ -532,7 +532,7 @@ protected override void Up(MigrationBuilder migrationBuilder) table.PrimaryKey("PK_user_token", x => new { x.UserId, x.LoginProvider, x.Name }); table.ForeignKey( name: "FK_user_token_user_UserId", - column: x => x.UserId, + column: _ => _.UserId, principalSchema: "metabase", principalTable: "user", principalColumn: "Id", @@ -560,17 +560,17 @@ protected override void Up(MigrationBuilder migrationBuilder) }, constraints: table => { - table.PrimaryKey("PK_OpenIddictTokens", x => x.Id); + table.PrimaryKey("PK_OpenIddictTokens", _ => _.Id); table.ForeignKey( name: "FK_OpenIddictTokens_OpenIddictApplications_ApplicationId", - column: x => x.ApplicationId, + column: _ => _.ApplicationId, principalSchema: "metabase", principalTable: "OpenIddictApplications", principalColumn: "Id", onDelete: ReferentialAction.Restrict); table.ForeignKey( name: "FK_OpenIddictTokens_OpenIddictAuthorizations_AuthorizationId", - column: x => x.AuthorizationId, + column: _ => _.AuthorizationId, principalSchema: "metabase", principalTable: "OpenIddictAuthorizations", principalColumn: "Id", @@ -797,4 +797,4 @@ protected override void Down(MigrationBuilder migrationBuilder) .OldAnnotation("Npgsql:Enum:public.standardizer", "aerc,agi,ashrae,breeam,bs,bsi,cen,cie,dgnb,din,dvwg,iec,ies,ift,iso,jis,leed,nfrc,riba,ul,unece,vdi,vff,well") .OldAnnotation("Npgsql:PostgresExtension:pgcrypto", ",,"); } -} \ No newline at end of file +} diff --git a/backend/src/Migrations/20211110161448_AddReflexiveAssociationsToComponent.cs b/backend/src/Migrations/20211110161448_AddReflexiveAssociationsToComponent.cs index 811b881a9..00bfa4c71 100644 --- a/backend/src/Migrations/20211110161448_AddReflexiveAssociationsToComponent.cs +++ b/backend/src/Migrations/20211110161448_AddReflexiveAssociationsToComponent.cs @@ -30,14 +30,14 @@ protected override void Up(MigrationBuilder migrationBuilder) table.PrimaryKey("PK_component_assembly", x => new { x.AssembledComponentId, x.PartComponentId }); table.ForeignKey( name: "FK_component_assembly_component_AssembledComponentId", - column: x => x.AssembledComponentId, + column: _ => _.AssembledComponentId, principalSchema: "metabase", principalTable: "component", principalColumn: "Id", onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "FK_component_assembly_component_PartComponentId", - column: x => x.PartComponentId, + column: _ => _.PartComponentId, principalSchema: "metabase", principalTable: "component", principalColumn: "Id", @@ -57,14 +57,14 @@ protected override void Up(MigrationBuilder migrationBuilder) table.PrimaryKey("PK_component_variant", x => new { x.OfComponentId, x.ToComponentId }); table.ForeignKey( name: "FK_component_variant_component_OfComponentId", - column: x => x.OfComponentId, + column: _ => _.OfComponentId, principalSchema: "metabase", principalTable: "component", principalColumn: "Id", onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "FK_component_variant_component_ToComponentId", - column: x => x.ToComponentId, + column: _ => _.ToComponentId, principalSchema: "metabase", principalTable: "component", principalColumn: "Id", @@ -110,4 +110,4 @@ protected override void Down(MigrationBuilder migrationBuilder) nullable: false, defaultValue: 0L); } -} \ No newline at end of file +} diff --git a/backend/src/Migrations/20230210135411_AddDataProtectionKeys.cs b/backend/src/Migrations/20230210135411_AddDataProtectionKeys.cs index 7776aba20..7170305bd 100644 --- a/backend/src/Migrations/20230210135411_AddDataProtectionKeys.cs +++ b/backend/src/Migrations/20230210135411_AddDataProtectionKeys.cs @@ -23,7 +23,7 @@ protected override void Up(MigrationBuilder migrationBuilder) }, constraints: table => { - table.PrimaryKey("PK_DataProtectionKeys", x => x.Id); + table.PrimaryKey("PK_DataProtectionKeys", _ => _.Id); }); } @@ -34,4 +34,4 @@ protected override void Down(MigrationBuilder migrationBuilder) name: "DataProtectionKeys", schema: "metabase"); } -} \ No newline at end of file +} diff --git a/backend/src/Migrations/20241213072103_AddRelationInstitutionToApplication.cs b/backend/src/Migrations/20241213072103_AddRelationInstitutionToApplication.cs index 558a395f5..0a77a6556 100644 --- a/backend/src/Migrations/20241213072103_AddRelationInstitutionToApplication.cs +++ b/backend/src/Migrations/20241213072103_AddRelationInstitutionToApplication.cs @@ -90,14 +90,14 @@ protected override void Up(MigrationBuilder migrationBuilder) table.PrimaryKey("PK_institution_application", x => new { x.InstitutionId, x.ApplicationId }); table.ForeignKey( name: "FK_institution_application_OpenIddictApplications_ApplicationId", - column: x => x.ApplicationId, + column: _ => _.ApplicationId, principalSchema: "metabase", principalTable: "OpenIddictApplications", principalColumn: "Id", onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "FK_institution_application_institution_InstitutionId", - column: x => x.InstitutionId, + column: _ => _.InstitutionId, principalSchema: "metabase", principalTable: "institution", principalColumn: "Id", @@ -184,4 +184,4 @@ protected override void Down(MigrationBuilder migrationBuilder) oldClrType: typeof(Guid), oldType: "uuid"); } -} \ No newline at end of file +} diff --git a/backend/src/Migrations/20250815155558_MakeOpenIddictApplicationsBelongToExactlyOneInstitution.cs b/backend/src/Migrations/20250815155558_MakeOpenIddictApplicationsBelongToExactlyOneInstitution.cs index 80d6253e3..889719e21 100644 --- a/backend/src/Migrations/20250815155558_MakeOpenIddictApplicationsBelongToExactlyOneInstitution.cs +++ b/backend/src/Migrations/20250815155558_MakeOpenIddictApplicationsBelongToExactlyOneInstitution.cs @@ -127,14 +127,14 @@ protected override void Down(MigrationBuilder migrationBuilder) table.PrimaryKey("PK_institution_open_id_connect_application", x => new { x.InstitutionId, x.ApplicationId }); table.ForeignKey( name: "FK_institution_open_id_connect_application_OpenIddictApplicati~", - column: x => x.ApplicationId, + column: _ => _.ApplicationId, principalSchema: "metabase", principalTable: "OpenIddictApplications", principalColumn: "Id", onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "FK_institution_open_id_connect_application_institution_Institu~", - column: x => x.InstitutionId, + column: _ => _.InstitutionId, principalSchema: "metabase", principalTable: "institution", principalColumn: "Id", @@ -148,4 +148,4 @@ protected override void Down(MigrationBuilder migrationBuilder) column: "ApplicationId"); } } -} \ No newline at end of file +} diff --git a/backend/src/Migrations/20250826140321_MakeGnuPgFingerprintItsOwnEntity.cs b/backend/src/Migrations/20250826140321_MakeGnuPgFingerprintItsOwnEntity.cs index b96072547..c634e4885 100644 --- a/backend/src/Migrations/20250826140321_MakeGnuPgFingerprintItsOwnEntity.cs +++ b/backend/src/Migrations/20250826140321_MakeGnuPgFingerprintItsOwnEntity.cs @@ -39,17 +39,17 @@ protected override void Up(MigrationBuilder migrationBuilder) }, constraints: table => { - table.PrimaryKey("PK_gnu_pg_fingerprint", x => x.Id); + table.PrimaryKey("PK_gnu_pg_fingerprint", _ => _.Id); table.ForeignKey( name: "FK_gnu_pg_fingerprint_institution_InstitutionId", - column: x => x.InstitutionId, + column: _ => _.InstitutionId, principalSchema: "metabase", principalTable: "institution", principalColumn: "Id", onDelete: ReferentialAction.Cascade); table.ForeignKey( name: "FK_gnu_pg_fingerprint_user_UserId", - column: x => x.UserId, + column: _ => _.UserId, principalSchema: "metabase", principalTable: "user", principalColumn: "Id", @@ -103,4 +103,4 @@ protected override void Down(MigrationBuilder migrationBuilder) defaultValue: Array.Empty()); } } -} \ No newline at end of file +} diff --git a/backend/src/Migrations/20251128175152_AddParametersAndSourcesToMethods.cs b/backend/src/Migrations/20251128175152_AddParametersAndSourcesToMethods.cs index b855f12de..012881abb 100644 --- a/backend/src/Migrations/20251128175152_AddParametersAndSourcesToMethods.cs +++ b/backend/src/Migrations/20251128175152_AddParametersAndSourcesToMethods.cs @@ -247,7 +247,7 @@ protected override void Up(MigrationBuilder migrationBuilder) table.PrimaryKey("PK_MethodParameter", x => new { x.MethodId, x.Id }); table.ForeignKey( name: "FK_MethodParameter_method_MethodId", - column: x => x.MethodId, + column: _ => _.MethodId, principalSchema: "metabase", principalTable: "method", principalColumn: "Id", @@ -270,7 +270,7 @@ protected override void Up(MigrationBuilder migrationBuilder) table.PrimaryKey("PK_MethodSource", x => new { x.MethodId, x.Id }); table.ForeignKey( name: "FK_MethodSource_method_MethodId", - column: x => x.MethodId, + column: _ => _.MethodId, principalSchema: "metabase", principalTable: "method", principalColumn: "Id", @@ -504,4 +504,4 @@ protected override void Down(MigrationBuilder migrationBuilder) newName: "Publication_Abstract"); } } -} \ No newline at end of file +} diff --git a/backend/src/Migrations/20260318153447_CorrectExistsFlagsOfReferences.cs b/backend/src/Migrations/20260318153447_CorrectExistsFlagsOfReferences.cs index 477ea7b73..6283595fe 100644 --- a/backend/src/Migrations/20260318153447_CorrectExistsFlagsOfReferences.cs +++ b/backend/src/Migrations/20260318153447_CorrectExistsFlagsOfReferences.cs @@ -1,4 +1,4 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/backend/src/Migrations/20260328153447_CorrectExistsFlagsOfReferencesSecondAttempt.Designer.cs b/backend/src/Migrations/20260328153447_CorrectExistsFlagsOfReferencesSecondAttempt.Designer.cs new file mode 100644 index 000000000..37714973f --- /dev/null +++ b/backend/src/Migrations/20260328153447_CorrectExistsFlagsOfReferencesSecondAttempt.Designer.cs @@ -0,0 +1,2023 @@ +// +using System; +using System.Text.Json; +using Metabase.Data; +using Metabase.Enumerations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; + +#nullable disable + +namespace Metabase.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260328153447_CorrectExistsFlagsOfReferencesSecondAttempt")] + partial class CorrectExistsFlagsOfReferencesSecondAttempt + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("metabase") + .HasAnnotation("ProductVersion", "10.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "component_category", new[] { "layer", "material", "unit" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "database_verification_state", new[] { "pending", "verified" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "institution_operating_state", new[] { "not_operating", "operating" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "institution_representative_role", new[] { "assistant", "owner" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "institution_state", new[] { "pending", "verified" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "method_category", new[] { "calculation", "measurement" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "prime_surface", new[] { "inside", "outside" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "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("Metabase.Data.Component", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Abbreviation") + .HasColumnType("text"); + + b.Property?>("Availability") + .HasColumnType("tstzrange"); + + b.PrimitiveCollection("Categories") + .IsRequired() + .HasColumnType("metabase.component_category[]"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extras") + .HasColumnType("jsonb"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("ManagerId"); + + b.ToTable("component", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentAssembly", b => + { + b.Property("AssembledComponentId") + .HasColumnType("uuid"); + + b.Property("PartComponentId") + .HasColumnType("uuid"); + + b.Property("Index") + .HasColumnType("smallint"); + + b.Property("PrimeSurface") + .HasColumnType("metabase.prime_surface"); + + b.HasKey("AssembledComponentId", "PartComponentId"); + + b.HasIndex("PartComponentId"); + + b.ToTable("component_assembly", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentConcretizationAndGeneralization", b => + { + b.Property("GeneralComponentId") + .HasColumnType("uuid"); + + b.Property("ConcreteComponentId") + .HasColumnType("uuid"); + + b.HasKey("GeneralComponentId", "ConcreteComponentId"); + + b.HasIndex("ConcreteComponentId"); + + b.ToTable("component_concretization_and_generalization", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentManufacturer", b => + { + b.Property("ComponentId") + .HasColumnType("uuid"); + + b.Property("InstitutionId") + .HasColumnType("uuid"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.HasKey("ComponentId", "InstitutionId"); + + b.HasIndex("InstitutionId"); + + b.ToTable("component_manufacturer", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentVariant", b => + { + b.Property("OfComponentId") + .HasColumnType("uuid"); + + b.Property("ToComponentId") + .HasColumnType("uuid"); + + b.HasKey("OfComponentId", "ToComponentId"); + + b.HasIndex("ToComponentId"); + + b.ToTable("component_variant", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.DataFormat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extension") + .HasColumnType("text"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("MediaType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SchemaLocator") + .HasColumnType("text"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("ManagerId"); + + b.ToTable("data_format", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Database", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Locator") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OperatorId") + .HasColumnType("uuid"); + + b.Property("VerificationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerificationState") + .HasColumnType("metabase.database_verification_state"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("OperatorId"); + + b.ToTable("database", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.GnuPgKeyFingerprint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AllowedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Fingerprint") + .IsRequired() + .HasColumnType("text"); + + b.Property("ForbiddenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("InstitutionId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("Fingerprint") + .IsUnique(); + + b.HasIndex("InstitutionId"); + + b.HasIndex("UserId"); + + b.ToTable("gnu_pg_fingerprint", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Institution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Abbreviation") + .HasColumnType("text"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extras") + .HasColumnType("jsonb"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OperatingState") + .HasColumnType("metabase.institution_operating_state"); + + b.Property("State") + .HasColumnType("metabase.institution_state"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("ManagerId"); + + b.ToTable("institution", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionMethodDeveloper", b => + { + b.Property("InstitutionId") + .HasColumnType("uuid"); + + b.Property("MethodId") + .HasColumnType("uuid"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.HasKey("InstitutionId", "MethodId"); + + b.HasIndex("MethodId"); + + b.ToTable("institution_method_developer", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionRepresentative", b => + { + b.Property("InstitutionId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.Property("Role") + .HasColumnType("metabase.institution_representative_role"); + + b.HasKey("InstitutionId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("institution_representative", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Method", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property?>("Availability") + .HasColumnType("tstzrange"); + + b.Property("CalculationLocator") + .HasColumnType("text"); + + b.PrimitiveCollection("Categories") + .IsRequired() + .HasColumnType("metabase.method_category[]"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property?>("Validity") + .HasColumnType("tstzrange"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("ManagerId"); + + b.ToTable("method", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + 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("OwnerId") + .HasColumnType("uuid"); + + 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.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.HasIndex("OwnerId"); + + b.ToTable("OpenIddictApplications", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + 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.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + 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.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AuthorizationId") + .HasColumnType("uuid"); + + 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.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("role", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.RoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("role_claim", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("PostalAddress") + .HasColumnType("text"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("WebsiteLocator") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("user", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("user_claim", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("user_login", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserMethodDeveloper", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("MethodId") + .HasColumnType("uuid"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.HasKey("UserId", "MethodId"); + + b.HasIndex("MethodId"); + + b.ToTable("user_method_developer", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("user_role", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("user_token", "metabase"); + }); + + 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", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Component", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedComponents") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Metabase.Data.DescriptionOrReference", "PrimeDirection", b1 => + { + b1.Property("ComponentId") + .HasColumnType("uuid"); + + b1.Property("Description") + .HasColumnType("text"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("ComponentId"); + + b1.ToTable("component", "metabase"); + + b1.WithOwner() + .HasForeignKey("ComponentId"); + + b1.OwnsOne("Metabase.Data.Reference", "Reference", b2 => + { + b2.Property("DescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DescriptionOrReferenceComponentId"); + + b2.ToTable("component", "metabase"); + + b2.WithOwner() + .HasForeignKey("DescriptionOrReferenceComponentId"); + + b2.OwnsOne("Metabase.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + 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("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + }); + + b2.OwnsOne("Metabase.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.OwnsOne("Metabase.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDescriptionOrReferenceComponentId"); + + b4.ToTable("component", "metabase"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDescriptionOrReferenceComponentId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Reference"); + }); + + b.OwnsOne("Metabase.Data.DescriptionOrReference", "PrimeSurface", b1 => + { + b1.Property("ComponentId") + .HasColumnType("uuid"); + + b1.Property("Description") + .HasColumnType("text"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("ComponentId"); + + b1.ToTable("component", "metabase"); + + b1.WithOwner() + .HasForeignKey("ComponentId"); + + b1.OwnsOne("Metabase.Data.Reference", "Reference", b2 => + { + b2.Property("DescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DescriptionOrReferenceComponentId"); + + b2.ToTable("component", "metabase"); + + b2.WithOwner() + .HasForeignKey("DescriptionOrReferenceComponentId"); + + b2.OwnsOne("Metabase.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + 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("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + }); + + b2.OwnsOne("Metabase.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.OwnsOne("Metabase.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDescriptionOrReferenceComponentId"); + + b4.ToTable("component", "metabase"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDescriptionOrReferenceComponentId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Reference"); + }); + + b.OwnsOne("Metabase.Data.DescriptionOrReference", "SwitchableLayers", b1 => + { + b1.Property("ComponentId") + .HasColumnType("uuid"); + + b1.Property("Description") + .HasColumnType("text"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("ComponentId"); + + b1.ToTable("component", "metabase"); + + b1.WithOwner() + .HasForeignKey("ComponentId"); + + b1.OwnsOne("Metabase.Data.Reference", "Reference", b2 => + { + b2.Property("DescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DescriptionOrReferenceComponentId"); + + b2.ToTable("component", "metabase"); + + b2.WithOwner() + .HasForeignKey("DescriptionOrReferenceComponentId"); + + b2.OwnsOne("Metabase.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + 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("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + }); + + b2.OwnsOne("Metabase.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.OwnsOne("Metabase.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDescriptionOrReferenceComponentId"); + + b4.ToTable("component", "metabase"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDescriptionOrReferenceComponentId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Reference"); + }); + + b.Navigation("Manager"); + + b.Navigation("PrimeDirection"); + + b.Navigation("PrimeSurface"); + + b.Navigation("SwitchableLayers"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentAssembly", b => + { + b.HasOne("Metabase.Data.Component", "AssembledComponent") + .WithMany("PartEdges") + .HasForeignKey("AssembledComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Component", "PartComponent") + .WithMany("PartOfEdges") + .HasForeignKey("PartComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AssembledComponent"); + + b.Navigation("PartComponent"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentConcretizationAndGeneralization", b => + { + b.HasOne("Metabase.Data.Component", "ConcreteComponent") + .WithMany("GeneralizationEdges") + .HasForeignKey("ConcreteComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Component", "GeneralComponent") + .WithMany("ConcretizationEdges") + .HasForeignKey("GeneralComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ConcreteComponent"); + + b.Navigation("GeneralComponent"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentManufacturer", b => + { + b.HasOne("Metabase.Data.Component", "Component") + .WithMany("ManufacturerEdges") + .HasForeignKey("ComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("ManufacturedComponentEdges") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Component"); + + b.Navigation("Institution"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentVariant", b => + { + b.HasOne("Metabase.Data.Component", "OfComponent") + .WithMany("VariantEdges") + .HasForeignKey("OfComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Component", "ToComponent") + .WithMany("VariantOfEdges") + .HasForeignKey("ToComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OfComponent"); + + b.Navigation("ToComponent"); + }); + + modelBuilder.Entity("Metabase.Data.DataFormat", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedDataFormats") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Metabase.Data.Reference", "Reference", b1 => + { + b1.Property("DataFormatId") + .HasColumnType("uuid"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("DataFormatId"); + + b1.ToTable("data_format", "metabase"); + + b1.WithOwner() + .HasForeignKey("DataFormatId"); + + b1.OwnsOne("Metabase.Data.Publication", "Publication", b2 => + { + b2.Property("ReferenceDataFormatId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("ArXiv") + .HasColumnType("text"); + + b2.PrimitiveCollection("Authors") + .HasColumnType("text[]"); + + b2.Property("Doi") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Urn") + .HasColumnType("text"); + + b2.Property("WebAddress") + .HasColumnType("text"); + + b2.HasKey("ReferenceDataFormatId"); + + b2.ToTable("data_format", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceDataFormatId"); + }); + + b1.OwnsOne("Metabase.Data.Standard", "Standard", b2 => + { + b2.Property("ReferenceDataFormatId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Locator") + .HasColumnType("text"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Year") + .HasColumnType("integer"); + + b2.HasKey("ReferenceDataFormatId"); + + b2.ToTable("data_format", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceDataFormatId"); + + b2.OwnsOne("Metabase.Data.Numeration", "Numeration", b3 => + { + b3.Property("StandardReferenceDataFormatId") + .HasColumnType("uuid"); + + b3.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b3.Property("Prefix") + .HasColumnType("text"); + + b3.Property("Suffix") + .HasColumnType("text"); + + b3.HasKey("StandardReferenceDataFormatId"); + + b3.ToTable("data_format", "metabase"); + + b3.WithOwner() + .HasForeignKey("StandardReferenceDataFormatId"); + }); + + b2.Navigation("Numeration") + .IsRequired(); + }); + + b1.Navigation("Publication"); + + b1.Navigation("Standard"); + }); + + b.Navigation("Manager"); + + b.Navigation("Reference"); + }); + + modelBuilder.Entity("Metabase.Data.Database", b => + { + b.HasOne("Metabase.Data.Institution", "Operator") + .WithMany("OperatedDatabases") + .HasForeignKey("OperatorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Operator"); + }); + + modelBuilder.Entity("Metabase.Data.GnuPgKeyFingerprint", b => + { + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("GnuPgKeyFingerprints") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", "User") + .WithMany("GnuPgKeyFingerprints") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Institution"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Metabase.Data.Institution", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedInstitutions") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict); + + b.OwnsOne("Metabase.Data.ContactInformation", "Contact", b1 => + { + b1.Property("InstitutionId") + .HasColumnType("uuid"); + + b1.Property("EmailAddress") + .HasColumnType("text"); + + b1.Property("IsEmailAddressConfirmed") + .HasColumnType("boolean"); + + b1.Property("IsPhoneNumberConfirmed") + .HasColumnType("boolean"); + + b1.Property("PhoneNumber") + .HasColumnType("text"); + + b1.Property("PostalAddress") + .HasColumnType("text"); + + b1.Property("WebsiteLocator") + .HasColumnType("text"); + + b1.HasKey("InstitutionId"); + + b1.ToTable("institution", "metabase"); + + b1.WithOwner() + .HasForeignKey("InstitutionId"); + }); + + b.Navigation("Contact"); + + b.Navigation("Manager"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionMethodDeveloper", b => + { + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("DevelopedMethodEdges") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Method", "Method") + .WithMany("InstitutionDeveloperEdges") + .HasForeignKey("MethodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Institution"); + + b.Navigation("Method"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionRepresentative", b => + { + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("RepresentativeEdges") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", "User") + .WithMany("RepresentedInstitutionEdges") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Institution"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Metabase.Data.Method", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedMethods") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Metabase.Data.Reference", "Reference", b1 => + { + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("MethodId"); + + b1.ToTable("method", "metabase"); + + b1.WithOwner() + .HasForeignKey("MethodId"); + + b1.OwnsOne("Metabase.Data.Publication", "Publication", b2 => + { + b2.Property("ReferenceMethodId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("ArXiv") + .HasColumnType("text"); + + b2.PrimitiveCollection("Authors") + .HasColumnType("text[]"); + + b2.Property("Doi") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Urn") + .HasColumnType("text"); + + b2.Property("WebAddress") + .HasColumnType("text"); + + b2.HasKey("ReferenceMethodId"); + + b2.ToTable("method", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceMethodId"); + }); + + b1.OwnsOne("Metabase.Data.Standard", "Standard", b2 => + { + b2.Property("ReferenceMethodId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Locator") + .HasColumnType("text"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Year") + .HasColumnType("integer"); + + b2.HasKey("ReferenceMethodId"); + + b2.ToTable("method", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceMethodId"); + + b2.OwnsOne("Metabase.Data.Numeration", "Numeration", b3 => + { + b3.Property("StandardReferenceMethodId") + .HasColumnType("uuid"); + + b3.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b3.Property("Prefix") + .HasColumnType("text"); + + b3.Property("Suffix") + .HasColumnType("text"); + + b3.HasKey("StandardReferenceMethodId"); + + b3.ToTable("method", "metabase"); + + b3.WithOwner() + .HasForeignKey("StandardReferenceMethodId"); + }); + + b2.Navigation("Numeration") + .IsRequired(); + }); + + b1.Navigation("Publication"); + + b1.Navigation("Standard"); + }); + + b.OwnsMany("Metabase.Data.MethodParameter", "Parameters", b1 => + { + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Type") + .HasColumnType("jsonb"); + + b1.HasKey("MethodId", "Id"); + + b1.ToTable("MethodParameter", "metabase"); + + b1.WithOwner() + .HasForeignKey("MethodId"); + }); + + b.OwnsMany("Metabase.Data.MethodSource", "Sources", b1 => + { + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("MethodId", "Id"); + + b1.ToTable("MethodSource", "metabase"); + + b1.WithOwner() + .HasForeignKey("MethodId"); + }); + + b.Navigation("Manager"); + + b.Navigation("Parameters"); + + b.Navigation("Reference"); + + b.Navigation("Sources"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", b => + { + b.HasOne("Metabase.Data.Institution", "Owner") + .WithMany("OpenIdConnectApplications") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", b => + { + b.HasOne("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectToken", b => + { + b.HasOne("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("Metabase.Data.RoleClaim", b => + { + b.HasOne("Metabase.Data.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserClaim", b => + { + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserLogin", b => + { + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserMethodDeveloper", b => + { + b.HasOne("Metabase.Data.Method", "Method") + .WithMany("UserDeveloperEdges") + .HasForeignKey("MethodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", "User") + .WithMany("DevelopedMethodEdges") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Method"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Metabase.Data.UserRole", b => + { + b.HasOne("Metabase.Data.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserToken", b => + { + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.Component", b => + { + b.Navigation("ConcretizationEdges"); + + b.Navigation("GeneralizationEdges"); + + b.Navigation("ManufacturerEdges"); + + b.Navigation("PartEdges"); + + b.Navigation("PartOfEdges"); + + b.Navigation("VariantEdges"); + + b.Navigation("VariantOfEdges"); + }); + + modelBuilder.Entity("Metabase.Data.Institution", b => + { + b.Navigation("DevelopedMethodEdges"); + + b.Navigation("GnuPgKeyFingerprints"); + + b.Navigation("ManagedComponents"); + + b.Navigation("ManagedDataFormats"); + + b.Navigation("ManagedInstitutions"); + + b.Navigation("ManagedMethods"); + + b.Navigation("ManufacturedComponentEdges"); + + b.Navigation("OpenIdConnectApplications"); + + b.Navigation("OperatedDatabases"); + + b.Navigation("RepresentativeEdges"); + }); + + modelBuilder.Entity("Metabase.Data.Method", b => + { + b.Navigation("InstitutionDeveloperEdges"); + + b.Navigation("UserDeveloperEdges"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", b => + { + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Metabase.Data.User", b => + { + b.Navigation("DevelopedMethodEdges"); + + b.Navigation("GnuPgKeyFingerprints"); + + b.Navigation("RepresentedInstitutionEdges"); + }); +#pragma warning restore 612, 618 + } + } +} \ No newline at end of file diff --git a/backend/src/Migrations/20260328153447_CorrectExistsFlagsOfReferencesSecondAttempt.cs b/backend/src/Migrations/20260328153447_CorrectExistsFlagsOfReferencesSecondAttempt.cs new file mode 100644 index 000000000..eba82599b --- /dev/null +++ b/backend/src/Migrations/20260328153447_CorrectExistsFlagsOfReferencesSecondAttempt.cs @@ -0,0 +1,328 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Metabase.Migrations +{ + /// + public partial class CorrectExistsFlagsOfReferencesSecondAttempt : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" + UPDATE metabase.method + SET + ""Reference_Standard_Exists"" = CASE WHEN + ""Reference_Standard_Title"" IS NOT NULL OR + ""Reference_Standard_Abstract"" IS NOT NULL OR + ""Reference_Standard_Section"" IS NOT NULL OR + ""Reference_Standard_Year"" IS NOT NULL OR + ""Reference_Standard_Numeration_Prefix"" IS NOT NULL OR + ""Reference_Standard_Numeration_MainNumber"" IS NOT NULL OR + ""Reference_Standard_Numeration_Suffix"" IS NOT NULL OR + ""Reference_Standard_Standardizers"" IS NOT NULL OR + ""Reference_Standard_Locator"" IS NOT NULL + THEN TRUE + ELSE NULL + END, + ""Reference_Publication_Exists"" = CASE WHEN + ""Reference_Publication_Title"" IS NOT NULL OR + ""Reference_Publication_Abstract"" IS NOT NULL OR + ""Reference_Publication_Section"" IS NOT NULL OR + ""Reference_Publication_Authors"" IS NOT NULL OR + ""Reference_Publication_Doi"" IS NOT NULL OR + ""Reference_Publication_ArXiv"" IS NOT NULL OR + ""Reference_Publication_Urn"" IS NOT NULL OR + ""Reference_Publication_WebAddress"" IS NOT NULL + THEN TRUE + ELSE NULL + END, + ""Reference_Exists"" = CASE WHEN + ""Reference_Standard_Title"" IS NOT NULL OR + ""Reference_Standard_Abstract"" IS NOT NULL OR + ""Reference_Standard_Section"" IS NOT NULL OR + ""Reference_Standard_Year"" IS NOT NULL OR + ""Reference_Standard_Numeration_Prefix"" IS NOT NULL OR + ""Reference_Standard_Numeration_MainNumber"" IS NOT NULL OR + ""Reference_Standard_Numeration_Suffix"" IS NOT NULL OR + ""Reference_Standard_Standardizers"" IS NOT NULL OR + ""Reference_Standard_Locator"" IS NOT NULL OR + ""Reference_Publication_Title"" IS NOT NULL OR + ""Reference_Publication_Abstract"" IS NOT NULL OR + ""Reference_Publication_Section"" IS NOT NULL OR + ""Reference_Publication_Authors"" IS NOT NULL OR + ""Reference_Publication_Doi"" IS NOT NULL OR + ""Reference_Publication_ArXiv"" IS NOT NULL OR + ""Reference_Publication_Urn"" IS NOT NULL OR + ""Reference_Publication_WebAddress"" IS NOT NULL + THEN TRUE + ELSE NULL + END; + + UPDATE metabase.data_format + SET + ""Reference_Standard_Exists"" = CASE WHEN + ""Reference_Standard_Title"" IS NOT NULL OR + ""Reference_Standard_Abstract"" IS NOT NULL OR + ""Reference_Standard_Section"" IS NOT NULL OR + ""Reference_Standard_Year"" IS NOT NULL OR + ""Reference_Standard_Numeration_Prefix"" IS NOT NULL OR + ""Reference_Standard_Numeration_MainNumber"" IS NOT NULL OR + ""Reference_Standard_Numeration_Suffix"" IS NOT NULL OR + ""Reference_Standard_Standardizers"" IS NOT NULL OR + ""Reference_Standard_Locator"" IS NOT NULL + THEN TRUE + ELSE NULL + END, + ""Reference_Publication_Exists"" = CASE WHEN + ""Reference_Publication_Title"" IS NOT NULL OR + ""Reference_Publication_Abstract"" IS NOT NULL OR + ""Reference_Publication_Section"" IS NOT NULL OR + ""Reference_Publication_Authors"" IS NOT NULL OR + ""Reference_Publication_Doi"" IS NOT NULL OR + ""Reference_Publication_ArXiv"" IS NOT NULL OR + ""Reference_Publication_Urn"" IS NOT NULL OR + ""Reference_Publication_WebAddress"" IS NOT NULL + THEN TRUE + ELSE NULL + END, + ""Reference_Exists"" = CASE WHEN + ""Reference_Standard_Title"" IS NOT NULL OR + ""Reference_Standard_Abstract"" IS NOT NULL OR + ""Reference_Standard_Section"" IS NOT NULL OR + ""Reference_Standard_Year"" IS NOT NULL OR + ""Reference_Standard_Numeration_Prefix"" IS NOT NULL OR + ""Reference_Standard_Numeration_MainNumber"" IS NOT NULL OR + ""Reference_Standard_Numeration_Suffix"" IS NOT NULL OR + ""Reference_Standard_Standardizers"" IS NOT NULL OR + ""Reference_Standard_Locator"" IS NOT NULL OR + ""Reference_Publication_Title"" IS NOT NULL OR + ""Reference_Publication_Abstract"" IS NOT NULL OR + ""Reference_Publication_Section"" IS NOT NULL OR + ""Reference_Publication_Authors"" IS NOT NULL OR + ""Reference_Publication_Doi"" IS NOT NULL OR + ""Reference_Publication_ArXiv"" IS NOT NULL OR + ""Reference_Publication_Urn"" IS NOT NULL OR + ""Reference_Publication_WebAddress"" IS NOT NULL + THEN TRUE + ELSE NULL + END; + + UPDATE metabase.component + SET + ""PrimeSurface_Reference_Standard_Exists"" = CASE WHEN + ""PrimeSurface_Reference_Standard_Title"" IS NOT NULL OR + ""PrimeSurface_Reference_Standard_Abstract"" IS NOT NULL OR + ""PrimeSurface_Reference_Standard_Section"" IS NOT NULL OR + ""PrimeSurface_Reference_Standard_Year"" IS NOT NULL OR + ""PrimeSurface_Reference_Standard_Numeration_Prefix"" IS NOT NULL OR + ""PrimeSurface_Reference_Standard_Numeration_MainNumber"" IS NOT NULL OR + ""PrimeSurface_Reference_Standard_Numeration_Suffix"" IS NOT NULL OR + ""PrimeSurface_Reference_Standard_Standardizers"" IS NOT NULL OR + ""PrimeSurface_Reference_Standard_Locator"" IS NOT NULL + THEN TRUE + ELSE NULL + END, + ""PrimeSurface_Reference_Publication_Exists"" = CASE WHEN + ""PrimeSurface_Reference_Publication_Title"" IS NOT NULL OR + ""PrimeSurface_Reference_Publication_Abstract"" IS NOT NULL OR + ""PrimeSurface_Reference_Publication_Section"" IS NOT NULL OR + ""PrimeSurface_Reference_Publication_Authors"" IS NOT NULL OR + ""PrimeSurface_Reference_Publication_Doi"" IS NOT NULL OR + ""PrimeSurface_Reference_Publication_ArXiv"" IS NOT NULL OR + ""PrimeSurface_Reference_Publication_Urn"" IS NOT NULL OR + ""PrimeSurface_Reference_Publication_WebAddress"" IS NOT NULL + THEN TRUE + ELSE NULL + END, + ""PrimeSurface_Reference_Exists"" = CASE WHEN + ""PrimeSurface_Reference_Standard_Title"" IS NOT NULL OR + ""PrimeSurface_Reference_Standard_Abstract"" IS NOT NULL OR + ""PrimeSurface_Reference_Standard_Section"" IS NOT NULL OR + ""PrimeSurface_Reference_Standard_Year"" IS NOT NULL OR + ""PrimeSurface_Reference_Standard_Numeration_Prefix"" IS NOT NULL OR + ""PrimeSurface_Reference_Standard_Numeration_MainNumber"" IS NOT NULL OR + ""PrimeSurface_Reference_Standard_Numeration_Suffix"" IS NOT NULL OR + ""PrimeSurface_Reference_Standard_Standardizers"" IS NOT NULL OR + ""PrimeSurface_Reference_Standard_Locator"" IS NOT NULL OR + ""PrimeSurface_Reference_Publication_Title"" IS NOT NULL OR + ""PrimeSurface_Reference_Publication_Abstract"" IS NOT NULL OR + ""PrimeSurface_Reference_Publication_Section"" IS NOT NULL OR + ""PrimeSurface_Reference_Publication_Authors"" IS NOT NULL OR + ""PrimeSurface_Reference_Publication_Doi"" IS NOT NULL OR + ""PrimeSurface_Reference_Publication_ArXiv"" IS NOT NULL OR + ""PrimeSurface_Reference_Publication_Urn"" IS NOT NULL OR + ""PrimeSurface_Reference_Publication_WebAddress"" IS NOT NULL + THEN TRUE + ELSE NULL + END, + ""PrimeSurface_Exists"" = CASE WHEN + ""PrimeSurface_Description"" IS NOT NULL OR + ""PrimeSurface_Reference_Standard_Title"" IS NOT NULL OR + ""PrimeSurface_Reference_Standard_Abstract"" IS NOT NULL OR + ""PrimeSurface_Reference_Standard_Section"" IS NOT NULL OR + ""PrimeSurface_Reference_Standard_Year"" IS NOT NULL OR + ""PrimeSurface_Reference_Standard_Numeration_Prefix"" IS NOT NULL OR + ""PrimeSurface_Reference_Standard_Numeration_MainNumber"" IS NOT NULL OR + ""PrimeSurface_Reference_Standard_Numeration_Suffix"" IS NOT NULL OR + ""PrimeSurface_Reference_Standard_Standardizers"" IS NOT NULL OR + ""PrimeSurface_Reference_Standard_Locator"" IS NOT NULL OR + ""PrimeSurface_Reference_Publication_Title"" IS NOT NULL OR + ""PrimeSurface_Reference_Publication_Abstract"" IS NOT NULL OR + ""PrimeSurface_Reference_Publication_Section"" IS NOT NULL OR + ""PrimeSurface_Reference_Publication_Authors"" IS NOT NULL OR + ""PrimeSurface_Reference_Publication_Doi"" IS NOT NULL OR + ""PrimeSurface_Reference_Publication_ArXiv"" IS NOT NULL OR + ""PrimeSurface_Reference_Publication_Urn"" IS NOT NULL OR + ""PrimeSurface_Reference_Publication_WebAddress"" IS NOT NULL + THEN TRUE + ELSE NULL + END, + + ""PrimeDirection_Reference_Standard_Exists"" = CASE WHEN + ""PrimeDirection_Reference_Standard_Title"" IS NOT NULL OR + ""PrimeDirection_Reference_Standard_Abstract"" IS NOT NULL OR + ""PrimeDirection_Reference_Standard_Section"" IS NOT NULL OR + ""PrimeDirection_Reference_Standard_Year"" IS NOT NULL OR + ""PrimeDirection_Reference_Standard_Numeration_Prefix"" IS NOT NULL OR + ""PrimeDirection_Reference_Standard_Numeration_MainNumber"" IS NOT NULL OR + ""PrimeDirection_Reference_Standard_Numeration_Suffix"" IS NOT NULL OR + ""PrimeDirection_Reference_Standard_Standardizers"" IS NOT NULL OR + ""PrimeDirection_Reference_Standard_Locator"" IS NOT NULL + THEN TRUE + ELSE NULL + END, + ""PrimeDirection_Reference_Publication_Exists"" = CASE WHEN + ""PrimeDirection_Reference_Publication_Title"" IS NOT NULL OR + ""PrimeDirection_Reference_Publication_Abstract"" IS NOT NULL OR + ""PrimeDirection_Reference_Publication_Section"" IS NOT NULL OR + ""PrimeDirection_Reference_Publication_Authors"" IS NOT NULL OR + ""PrimeDirection_Reference_Publication_Doi"" IS NOT NULL OR + ""PrimeDirection_Reference_Publication_ArXiv"" IS NOT NULL OR + ""PrimeDirection_Reference_Publication_Urn"" IS NOT NULL OR + ""PrimeDirection_Reference_Publication_WebAddress"" IS NOT NULL + THEN TRUE + ELSE NULL + END, + ""PrimeDirection_Reference_Exists"" = CASE WHEN + ""PrimeDirection_Reference_Standard_Title"" IS NOT NULL OR + ""PrimeDirection_Reference_Standard_Abstract"" IS NOT NULL OR + ""PrimeDirection_Reference_Standard_Section"" IS NOT NULL OR + ""PrimeDirection_Reference_Standard_Year"" IS NOT NULL OR + ""PrimeDirection_Reference_Standard_Numeration_Prefix"" IS NOT NULL OR + ""PrimeDirection_Reference_Standard_Numeration_MainNumber"" IS NOT NULL OR + ""PrimeDirection_Reference_Standard_Numeration_Suffix"" IS NOT NULL OR + ""PrimeDirection_Reference_Standard_Standardizers"" IS NOT NULL OR + ""PrimeDirection_Reference_Standard_Locator"" IS NOT NULL OR + ""PrimeDirection_Reference_Publication_Title"" IS NOT NULL OR + ""PrimeDirection_Reference_Publication_Abstract"" IS NOT NULL OR + ""PrimeDirection_Reference_Publication_Section"" IS NOT NULL OR + ""PrimeDirection_Reference_Publication_Authors"" IS NOT NULL OR + ""PrimeDirection_Reference_Publication_Doi"" IS NOT NULL OR + ""PrimeDirection_Reference_Publication_ArXiv"" IS NOT NULL OR + ""PrimeDirection_Reference_Publication_Urn"" IS NOT NULL OR + ""PrimeDirection_Reference_Publication_WebAddress"" IS NOT NULL + THEN TRUE + ELSE NULL + END, + ""PrimeDirection_Exists"" = CASE WHEN + ""PrimeDirection_Description"" IS NOT NULL OR + ""PrimeDirection_Reference_Standard_Title"" IS NOT NULL OR + ""PrimeDirection_Reference_Standard_Abstract"" IS NOT NULL OR + ""PrimeDirection_Reference_Standard_Section"" IS NOT NULL OR + ""PrimeDirection_Reference_Standard_Year"" IS NOT NULL OR + ""PrimeDirection_Reference_Standard_Numeration_Prefix"" IS NOT NULL OR + ""PrimeDirection_Reference_Standard_Numeration_MainNumber"" IS NOT NULL OR + ""PrimeDirection_Reference_Standard_Numeration_Suffix"" IS NOT NULL OR + ""PrimeDirection_Reference_Standard_Standardizers"" IS NOT NULL OR + ""PrimeDirection_Reference_Standard_Locator"" IS NOT NULL OR + ""PrimeDirection_Reference_Publication_Title"" IS NOT NULL OR + ""PrimeDirection_Reference_Publication_Abstract"" IS NOT NULL OR + ""PrimeDirection_Reference_Publication_Section"" IS NOT NULL OR + ""PrimeDirection_Reference_Publication_Authors"" IS NOT NULL OR + ""PrimeDirection_Reference_Publication_Doi"" IS NOT NULL OR + ""PrimeDirection_Reference_Publication_ArXiv"" IS NOT NULL OR + ""PrimeDirection_Reference_Publication_Urn"" IS NOT NULL OR + ""PrimeDirection_Reference_Publication_WebAddress"" IS NOT NULL + THEN TRUE + ELSE NULL + END, + + ""SwitchableLayers_Reference_Standard_Exists"" = CASE WHEN + ""SwitchableLayers_Reference_Standard_Title"" IS NOT NULL OR + ""SwitchableLayers_Reference_Standard_Abstract"" IS NOT NULL OR + ""SwitchableLayers_Reference_Standard_Section"" IS NOT NULL OR + ""SwitchableLayers_Reference_Standard_Year"" IS NOT NULL OR + ""SwitchableLayers_Reference_Standard_Numeration_Prefix"" IS NOT NULL OR + ""SwitchableLayers_Reference_Standard_Numeration_MainNumber"" IS NOT NULL OR + ""SwitchableLayers_Reference_Standard_Numeration_Suffix"" IS NOT NULL OR + ""SwitchableLayers_Reference_Standard_Standardizers"" IS NOT NULL OR + ""SwitchableLayers_Reference_Standard_Locator"" IS NOT NULL + THEN TRUE + ELSE NULL + END, + ""SwitchableLayers_Reference_Publication_Exists"" = CASE WHEN + ""SwitchableLayers_Reference_Publication_Title"" IS NOT NULL OR + ""SwitchableLayers_Reference_Publication_Abstract"" IS NOT NULL OR + ""SwitchableLayers_Reference_Publication_Section"" IS NOT NULL OR + ""SwitchableLayers_Reference_Publication_Authors"" IS NOT NULL OR + ""SwitchableLayers_Reference_Publication_Doi"" IS NOT NULL OR + ""SwitchableLayers_Reference_Publication_ArXiv"" IS NOT NULL OR + ""SwitchableLayers_Reference_Publication_Urn"" IS NOT NULL OR + ""SwitchableLayers_Reference_Publication_WebAddress"" IS NOT NULL + THEN TRUE + ELSE NULL + END, + ""SwitchableLayers_Reference_Exists"" = CASE WHEN + ""SwitchableLayers_Reference_Standard_Title"" IS NOT NULL OR + ""SwitchableLayers_Reference_Standard_Abstract"" IS NOT NULL OR + ""SwitchableLayers_Reference_Standard_Section"" IS NOT NULL OR + ""SwitchableLayers_Reference_Standard_Year"" IS NOT NULL OR + ""SwitchableLayers_Reference_Standard_Numeration_Prefix"" IS NOT NULL OR + ""SwitchableLayers_Reference_Standard_Numeration_MainNumber"" IS NOT NULL OR + ""SwitchableLayers_Reference_Standard_Numeration_Suffix"" IS NOT NULL OR + ""SwitchableLayers_Reference_Standard_Standardizers"" IS NOT NULL OR + ""SwitchableLayers_Reference_Standard_Locator"" IS NOT NULL OR + ""SwitchableLayers_Reference_Publication_Title"" IS NOT NULL OR + ""SwitchableLayers_Reference_Publication_Abstract"" IS NOT NULL OR + ""SwitchableLayers_Reference_Publication_Section"" IS NOT NULL OR + ""SwitchableLayers_Reference_Publication_Authors"" IS NOT NULL OR + ""SwitchableLayers_Reference_Publication_Doi"" IS NOT NULL OR + ""SwitchableLayers_Reference_Publication_ArXiv"" IS NOT NULL OR + ""SwitchableLayers_Reference_Publication_Urn"" IS NOT NULL OR + ""SwitchableLayers_Reference_Publication_WebAddress"" IS NOT NULL + THEN TRUE + ELSE NULL + END, + ""SwitchableLayers_Exists"" = CASE WHEN + ""SwitchableLayers_Description"" IS NOT NULL OR + ""SwitchableLayers_Reference_Standard_Title"" IS NOT NULL OR + ""SwitchableLayers_Reference_Standard_Abstract"" IS NOT NULL OR + ""SwitchableLayers_Reference_Standard_Section"" IS NOT NULL OR + ""SwitchableLayers_Reference_Standard_Year"" IS NOT NULL OR + ""SwitchableLayers_Reference_Standard_Numeration_Prefix"" IS NOT NULL OR + ""SwitchableLayers_Reference_Standard_Numeration_MainNumber"" IS NOT NULL OR + ""SwitchableLayers_Reference_Standard_Numeration_Suffix"" IS NOT NULL OR + ""SwitchableLayers_Reference_Standard_Standardizers"" IS NOT NULL OR + ""SwitchableLayers_Reference_Standard_Locator"" IS NOT NULL OR + ""SwitchableLayers_Reference_Publication_Title"" IS NOT NULL OR + ""SwitchableLayers_Reference_Publication_Abstract"" IS NOT NULL OR + ""SwitchableLayers_Reference_Publication_Section"" IS NOT NULL OR + ""SwitchableLayers_Reference_Publication_Authors"" IS NOT NULL OR + ""SwitchableLayers_Reference_Publication_Doi"" IS NOT NULL OR + ""SwitchableLayers_Reference_Publication_ArXiv"" IS NOT NULL OR + ""SwitchableLayers_Reference_Publication_Urn"" IS NOT NULL OR + ""SwitchableLayers_Reference_Publication_WebAddress"" IS NOT NULL + THEN TRUE + ELSE NULL + END; + "); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + } + } +} \ No newline at end of file diff --git a/backend/src/Migrations/20260402204629_AddUpdatedAndCreatedAtTimestamps.Designer.cs b/backend/src/Migrations/20260402204629_AddUpdatedAndCreatedAtTimestamps.Designer.cs new file mode 100644 index 000000000..1216893cf --- /dev/null +++ b/backend/src/Migrations/20260402204629_AddUpdatedAndCreatedAtTimestamps.Designer.cs @@ -0,0 +1,2243 @@ +// +using System; +using System.Text.Json; +using Metabase.Data; +using Metabase.Enumerations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; + +#nullable disable + +namespace Metabase.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260402204629_AddUpdatedAndCreatedAtTimestamps")] + partial class AddUpdatedAndCreatedAtTimestamps + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("metabase") + .HasAnnotation("ProductVersion", "10.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "component_category", new[] { "layer", "material", "unit" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "database_verification_state", new[] { "pending", "verified" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "institution_operating_state", new[] { "not_operating", "operating" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "institution_representative_role", new[] { "assistant", "owner" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "institution_state", new[] { "pending", "verified" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "method_category", new[] { "calculation", "measurement" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "prime_surface", new[] { "inside", "outside" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "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("Metabase.Data.Component", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Abbreviation") + .HasColumnType("text"); + + b.Property?>("Availability") + .HasColumnType("tstzrange"); + + b.PrimitiveCollection("Categories") + .IsRequired() + .HasColumnType("metabase.component_category[]"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extras") + .HasColumnType("jsonb"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("Name") + .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("ManagerId"); + + b.ToTable("component", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentAssembly", b => + { + b.Property("AssembledComponentId") + .HasColumnType("uuid"); + + b.Property("PartComponentId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Index") + .HasColumnType("smallint"); + + b.Property("PrimeSurface") + .HasColumnType("metabase.prime_surface"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("AssembledComponentId", "PartComponentId"); + + b.HasIndex("PartComponentId"); + + b.ToTable("component_assembly", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentConcretizationAndGeneralization", b => + { + b.Property("GeneralComponentId") + .HasColumnType("uuid"); + + b.Property("ConcreteComponentId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("GeneralComponentId", "ConcreteComponentId"); + + b.HasIndex("ConcreteComponentId"); + + b.ToTable("component_concretization_and_generalization", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentManufacturer", b => + { + b.Property("ComponentId") + .HasColumnType("uuid"); + + b.Property("InstitutionId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("ComponentId", "InstitutionId"); + + b.HasIndex("InstitutionId"); + + b.ToTable("component_manufacturer", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentVariant", b => + { + b.Property("OfComponentId") + .HasColumnType("uuid"); + + b.Property("ToComponentId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("OfComponentId", "ToComponentId"); + + b.HasIndex("ToComponentId"); + + b.ToTable("component_variant", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.DataFormat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extension") + .HasColumnType("text"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("MediaType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SchemaLocator") + .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("ManagerId"); + + b.ToTable("data_format", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Database", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Locator") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OperatorId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VerificationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerificationState") + .HasColumnType("metabase.database_verification_state"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("OperatorId"); + + b.ToTable("database", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.GnuPgKeyFingerprint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AllowedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Fingerprint") + .IsRequired() + .HasColumnType("text"); + + b.Property("ForbiddenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("InstitutionId") + .HasColumnType("uuid"); + + 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.HasKey("Id"); + + b.HasIndex("Fingerprint") + .IsUnique(); + + b.HasIndex("InstitutionId"); + + b.HasIndex("UserId"); + + b.ToTable("gnu_pg_fingerprint", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Institution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Abbreviation") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extras") + .HasColumnType("jsonb"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OperatingState") + .HasColumnType("metabase.institution_operating_state"); + + b.Property("State") + .HasColumnType("metabase.institution_state"); + + 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("ManagerId"); + + b.ToTable("institution", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionMethodDeveloper", b => + { + b.Property("InstitutionId") + .HasColumnType("uuid"); + + b.Property("MethodId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("InstitutionId", "MethodId"); + + b.HasIndex("MethodId"); + + b.ToTable("institution_method_developer", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionRepresentative", b => + { + b.Property("InstitutionId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.Property("Role") + .HasColumnType("metabase.institution_representative_role"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("InstitutionId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("institution_representative", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Method", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property?>("Availability") + .HasColumnType("tstzrange"); + + b.Property("CalculationLocator") + .HasColumnType("text"); + + b.PrimitiveCollection("Categories") + .IsRequired() + .HasColumnType("metabase.method_category[]"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property?>("Validity") + .HasColumnType("tstzrange"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("ManagerId"); + + b.ToTable("method", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + 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("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("JsonWebKeySet") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + 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.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.HasIndex("OwnerId"); + + b.ToTable("OpenIddictApplications", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + 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.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + 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.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("OpenIddictScopes", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AuthorizationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + 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.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("role", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.RoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("role_claim", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("PostalAddress") + .HasColumnType("text"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("WebsiteLocator") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("user", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("user_claim", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("user_login", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserMethodDeveloper", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("MethodId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("UserId", "MethodId"); + + b.HasIndex("MethodId"); + + b.ToTable("user_method_developer", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("user_role", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("user_token", "metabase"); + }); + + 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", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Component", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedComponents") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Metabase.Data.DescriptionOrReference", "PrimeDirection", b1 => + { + b1.Property("ComponentId") + .HasColumnType("uuid"); + + b1.Property("Description") + .HasColumnType("text"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("ComponentId"); + + b1.ToTable("component", "metabase"); + + b1.WithOwner() + .HasForeignKey("ComponentId"); + + b1.OwnsOne("Metabase.Data.Reference", "Reference", b2 => + { + b2.Property("DescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DescriptionOrReferenceComponentId"); + + b2.ToTable("component", "metabase"); + + b2.WithOwner() + .HasForeignKey("DescriptionOrReferenceComponentId"); + + b2.OwnsOne("Metabase.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + 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("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + }); + + b2.OwnsOne("Metabase.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.OwnsOne("Metabase.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDescriptionOrReferenceComponentId"); + + b4.ToTable("component", "metabase"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDescriptionOrReferenceComponentId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Reference"); + }); + + b.OwnsOne("Metabase.Data.DescriptionOrReference", "PrimeSurface", b1 => + { + b1.Property("ComponentId") + .HasColumnType("uuid"); + + b1.Property("Description") + .HasColumnType("text"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("ComponentId"); + + b1.ToTable("component", "metabase"); + + b1.WithOwner() + .HasForeignKey("ComponentId"); + + b1.OwnsOne("Metabase.Data.Reference", "Reference", b2 => + { + b2.Property("DescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DescriptionOrReferenceComponentId"); + + b2.ToTable("component", "metabase"); + + b2.WithOwner() + .HasForeignKey("DescriptionOrReferenceComponentId"); + + b2.OwnsOne("Metabase.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + 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("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + }); + + b2.OwnsOne("Metabase.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.OwnsOne("Metabase.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDescriptionOrReferenceComponentId"); + + b4.ToTable("component", "metabase"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDescriptionOrReferenceComponentId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Reference"); + }); + + b.OwnsOne("Metabase.Data.DescriptionOrReference", "SwitchableLayers", b1 => + { + b1.Property("ComponentId") + .HasColumnType("uuid"); + + b1.Property("Description") + .HasColumnType("text"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("ComponentId"); + + b1.ToTable("component", "metabase"); + + b1.WithOwner() + .HasForeignKey("ComponentId"); + + b1.OwnsOne("Metabase.Data.Reference", "Reference", b2 => + { + b2.Property("DescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DescriptionOrReferenceComponentId"); + + b2.ToTable("component", "metabase"); + + b2.WithOwner() + .HasForeignKey("DescriptionOrReferenceComponentId"); + + b2.OwnsOne("Metabase.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + 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("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + }); + + b2.OwnsOne("Metabase.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.OwnsOne("Metabase.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDescriptionOrReferenceComponentId"); + + b4.ToTable("component", "metabase"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDescriptionOrReferenceComponentId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Reference"); + }); + + b.Navigation("Manager"); + + b.Navigation("PrimeDirection"); + + b.Navigation("PrimeSurface"); + + b.Navigation("SwitchableLayers"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentAssembly", b => + { + b.HasOne("Metabase.Data.Component", "AssembledComponent") + .WithMany("PartEdges") + .HasForeignKey("AssembledComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Component", "PartComponent") + .WithMany("PartOfEdges") + .HasForeignKey("PartComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AssembledComponent"); + + b.Navigation("PartComponent"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentConcretizationAndGeneralization", b => + { + b.HasOne("Metabase.Data.Component", "ConcreteComponent") + .WithMany("GeneralizationEdges") + .HasForeignKey("ConcreteComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Component", "GeneralComponent") + .WithMany("ConcretizationEdges") + .HasForeignKey("GeneralComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ConcreteComponent"); + + b.Navigation("GeneralComponent"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentManufacturer", b => + { + b.HasOne("Metabase.Data.Component", "Component") + .WithMany("ManufacturerEdges") + .HasForeignKey("ComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("ManufacturedComponentEdges") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Component"); + + b.Navigation("Institution"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentVariant", b => + { + b.HasOne("Metabase.Data.Component", "OfComponent") + .WithMany("VariantEdges") + .HasForeignKey("OfComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Component", "ToComponent") + .WithMany("VariantOfEdges") + .HasForeignKey("ToComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OfComponent"); + + b.Navigation("ToComponent"); + }); + + modelBuilder.Entity("Metabase.Data.DataFormat", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedDataFormats") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Metabase.Data.Reference", "Reference", b1 => + { + b1.Property("DataFormatId") + .HasColumnType("uuid"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("DataFormatId"); + + b1.ToTable("data_format", "metabase"); + + b1.WithOwner() + .HasForeignKey("DataFormatId"); + + b1.OwnsOne("Metabase.Data.Publication", "Publication", b2 => + { + b2.Property("ReferenceDataFormatId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("ArXiv") + .HasColumnType("text"); + + b2.PrimitiveCollection("Authors") + .HasColumnType("text[]"); + + b2.Property("Doi") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Urn") + .HasColumnType("text"); + + b2.Property("WebAddress") + .HasColumnType("text"); + + b2.HasKey("ReferenceDataFormatId"); + + b2.ToTable("data_format", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceDataFormatId"); + }); + + b1.OwnsOne("Metabase.Data.Standard", "Standard", b2 => + { + b2.Property("ReferenceDataFormatId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Locator") + .HasColumnType("text"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Year") + .HasColumnType("integer"); + + b2.HasKey("ReferenceDataFormatId"); + + b2.ToTable("data_format", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceDataFormatId"); + + b2.OwnsOne("Metabase.Data.Numeration", "Numeration", b3 => + { + b3.Property("StandardReferenceDataFormatId") + .HasColumnType("uuid"); + + b3.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b3.Property("Prefix") + .HasColumnType("text"); + + b3.Property("Suffix") + .HasColumnType("text"); + + b3.HasKey("StandardReferenceDataFormatId"); + + b3.ToTable("data_format", "metabase"); + + b3.WithOwner() + .HasForeignKey("StandardReferenceDataFormatId"); + }); + + b2.Navigation("Numeration") + .IsRequired(); + }); + + b1.Navigation("Publication"); + + b1.Navigation("Standard"); + }); + + b.Navigation("Manager"); + + b.Navigation("Reference"); + }); + + modelBuilder.Entity("Metabase.Data.Database", b => + { + b.HasOne("Metabase.Data.Institution", "Operator") + .WithMany("OperatedDatabases") + .HasForeignKey("OperatorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Operator"); + }); + + modelBuilder.Entity("Metabase.Data.GnuPgKeyFingerprint", b => + { + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("GnuPgKeyFingerprints") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", "User") + .WithMany("GnuPgKeyFingerprints") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Institution"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Metabase.Data.Institution", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedInstitutions") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict); + + b.OwnsOne("Metabase.Data.ContactInformation", "Contact", b1 => + { + b1.Property("InstitutionId") + .HasColumnType("uuid"); + + b1.Property("EmailAddress") + .HasColumnType("text"); + + b1.Property("IsEmailAddressConfirmed") + .HasColumnType("boolean"); + + b1.Property("IsPhoneNumberConfirmed") + .HasColumnType("boolean"); + + b1.Property("PhoneNumber") + .HasColumnType("text"); + + b1.Property("PostalAddress") + .HasColumnType("text"); + + b1.Property("WebsiteLocator") + .HasColumnType("text"); + + b1.HasKey("InstitutionId"); + + b1.ToTable("institution", "metabase"); + + b1.WithOwner() + .HasForeignKey("InstitutionId"); + }); + + b.Navigation("Contact"); + + b.Navigation("Manager"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionMethodDeveloper", b => + { + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("DevelopedMethodEdges") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Method", "Method") + .WithMany("InstitutionDeveloperEdges") + .HasForeignKey("MethodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Institution"); + + b.Navigation("Method"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionRepresentative", b => + { + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("RepresentativeEdges") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", "User") + .WithMany("RepresentedInstitutionEdges") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Institution"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Metabase.Data.Method", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedMethods") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Metabase.Data.Reference", "Reference", b1 => + { + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("MethodId"); + + b1.ToTable("method", "metabase"); + + b1.WithOwner() + .HasForeignKey("MethodId"); + + b1.OwnsOne("Metabase.Data.Publication", "Publication", b2 => + { + b2.Property("ReferenceMethodId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("ArXiv") + .HasColumnType("text"); + + b2.PrimitiveCollection("Authors") + .HasColumnType("text[]"); + + b2.Property("Doi") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Urn") + .HasColumnType("text"); + + b2.Property("WebAddress") + .HasColumnType("text"); + + b2.HasKey("ReferenceMethodId"); + + b2.ToTable("method", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceMethodId"); + }); + + b1.OwnsOne("Metabase.Data.Standard", "Standard", b2 => + { + b2.Property("ReferenceMethodId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Locator") + .HasColumnType("text"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Year") + .HasColumnType("integer"); + + b2.HasKey("ReferenceMethodId"); + + b2.ToTable("method", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceMethodId"); + + b2.OwnsOne("Metabase.Data.Numeration", "Numeration", b3 => + { + b3.Property("StandardReferenceMethodId") + .HasColumnType("uuid"); + + b3.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b3.Property("Prefix") + .HasColumnType("text"); + + b3.Property("Suffix") + .HasColumnType("text"); + + b3.HasKey("StandardReferenceMethodId"); + + b3.ToTable("method", "metabase"); + + b3.WithOwner() + .HasForeignKey("StandardReferenceMethodId"); + }); + + b2.Navigation("Numeration") + .IsRequired(); + }); + + b1.Navigation("Publication"); + + b1.Navigation("Standard"); + }); + + b.OwnsMany("Metabase.Data.MethodParameter", "Parameters", b1 => + { + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Type") + .HasColumnType("jsonb"); + + b1.HasKey("MethodId", "Id"); + + b1.ToTable("MethodParameter", "metabase"); + + b1.WithOwner() + .HasForeignKey("MethodId"); + }); + + b.OwnsMany("Metabase.Data.MethodSource", "Sources", b1 => + { + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("MethodId", "Id"); + + b1.ToTable("MethodSource", "metabase"); + + b1.WithOwner() + .HasForeignKey("MethodId"); + }); + + b.Navigation("Manager"); + + b.Navigation("Parameters"); + + b.Navigation("Reference"); + + b.Navigation("Sources"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", b => + { + b.HasOne("Metabase.Data.Institution", "Owner") + .WithMany("OpenIdConnectApplications") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", b => + { + b.HasOne("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectToken", b => + { + b.HasOne("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("Metabase.Data.RoleClaim", b => + { + b.HasOne("Metabase.Data.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserClaim", b => + { + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserLogin", b => + { + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserMethodDeveloper", b => + { + b.HasOne("Metabase.Data.Method", "Method") + .WithMany("UserDeveloperEdges") + .HasForeignKey("MethodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", "User") + .WithMany("DevelopedMethodEdges") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Method"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Metabase.Data.UserRole", b => + { + b.HasOne("Metabase.Data.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserToken", b => + { + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.Component", b => + { + b.Navigation("ConcretizationEdges"); + + b.Navigation("GeneralizationEdges"); + + b.Navigation("ManufacturerEdges"); + + b.Navigation("PartEdges"); + + b.Navigation("PartOfEdges"); + + b.Navigation("VariantEdges"); + + b.Navigation("VariantOfEdges"); + }); + + modelBuilder.Entity("Metabase.Data.Institution", b => + { + b.Navigation("DevelopedMethodEdges"); + + b.Navigation("GnuPgKeyFingerprints"); + + b.Navigation("ManagedComponents"); + + b.Navigation("ManagedDataFormats"); + + b.Navigation("ManagedInstitutions"); + + b.Navigation("ManagedMethods"); + + b.Navigation("ManufacturedComponentEdges"); + + b.Navigation("OpenIdConnectApplications"); + + b.Navigation("OperatedDatabases"); + + b.Navigation("RepresentativeEdges"); + }); + + modelBuilder.Entity("Metabase.Data.Method", b => + { + b.Navigation("InstitutionDeveloperEdges"); + + b.Navigation("UserDeveloperEdges"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", b => + { + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Metabase.Data.User", b => + { + b.Navigation("DevelopedMethodEdges"); + + b.Navigation("GnuPgKeyFingerprints"); + + b.Navigation("RepresentedInstitutionEdges"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Migrations/20260402204629_AddUpdatedAndCreatedAtTimestamps.cs b/backend/src/Migrations/20260402204629_AddUpdatedAndCreatedAtTimestamps.cs new file mode 100644 index 000000000..7870f69de --- /dev/null +++ b/backend/src/Migrations/20260402204629_AddUpdatedAndCreatedAtTimestamps.cs @@ -0,0 +1,693 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Metabase.Migrations +{ + /// + public partial class AddUpdatedAndCreatedAtTimestamps : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CreatedAt", + schema: "metabase", + table: "user_method_developer", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "metabase", + table: "user_method_developer", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "xmin", + schema: "metabase", + table: "user_method_developer", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "user", + type: "uuid", + nullable: false, + defaultValueSql: "gen_random_uuid()", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AddColumn( + name: "CreatedAt", + schema: "metabase", + table: "user", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "metabase", + table: "user", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "OpenIddictTokens", + type: "uuid", + nullable: false, + defaultValueSql: "gen_random_uuid()", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AddColumn( + name: "CreatedAt", + schema: "metabase", + table: "OpenIddictTokens", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "metabase", + table: "OpenIddictTokens", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "OpenIddictScopes", + type: "uuid", + nullable: false, + defaultValueSql: "gen_random_uuid()", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AddColumn( + name: "CreatedAt", + schema: "metabase", + table: "OpenIddictScopes", + type: "timestamp with time zone", + nullable: false, + defaultValue: NodaTime.Instant.FromUnixTimeTicks(0L)); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "metabase", + table: "OpenIddictScopes", + type: "timestamp with time zone", + nullable: false, + defaultValue: NodaTime.Instant.FromUnixTimeTicks(0L)); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "OpenIddictAuthorizations", + type: "uuid", + nullable: false, + defaultValueSql: "gen_random_uuid()", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AddColumn( + name: "CreatedAt", + schema: "metabase", + table: "OpenIddictAuthorizations", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "metabase", + table: "OpenIddictAuthorizations", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "OpenIddictApplications", + type: "uuid", + nullable: false, + defaultValueSql: "gen_random_uuid()", + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AddColumn( + name: "CreatedAt", + schema: "metabase", + table: "OpenIddictApplications", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "metabase", + table: "OpenIddictApplications", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "CreatedAt", + schema: "metabase", + table: "method", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "metabase", + table: "method", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "CreatedAt", + schema: "metabase", + table: "institution_representative", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "metabase", + table: "institution_representative", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "xmin", + schema: "metabase", + table: "institution_representative", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AddColumn( + name: "CreatedAt", + schema: "metabase", + table: "institution_method_developer", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "metabase", + table: "institution_method_developer", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "xmin", + schema: "metabase", + table: "institution_method_developer", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AddColumn( + name: "CreatedAt", + schema: "metabase", + table: "institution", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "metabase", + table: "institution", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + schema: "metabase", + table: "gnu_pg_fingerprint", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()", + oldClrType: typeof(OffsetDateTime), + oldType: "timestamp with time zone"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "metabase", + table: "gnu_pg_fingerprint", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "CreatedAt", + schema: "metabase", + table: "database", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "metabase", + table: "database", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "CreatedAt", + schema: "metabase", + table: "data_format", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "metabase", + table: "data_format", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "CreatedAt", + schema: "metabase", + table: "component_variant", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "metabase", + table: "component_variant", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "xmin", + schema: "metabase", + table: "component_variant", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AddColumn( + name: "CreatedAt", + schema: "metabase", + table: "component_manufacturer", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "metabase", + table: "component_manufacturer", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "xmin", + schema: "metabase", + table: "component_manufacturer", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AddColumn( + name: "CreatedAt", + schema: "metabase", + table: "component_concretization_and_generalization", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "metabase", + table: "component_concretization_and_generalization", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "xmin", + schema: "metabase", + table: "component_concretization_and_generalization", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AddColumn( + name: "CreatedAt", + schema: "metabase", + table: "component_assembly", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "metabase", + table: "component_assembly", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "xmin", + schema: "metabase", + table: "component_assembly", + type: "xid", + rowVersion: true, + nullable: false, + defaultValue: 0u); + + migrationBuilder.AddColumn( + name: "CreatedAt", + schema: "metabase", + table: "component", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + + migrationBuilder.AddColumn( + name: "UpdatedAt", + schema: "metabase", + table: "component", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CreatedAt", + schema: "metabase", + table: "user_method_developer"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "metabase", + table: "user_method_developer"); + + migrationBuilder.DropColumn( + name: "xmin", + schema: "metabase", + table: "user_method_developer"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + schema: "metabase", + table: "user"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "metabase", + table: "user"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + schema: "metabase", + table: "OpenIddictTokens"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "metabase", + table: "OpenIddictTokens"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + schema: "metabase", + table: "OpenIddictScopes"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "metabase", + table: "OpenIddictScopes"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + schema: "metabase", + table: "OpenIddictAuthorizations"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "metabase", + table: "OpenIddictAuthorizations"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + schema: "metabase", + table: "OpenIddictApplications"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "metabase", + table: "OpenIddictApplications"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + schema: "metabase", + table: "method"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "metabase", + table: "method"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + schema: "metabase", + table: "institution_representative"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "metabase", + table: "institution_representative"); + + migrationBuilder.DropColumn( + name: "xmin", + schema: "metabase", + table: "institution_representative"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + schema: "metabase", + table: "institution_method_developer"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "metabase", + table: "institution_method_developer"); + + migrationBuilder.DropColumn( + name: "xmin", + schema: "metabase", + table: "institution_method_developer"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + schema: "metabase", + table: "institution"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "metabase", + table: "institution"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "metabase", + table: "gnu_pg_fingerprint"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + schema: "metabase", + table: "database"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "metabase", + table: "database"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + schema: "metabase", + table: "data_format"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "metabase", + table: "data_format"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + schema: "metabase", + table: "component_variant"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "metabase", + table: "component_variant"); + + migrationBuilder.DropColumn( + name: "xmin", + schema: "metabase", + table: "component_variant"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + schema: "metabase", + table: "component_manufacturer"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "metabase", + table: "component_manufacturer"); + + migrationBuilder.DropColumn( + name: "xmin", + schema: "metabase", + table: "component_manufacturer"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + schema: "metabase", + table: "component_concretization_and_generalization"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "metabase", + table: "component_concretization_and_generalization"); + + migrationBuilder.DropColumn( + name: "xmin", + schema: "metabase", + table: "component_concretization_and_generalization"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + schema: "metabase", + table: "component_assembly"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "metabase", + table: "component_assembly"); + + migrationBuilder.DropColumn( + name: "xmin", + schema: "metabase", + table: "component_assembly"); + + migrationBuilder.DropColumn( + name: "CreatedAt", + schema: "metabase", + table: "component"); + + migrationBuilder.DropColumn( + name: "UpdatedAt", + schema: "metabase", + table: "component"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "user", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "gen_random_uuid()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "OpenIddictTokens", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "gen_random_uuid()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "OpenIddictScopes", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "gen_random_uuid()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "OpenIddictAuthorizations", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "gen_random_uuid()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "OpenIddictApplications", + type: "uuid", + nullable: false, + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "gen_random_uuid()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + schema: "metabase", + table: "gnu_pg_fingerprint", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(Instant), + oldType: "timestamp with time zone", + oldDefaultValueSql: "now()"); + } + } +} diff --git a/backend/src/Migrations/20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations.Designer.cs b/backend/src/Migrations/20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations.Designer.cs new file mode 100644 index 000000000..52880f6a0 --- /dev/null +++ b/backend/src/Migrations/20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations.Designer.cs @@ -0,0 +1,2298 @@ +// +using System; +using System.Text.Json; +using Metabase.Data; +using Metabase.Enumerations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; + +#nullable disable + +namespace Metabase.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations")] + partial class AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("metabase") + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "component_category", new[] { "layer", "material", "unit" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "database_verification_state", new[] { "pending", "verified" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "institution_operating_state", new[] { "not_operating", "operating" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "institution_representative_role", new[] { "assistant", "owner" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "institution_state", new[] { "pending", "verified" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "method_category", new[] { "calculation", "measurement" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "prime_surface", new[] { "inside", "outside" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "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("Metabase.Data.Component", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Abbreviation") + .HasColumnType("text"); + + b.Property?>("Availability") + .HasColumnType("tstzrange"); + + b.PrimitiveCollection("Categories") + .IsRequired() + .HasColumnType("metabase.component_category[]"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extras") + .HasColumnType("jsonb"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("Name") + .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("ManagerId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("component", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentAssembly", b => + { + b.Property("AssembledComponentId") + .HasColumnType("uuid"); + + b.Property("PartComponentId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Index") + .HasColumnType("smallint"); + + b.Property("PrimeSurface") + .HasColumnType("metabase.prime_surface"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("AssembledComponentId", "PartComponentId"); + + b.HasIndex("PartComponentId"); + + b.ToTable("component_assembly", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentConcretizationAndGeneralization", b => + { + b.Property("GeneralComponentId") + .HasColumnType("uuid"); + + b.Property("ConcreteComponentId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("GeneralComponentId", "ConcreteComponentId"); + + b.HasIndex("ConcreteComponentId"); + + b.ToTable("component_concretization_and_generalization", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentManufacturer", b => + { + b.Property("ComponentId") + .HasColumnType("uuid"); + + b.Property("InstitutionId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("ComponentId", "InstitutionId"); + + b.HasIndex("InstitutionId"); + + b.ToTable("component_manufacturer", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentVariant", b => + { + b.Property("OfComponentId") + .HasColumnType("uuid"); + + b.Property("ToComponentId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("OfComponentId", "ToComponentId"); + + b.HasIndex("ToComponentId"); + + b.ToTable("component_variant", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.DataFormat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extension") + .HasColumnType("text"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("MediaType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SchemaLocator") + .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("ManagerId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("data_format", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Database", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Locator") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OperatorId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VerificationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerificationState") + .HasColumnType("metabase.database_verification_state"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("OperatorId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("database", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.GnuPgKeyFingerprint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AllowedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Fingerprint") + .IsRequired() + .HasColumnType("text"); + + b.Property("ForbiddenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("InstitutionId") + .HasColumnType("uuid"); + + 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.HasKey("Id"); + + b.HasIndex("Fingerprint") + .IsUnique(); + + b.HasIndex("InstitutionId"); + + b.HasIndex("UserId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.ToTable("gnu_pg_fingerprint", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Institution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Abbreviation") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extras") + .HasColumnType("jsonb"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OperatingState") + .HasColumnType("metabase.institution_operating_state"); + + b.Property("State") + .HasColumnType("metabase.institution_state"); + + 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("ManagerId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("institution", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionMethodDeveloper", b => + { + b.Property("InstitutionId") + .HasColumnType("uuid"); + + b.Property("MethodId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("InstitutionId", "MethodId"); + + b.HasIndex("MethodId"); + + b.ToTable("institution_method_developer", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionRepresentative", b => + { + b.Property("InstitutionId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.Property("Role") + .HasColumnType("metabase.institution_representative_role"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("InstitutionId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("institution_representative", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Method", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property?>("Availability") + .HasColumnType("tstzrange"); + + b.Property("CalculationLocator") + .HasColumnType("text"); + + b.PrimitiveCollection("Categories") + .IsRequired() + .HasColumnType("metabase.method_category[]"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property?>("Validity") + .HasColumnType("tstzrange"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("ManagerId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("method", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + 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("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("JsonWebKeySet") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + 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.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.HasIndex("OwnerId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.ToTable("OpenIddictApplications", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + 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.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.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + 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.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.ToTable("OpenIddictScopes", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AuthorizationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + 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.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("role", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.RoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("role_claim", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("PostalAddress") + .HasColumnType("text"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("WebsiteLocator") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("user", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("user_claim", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("user_login", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserMethodDeveloper", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("MethodId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("UserId", "MethodId"); + + b.HasIndex("MethodId"); + + b.ToTable("user_method_developer", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("user_role", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("user_token", "metabase"); + }); + + 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", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Component", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedComponents") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Metabase.Data.DescriptionOrReference", "PrimeDirection", b1 => + { + b1.Property("ComponentId") + .HasColumnType("uuid"); + + b1.Property("Description") + .HasColumnType("text"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("ComponentId"); + + b1.ToTable("component", "metabase"); + + b1.WithOwner() + .HasForeignKey("ComponentId"); + + b1.OwnsOne("Metabase.Data.Reference", "Reference", b2 => + { + b2.Property("DescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DescriptionOrReferenceComponentId"); + + b2.ToTable("component", "metabase"); + + b2.WithOwner() + .HasForeignKey("DescriptionOrReferenceComponentId"); + + b2.OwnsOne("Metabase.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + 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("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + }); + + b2.OwnsOne("Metabase.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.OwnsOne("Metabase.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDescriptionOrReferenceComponentId"); + + b4.ToTable("component", "metabase"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDescriptionOrReferenceComponentId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Reference"); + }); + + b.OwnsOne("Metabase.Data.DescriptionOrReference", "PrimeSurface", b1 => + { + b1.Property("ComponentId") + .HasColumnType("uuid"); + + b1.Property("Description") + .HasColumnType("text"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("ComponentId"); + + b1.ToTable("component", "metabase"); + + b1.WithOwner() + .HasForeignKey("ComponentId"); + + b1.OwnsOne("Metabase.Data.Reference", "Reference", b2 => + { + b2.Property("DescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DescriptionOrReferenceComponentId"); + + b2.ToTable("component", "metabase"); + + b2.WithOwner() + .HasForeignKey("DescriptionOrReferenceComponentId"); + + b2.OwnsOne("Metabase.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + 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("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + }); + + b2.OwnsOne("Metabase.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.OwnsOne("Metabase.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDescriptionOrReferenceComponentId"); + + b4.ToTable("component", "metabase"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDescriptionOrReferenceComponentId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Reference"); + }); + + b.OwnsOne("Metabase.Data.DescriptionOrReference", "SwitchableLayers", b1 => + { + b1.Property("ComponentId") + .HasColumnType("uuid"); + + b1.Property("Description") + .HasColumnType("text"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("ComponentId"); + + b1.ToTable("component", "metabase"); + + b1.WithOwner() + .HasForeignKey("ComponentId"); + + b1.OwnsOne("Metabase.Data.Reference", "Reference", b2 => + { + b2.Property("DescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DescriptionOrReferenceComponentId"); + + b2.ToTable("component", "metabase"); + + b2.WithOwner() + .HasForeignKey("DescriptionOrReferenceComponentId"); + + b2.OwnsOne("Metabase.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + 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("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + }); + + b2.OwnsOne("Metabase.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.OwnsOne("Metabase.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDescriptionOrReferenceComponentId"); + + b4.ToTable("component", "metabase"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDescriptionOrReferenceComponentId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Reference"); + }); + + b.Navigation("Manager"); + + b.Navigation("PrimeDirection"); + + b.Navigation("PrimeSurface"); + + b.Navigation("SwitchableLayers"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentAssembly", b => + { + b.HasOne("Metabase.Data.Component", "AssembledComponent") + .WithMany("PartEdges") + .HasForeignKey("AssembledComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Component", "PartComponent") + .WithMany("PartOfEdges") + .HasForeignKey("PartComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AssembledComponent"); + + b.Navigation("PartComponent"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentConcretizationAndGeneralization", b => + { + b.HasOne("Metabase.Data.Component", "ConcreteComponent") + .WithMany("GeneralizationEdges") + .HasForeignKey("ConcreteComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Component", "GeneralComponent") + .WithMany("ConcretizationEdges") + .HasForeignKey("GeneralComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ConcreteComponent"); + + b.Navigation("GeneralComponent"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentManufacturer", b => + { + b.HasOne("Metabase.Data.Component", "Component") + .WithMany("ManufacturerEdges") + .HasForeignKey("ComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("ManufacturedComponentEdges") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Component"); + + b.Navigation("Institution"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentVariant", b => + { + b.HasOne("Metabase.Data.Component", "OfComponent") + .WithMany("VariantEdges") + .HasForeignKey("OfComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Component", "ToComponent") + .WithMany("VariantOfEdges") + .HasForeignKey("ToComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OfComponent"); + + b.Navigation("ToComponent"); + }); + + modelBuilder.Entity("Metabase.Data.DataFormat", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedDataFormats") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Metabase.Data.Reference", "Reference", b1 => + { + b1.Property("DataFormatId") + .HasColumnType("uuid"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("DataFormatId"); + + b1.ToTable("data_format", "metabase"); + + b1.WithOwner() + .HasForeignKey("DataFormatId"); + + b1.OwnsOne("Metabase.Data.Publication", "Publication", b2 => + { + b2.Property("ReferenceDataFormatId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("ArXiv") + .HasColumnType("text"); + + b2.PrimitiveCollection("Authors") + .HasColumnType("text[]"); + + b2.Property("Doi") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Urn") + .HasColumnType("text"); + + b2.Property("WebAddress") + .HasColumnType("text"); + + b2.HasKey("ReferenceDataFormatId"); + + b2.ToTable("data_format", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceDataFormatId"); + }); + + b1.OwnsOne("Metabase.Data.Standard", "Standard", b2 => + { + b2.Property("ReferenceDataFormatId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Locator") + .HasColumnType("text"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Year") + .HasColumnType("integer"); + + b2.HasKey("ReferenceDataFormatId"); + + b2.ToTable("data_format", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceDataFormatId"); + + b2.OwnsOne("Metabase.Data.Numeration", "Numeration", b3 => + { + b3.Property("StandardReferenceDataFormatId") + .HasColumnType("uuid"); + + b3.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b3.Property("Prefix") + .HasColumnType("text"); + + b3.Property("Suffix") + .HasColumnType("text"); + + b3.HasKey("StandardReferenceDataFormatId"); + + b3.ToTable("data_format", "metabase"); + + b3.WithOwner() + .HasForeignKey("StandardReferenceDataFormatId"); + }); + + b2.Navigation("Numeration") + .IsRequired(); + }); + + b1.Navigation("Publication"); + + b1.Navigation("Standard"); + }); + + b.Navigation("Manager"); + + b.Navigation("Reference"); + }); + + modelBuilder.Entity("Metabase.Data.Database", b => + { + b.HasOne("Metabase.Data.Institution", "Operator") + .WithMany("OperatedDatabases") + .HasForeignKey("OperatorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Operator"); + }); + + modelBuilder.Entity("Metabase.Data.GnuPgKeyFingerprint", b => + { + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("GnuPgKeyFingerprints") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", "User") + .WithMany("GnuPgKeyFingerprints") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Institution"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Metabase.Data.Institution", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedInstitutions") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict); + + b.OwnsOne("Metabase.Data.ContactInformation", "Contact", b1 => + { + b1.Property("InstitutionId") + .HasColumnType("uuid"); + + b1.Property("EmailAddress") + .HasColumnType("text"); + + b1.Property("IsEmailAddressConfirmed") + .HasColumnType("boolean"); + + b1.Property("IsPhoneNumberConfirmed") + .HasColumnType("boolean"); + + b1.Property("PhoneNumber") + .HasColumnType("text"); + + b1.Property("PostalAddress") + .HasColumnType("text"); + + b1.Property("WebsiteLocator") + .HasColumnType("text"); + + b1.HasKey("InstitutionId"); + + b1.ToTable("institution", "metabase"); + + b1.WithOwner() + .HasForeignKey("InstitutionId"); + }); + + b.Navigation("Contact"); + + b.Navigation("Manager"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionMethodDeveloper", b => + { + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("DevelopedMethodEdges") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Method", "Method") + .WithMany("InstitutionDeveloperEdges") + .HasForeignKey("MethodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Institution"); + + b.Navigation("Method"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionRepresentative", b => + { + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("RepresentativeEdges") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", "User") + .WithMany("RepresentedInstitutionEdges") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Institution"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Metabase.Data.Method", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedMethods") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Metabase.Data.Reference", "Reference", b1 => + { + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("MethodId"); + + b1.ToTable("method", "metabase"); + + b1.WithOwner() + .HasForeignKey("MethodId"); + + b1.OwnsOne("Metabase.Data.Publication", "Publication", b2 => + { + b2.Property("ReferenceMethodId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("ArXiv") + .HasColumnType("text"); + + b2.PrimitiveCollection("Authors") + .HasColumnType("text[]"); + + b2.Property("Doi") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Urn") + .HasColumnType("text"); + + b2.Property("WebAddress") + .HasColumnType("text"); + + b2.HasKey("ReferenceMethodId"); + + b2.ToTable("method", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceMethodId"); + }); + + b1.OwnsOne("Metabase.Data.Standard", "Standard", b2 => + { + b2.Property("ReferenceMethodId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Locator") + .HasColumnType("text"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Year") + .HasColumnType("integer"); + + b2.HasKey("ReferenceMethodId"); + + b2.ToTable("method", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceMethodId"); + + b2.OwnsOne("Metabase.Data.Numeration", "Numeration", b3 => + { + b3.Property("StandardReferenceMethodId") + .HasColumnType("uuid"); + + b3.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b3.Property("Prefix") + .HasColumnType("text"); + + b3.Property("Suffix") + .HasColumnType("text"); + + b3.HasKey("StandardReferenceMethodId"); + + b3.ToTable("method", "metabase"); + + b3.WithOwner() + .HasForeignKey("StandardReferenceMethodId"); + }); + + b2.Navigation("Numeration") + .IsRequired(); + }); + + b1.Navigation("Publication"); + + b1.Navigation("Standard"); + }); + + b.OwnsMany("Metabase.Data.MethodParameter", "Parameters", b1 => + { + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Type") + .HasColumnType("jsonb"); + + b1.HasKey("MethodId", "Id"); + + b1.ToTable("MethodParameter", "metabase"); + + b1.WithOwner() + .HasForeignKey("MethodId"); + }); + + b.OwnsMany("Metabase.Data.MethodSource", "Sources", b1 => + { + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("MethodId", "Id"); + + b1.ToTable("MethodSource", "metabase"); + + b1.WithOwner() + .HasForeignKey("MethodId"); + }); + + b.Navigation("Manager"); + + b.Navigation("Parameters"); + + b.Navigation("Reference"); + + b.Navigation("Sources"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", b => + { + b.HasOne("Metabase.Data.Institution", "Owner") + .WithMany("OpenIdConnectApplications") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", b => + { + b.HasOne("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectToken", b => + { + b.HasOne("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("Metabase.Data.RoleClaim", b => + { + b.HasOne("Metabase.Data.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserClaim", b => + { + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserLogin", b => + { + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserMethodDeveloper", b => + { + b.HasOne("Metabase.Data.Method", "Method") + .WithMany("UserDeveloperEdges") + .HasForeignKey("MethodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", "User") + .WithMany("DevelopedMethodEdges") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Method"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Metabase.Data.UserRole", b => + { + b.HasOne("Metabase.Data.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserToken", b => + { + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.Component", b => + { + b.Navigation("ConcretizationEdges"); + + b.Navigation("GeneralizationEdges"); + + b.Navigation("ManufacturerEdges"); + + b.Navigation("PartEdges"); + + b.Navigation("PartOfEdges"); + + b.Navigation("VariantEdges"); + + b.Navigation("VariantOfEdges"); + }); + + modelBuilder.Entity("Metabase.Data.Institution", b => + { + b.Navigation("DevelopedMethodEdges"); + + b.Navigation("GnuPgKeyFingerprints"); + + b.Navigation("ManagedComponents"); + + b.Navigation("ManagedDataFormats"); + + b.Navigation("ManagedInstitutions"); + + b.Navigation("ManagedMethods"); + + b.Navigation("ManufacturedComponentEdges"); + + b.Navigation("OpenIdConnectApplications"); + + b.Navigation("OperatedDatabases"); + + b.Navigation("RepresentativeEdges"); + }); + + modelBuilder.Entity("Metabase.Data.Method", b => + { + b.Navigation("InstitutionDeveloperEdges"); + + b.Navigation("UserDeveloperEdges"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", b => + { + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Metabase.Data.User", b => + { + b.Navigation("DevelopedMethodEdges"); + + b.Navigation("GnuPgKeyFingerprints"); + + b.Navigation("RepresentedInstitutionEdges"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Migrations/20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations.cs b/backend/src/Migrations/20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations.cs new file mode 100644 index 000000000..52a80f0ea --- /dev/null +++ b/backend/src/Migrations/20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations.cs @@ -0,0 +1,266 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Metabase.Migrations +{ + /// + public partial class AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations : Migration + { + /// + [SuppressMessage("Performance", "CA1861")] + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "UpdatedAt", + schema: "metabase", + table: "OpenIddictScopes", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()", + oldClrType: typeof(Instant), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + schema: "metabase", + table: "OpenIddictScopes", + type: "timestamp with time zone", + nullable: false, + defaultValueSql: "now()", + oldClrType: typeof(Instant), + oldType: "timestamp with time zone"); + + migrationBuilder.CreateIndex( + name: "IX_user_CreatedAt_Id", + schema: "metabase", + table: "user", + columns: new[] { "CreatedAt", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_user_Name_Id", + schema: "metabase", + table: "user", + columns: new[] { "Name", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictTokens_CreatedAt_Id", + schema: "metabase", + table: "OpenIddictTokens", + columns: new[] { "CreatedAt", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictScopes_CreatedAt_Id", + schema: "metabase", + table: "OpenIddictScopes", + columns: new[] { "CreatedAt", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictAuthorizations_CreatedAt_Id", + schema: "metabase", + table: "OpenIddictAuthorizations", + columns: new[] { "CreatedAt", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OpenIddictApplications_CreatedAt_Id", + schema: "metabase", + table: "OpenIddictApplications", + columns: new[] { "CreatedAt", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_method_CreatedAt_Id", + schema: "metabase", + table: "method", + columns: new[] { "CreatedAt", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_method_Name_Id", + schema: "metabase", + table: "method", + columns: new[] { "Name", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_institution_CreatedAt_Id", + schema: "metabase", + table: "institution", + columns: new[] { "CreatedAt", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_institution_Name_Id", + schema: "metabase", + table: "institution", + columns: new[] { "Name", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_gnu_pg_fingerprint_CreatedAt_Id", + schema: "metabase", + table: "gnu_pg_fingerprint", + columns: new[] { "CreatedAt", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_database_CreatedAt_Id", + schema: "metabase", + table: "database", + columns: new[] { "CreatedAt", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_database_Name_Id", + schema: "metabase", + table: "database", + columns: new[] { "Name", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_data_format_CreatedAt_Id", + schema: "metabase", + table: "data_format", + columns: new[] { "CreatedAt", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_data_format_Name_Id", + schema: "metabase", + table: "data_format", + columns: new[] { "Name", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_component_CreatedAt_Id", + schema: "metabase", + table: "component", + columns: new[] { "CreatedAt", "Id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_component_Name_Id", + schema: "metabase", + table: "component", + columns: new[] { "Name", "Id" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_user_CreatedAt_Id", + schema: "metabase", + table: "user"); + + migrationBuilder.DropIndex( + name: "IX_user_Name_Id", + schema: "metabase", + table: "user"); + + migrationBuilder.DropIndex( + name: "IX_OpenIddictTokens_CreatedAt_Id", + schema: "metabase", + table: "OpenIddictTokens"); + + migrationBuilder.DropIndex( + name: "IX_OpenIddictScopes_CreatedAt_Id", + schema: "metabase", + table: "OpenIddictScopes"); + + migrationBuilder.DropIndex( + name: "IX_OpenIddictAuthorizations_CreatedAt_Id", + schema: "metabase", + table: "OpenIddictAuthorizations"); + + migrationBuilder.DropIndex( + name: "IX_OpenIddictApplications_CreatedAt_Id", + schema: "metabase", + table: "OpenIddictApplications"); + + migrationBuilder.DropIndex( + name: "IX_method_CreatedAt_Id", + schema: "metabase", + table: "method"); + + migrationBuilder.DropIndex( + name: "IX_method_Name_Id", + schema: "metabase", + table: "method"); + + migrationBuilder.DropIndex( + name: "IX_institution_CreatedAt_Id", + schema: "metabase", + table: "institution"); + + migrationBuilder.DropIndex( + name: "IX_institution_Name_Id", + schema: "metabase", + table: "institution"); + + migrationBuilder.DropIndex( + name: "IX_gnu_pg_fingerprint_CreatedAt_Id", + schema: "metabase", + table: "gnu_pg_fingerprint"); + + migrationBuilder.DropIndex( + name: "IX_database_CreatedAt_Id", + schema: "metabase", + table: "database"); + + migrationBuilder.DropIndex( + name: "IX_database_Name_Id", + schema: "metabase", + table: "database"); + + migrationBuilder.DropIndex( + name: "IX_data_format_CreatedAt_Id", + schema: "metabase", + table: "data_format"); + + migrationBuilder.DropIndex( + name: "IX_data_format_Name_Id", + schema: "metabase", + table: "data_format"); + + migrationBuilder.DropIndex( + name: "IX_component_CreatedAt_Id", + schema: "metabase", + table: "component"); + + migrationBuilder.DropIndex( + name: "IX_component_Name_Id", + schema: "metabase", + table: "component"); + + migrationBuilder.AlterColumn( + name: "UpdatedAt", + schema: "metabase", + table: "OpenIddictScopes", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTimeOffset), + oldType: "timestamp with time zone", + oldDefaultValueSql: "now()"); + + migrationBuilder.AlterColumn( + name: "CreatedAt", + schema: "metabase", + table: "OpenIddictScopes", + type: "timestamp with time zone", + nullable: false, + oldClrType: typeof(DateTimeOffset), + oldType: "timestamp with time zone", + oldDefaultValueSql: "now()"); + } + } +} diff --git a/backend/src/Migrations/20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication.Designer.cs b/backend/src/Migrations/20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication.Designer.cs new file mode 100644 index 000000000..35ff8fa96 --- /dev/null +++ b/backend/src/Migrations/20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication.Designer.cs @@ -0,0 +1,2298 @@ +// +using System; +using System.Text.Json; +using Metabase.Data; +using Metabase.Enumerations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; + +#nullable disable + +namespace Metabase.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication")] + partial class AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("metabase") + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "component_category", new[] { "layer", "material", "unit" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "database_verification_state", new[] { "pending", "verified" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "institution_operating_state", new[] { "not_operating", "operating" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "institution_representative_role", new[] { "assistant", "owner" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "institution_state", new[] { "pending", "verified" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "method_category", new[] { "calculation", "measurement" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "prime_surface", new[] { "inside", "outside" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "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("Metabase.Data.Component", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Abbreviation") + .HasColumnType("text"); + + b.Property?>("Availability") + .HasColumnType("tstzrange"); + + b.PrimitiveCollection("Categories") + .IsRequired() + .HasColumnType("metabase.component_category[]"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extras") + .HasColumnType("jsonb"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("Name") + .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("ManagerId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("component", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentAssembly", b => + { + b.Property("AssembledComponentId") + .HasColumnType("uuid"); + + b.Property("PartComponentId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Index") + .HasColumnType("smallint"); + + b.Property("PrimeSurface") + .HasColumnType("metabase.prime_surface"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("AssembledComponentId", "PartComponentId"); + + b.HasIndex("PartComponentId"); + + b.ToTable("component_assembly", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentConcretizationAndGeneralization", b => + { + b.Property("GeneralComponentId") + .HasColumnType("uuid"); + + b.Property("ConcreteComponentId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("GeneralComponentId", "ConcreteComponentId"); + + b.HasIndex("ConcreteComponentId"); + + b.ToTable("component_concretization_and_generalization", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentManufacturer", b => + { + b.Property("ComponentId") + .HasColumnType("uuid"); + + b.Property("InstitutionId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("ComponentId", "InstitutionId"); + + b.HasIndex("InstitutionId"); + + b.ToTable("component_manufacturer", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentVariant", b => + { + b.Property("OfComponentId") + .HasColumnType("uuid"); + + b.Property("ToComponentId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("OfComponentId", "ToComponentId"); + + b.HasIndex("ToComponentId"); + + b.ToTable("component_variant", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.DataFormat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extension") + .HasColumnType("text"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("MediaType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SchemaLocator") + .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("ManagerId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("data_format", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Database", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Locator") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OperatorId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VerificationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerificationState") + .HasColumnType("metabase.database_verification_state"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("OperatorId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("database", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.GnuPgKeyFingerprint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AllowedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Fingerprint") + .IsRequired() + .HasColumnType("text"); + + b.Property("ForbiddenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("InstitutionId") + .HasColumnType("uuid"); + + 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.HasKey("Id"); + + b.HasIndex("Fingerprint") + .IsUnique(); + + b.HasIndex("InstitutionId"); + + b.HasIndex("UserId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.ToTable("gnu_pg_fingerprint", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Institution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Abbreviation") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extras") + .HasColumnType("jsonb"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OperatingState") + .HasColumnType("metabase.institution_operating_state"); + + b.Property("State") + .HasColumnType("metabase.institution_state"); + + 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("ManagerId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("institution", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionMethodDeveloper", b => + { + b.Property("InstitutionId") + .HasColumnType("uuid"); + + b.Property("MethodId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("InstitutionId", "MethodId"); + + b.HasIndex("MethodId"); + + b.ToTable("institution_method_developer", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionRepresentative", b => + { + b.Property("InstitutionId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.Property("Role") + .HasColumnType("metabase.institution_representative_role"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("InstitutionId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("institution_representative", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Method", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property?>("Availability") + .HasColumnType("tstzrange"); + + b.Property("CalculationLocator") + .HasColumnType("text"); + + b.PrimitiveCollection("Categories") + .IsRequired() + .HasColumnType("metabase.method_category[]"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property?>("Validity") + .HasColumnType("tstzrange"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("ManagerId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("method", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + 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("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("JsonWebKeySet") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + 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.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.HasIndex("OwnerId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.ToTable("OpenIddictApplications", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + 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.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.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + 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.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.ToTable("OpenIddictScopes", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AuthorizationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + 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.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("role", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.RoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("role_claim", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("PostalAddress") + .HasColumnType("text"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("WebsiteLocator") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("user", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("user_claim", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("user_login", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserMethodDeveloper", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("MethodId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("UserId", "MethodId"); + + b.HasIndex("MethodId"); + + b.ToTable("user_method_developer", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("user_role", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("user_token", "metabase"); + }); + + 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", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Component", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedComponents") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Metabase.Data.DescriptionOrReference", "PrimeDirection", b1 => + { + b1.Property("ComponentId") + .HasColumnType("uuid"); + + b1.Property("Description") + .HasColumnType("text"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("ComponentId"); + + b1.ToTable("component", "metabase"); + + b1.WithOwner() + .HasForeignKey("ComponentId"); + + b1.OwnsOne("Metabase.Data.Reference", "Reference", b2 => + { + b2.Property("DescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DescriptionOrReferenceComponentId"); + + b2.ToTable("component", "metabase"); + + b2.WithOwner() + .HasForeignKey("DescriptionOrReferenceComponentId"); + + b2.OwnsOne("Metabase.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + 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("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + }); + + b2.OwnsOne("Metabase.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.OwnsOne("Metabase.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDescriptionOrReferenceComponentId"); + + b4.ToTable("component", "metabase"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDescriptionOrReferenceComponentId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Reference"); + }); + + b.OwnsOne("Metabase.Data.DescriptionOrReference", "PrimeSurface", b1 => + { + b1.Property("ComponentId") + .HasColumnType("uuid"); + + b1.Property("Description") + .HasColumnType("text"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("ComponentId"); + + b1.ToTable("component", "metabase"); + + b1.WithOwner() + .HasForeignKey("ComponentId"); + + b1.OwnsOne("Metabase.Data.Reference", "Reference", b2 => + { + b2.Property("DescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DescriptionOrReferenceComponentId"); + + b2.ToTable("component", "metabase"); + + b2.WithOwner() + .HasForeignKey("DescriptionOrReferenceComponentId"); + + b2.OwnsOne("Metabase.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + 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("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + }); + + b2.OwnsOne("Metabase.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.OwnsOne("Metabase.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDescriptionOrReferenceComponentId"); + + b4.ToTable("component", "metabase"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDescriptionOrReferenceComponentId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Reference"); + }); + + b.OwnsOne("Metabase.Data.DescriptionOrReference", "SwitchableLayers", b1 => + { + b1.Property("ComponentId") + .HasColumnType("uuid"); + + b1.Property("Description") + .HasColumnType("text"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("ComponentId"); + + b1.ToTable("component", "metabase"); + + b1.WithOwner() + .HasForeignKey("ComponentId"); + + b1.OwnsOne("Metabase.Data.Reference", "Reference", b2 => + { + b2.Property("DescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DescriptionOrReferenceComponentId"); + + b2.ToTable("component", "metabase"); + + b2.WithOwner() + .HasForeignKey("DescriptionOrReferenceComponentId"); + + b2.OwnsOne("Metabase.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + 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("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + }); + + b2.OwnsOne("Metabase.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.OwnsOne("Metabase.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDescriptionOrReferenceComponentId"); + + b4.ToTable("component", "metabase"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDescriptionOrReferenceComponentId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Reference"); + }); + + b.Navigation("Manager"); + + b.Navigation("PrimeDirection"); + + b.Navigation("PrimeSurface"); + + b.Navigation("SwitchableLayers"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentAssembly", b => + { + b.HasOne("Metabase.Data.Component", "AssembledComponent") + .WithMany("PartEdges") + .HasForeignKey("AssembledComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Component", "PartComponent") + .WithMany("PartOfEdges") + .HasForeignKey("PartComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AssembledComponent"); + + b.Navigation("PartComponent"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentConcretizationAndGeneralization", b => + { + b.HasOne("Metabase.Data.Component", "ConcreteComponent") + .WithMany("GeneralizationEdges") + .HasForeignKey("ConcreteComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Component", "GeneralComponent") + .WithMany("ConcretizationEdges") + .HasForeignKey("GeneralComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ConcreteComponent"); + + b.Navigation("GeneralComponent"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentManufacturer", b => + { + b.HasOne("Metabase.Data.Component", "Component") + .WithMany("ManufacturerEdges") + .HasForeignKey("ComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("ManufacturedComponentEdges") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Component"); + + b.Navigation("Institution"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentVariant", b => + { + b.HasOne("Metabase.Data.Component", "OfComponent") + .WithMany("VariantEdges") + .HasForeignKey("OfComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Component", "ToComponent") + .WithMany("VariantOfEdges") + .HasForeignKey("ToComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OfComponent"); + + b.Navigation("ToComponent"); + }); + + modelBuilder.Entity("Metabase.Data.DataFormat", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedDataFormats") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Metabase.Data.Reference", "Reference", b1 => + { + b1.Property("DataFormatId") + .HasColumnType("uuid"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("DataFormatId"); + + b1.ToTable("data_format", "metabase"); + + b1.WithOwner() + .HasForeignKey("DataFormatId"); + + b1.OwnsOne("Metabase.Data.Publication", "Publication", b2 => + { + b2.Property("ReferenceDataFormatId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("ArXiv") + .HasColumnType("text"); + + b2.PrimitiveCollection("Authors") + .HasColumnType("text[]"); + + b2.Property("Doi") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Urn") + .HasColumnType("text"); + + b2.Property("WebAddress") + .HasColumnType("text"); + + b2.HasKey("ReferenceDataFormatId"); + + b2.ToTable("data_format", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceDataFormatId"); + }); + + b1.OwnsOne("Metabase.Data.Standard", "Standard", b2 => + { + b2.Property("ReferenceDataFormatId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Locator") + .HasColumnType("text"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Year") + .HasColumnType("integer"); + + b2.HasKey("ReferenceDataFormatId"); + + b2.ToTable("data_format", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceDataFormatId"); + + b2.OwnsOne("Metabase.Data.Numeration", "Numeration", b3 => + { + b3.Property("StandardReferenceDataFormatId") + .HasColumnType("uuid"); + + b3.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b3.Property("Prefix") + .HasColumnType("text"); + + b3.Property("Suffix") + .HasColumnType("text"); + + b3.HasKey("StandardReferenceDataFormatId"); + + b3.ToTable("data_format", "metabase"); + + b3.WithOwner() + .HasForeignKey("StandardReferenceDataFormatId"); + }); + + b2.Navigation("Numeration") + .IsRequired(); + }); + + b1.Navigation("Publication"); + + b1.Navigation("Standard"); + }); + + b.Navigation("Manager"); + + b.Navigation("Reference"); + }); + + modelBuilder.Entity("Metabase.Data.Database", b => + { + b.HasOne("Metabase.Data.Institution", "Operator") + .WithMany("OperatedDatabases") + .HasForeignKey("OperatorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Operator"); + }); + + modelBuilder.Entity("Metabase.Data.GnuPgKeyFingerprint", b => + { + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("GnuPgKeyFingerprints") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", "User") + .WithMany("GnuPgKeyFingerprints") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Institution"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Metabase.Data.Institution", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedInstitutions") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict); + + b.OwnsOne("Metabase.Data.ContactInformation", "Contact", b1 => + { + b1.Property("InstitutionId") + .HasColumnType("uuid"); + + b1.Property("EmailAddress") + .HasColumnType("text"); + + b1.Property("IsEmailAddressConfirmed") + .HasColumnType("boolean"); + + b1.Property("IsPhoneNumberConfirmed") + .HasColumnType("boolean"); + + b1.Property("PhoneNumber") + .HasColumnType("text"); + + b1.Property("PostalAddress") + .HasColumnType("text"); + + b1.Property("WebsiteLocator") + .HasColumnType("text"); + + b1.HasKey("InstitutionId"); + + b1.ToTable("institution", "metabase"); + + b1.WithOwner() + .HasForeignKey("InstitutionId"); + }); + + b.Navigation("Contact"); + + b.Navigation("Manager"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionMethodDeveloper", b => + { + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("DevelopedMethodEdges") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Method", "Method") + .WithMany("InstitutionDeveloperEdges") + .HasForeignKey("MethodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Institution"); + + b.Navigation("Method"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionRepresentative", b => + { + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("RepresentativeEdges") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", "User") + .WithMany("RepresentedInstitutionEdges") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Institution"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Metabase.Data.Method", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedMethods") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Metabase.Data.Reference", "Reference", b1 => + { + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("MethodId"); + + b1.ToTable("method", "metabase"); + + b1.WithOwner() + .HasForeignKey("MethodId"); + + b1.OwnsOne("Metabase.Data.Publication", "Publication", b2 => + { + b2.Property("ReferenceMethodId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("ArXiv") + .HasColumnType("text"); + + b2.PrimitiveCollection("Authors") + .HasColumnType("text[]"); + + b2.Property("Doi") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Urn") + .HasColumnType("text"); + + b2.Property("WebAddress") + .HasColumnType("text"); + + b2.HasKey("ReferenceMethodId"); + + b2.ToTable("method", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceMethodId"); + }); + + b1.OwnsOne("Metabase.Data.Standard", "Standard", b2 => + { + b2.Property("ReferenceMethodId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Locator") + .HasColumnType("text"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Year") + .HasColumnType("integer"); + + b2.HasKey("ReferenceMethodId"); + + b2.ToTable("method", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceMethodId"); + + b2.OwnsOne("Metabase.Data.Numeration", "Numeration", b3 => + { + b3.Property("StandardReferenceMethodId") + .HasColumnType("uuid"); + + b3.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b3.Property("Prefix") + .HasColumnType("text"); + + b3.Property("Suffix") + .HasColumnType("text"); + + b3.HasKey("StandardReferenceMethodId"); + + b3.ToTable("method", "metabase"); + + b3.WithOwner() + .HasForeignKey("StandardReferenceMethodId"); + }); + + b2.Navigation("Numeration") + .IsRequired(); + }); + + b1.Navigation("Publication"); + + b1.Navigation("Standard"); + }); + + b.OwnsMany("Metabase.Data.MethodParameter", "Parameters", b1 => + { + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Type") + .HasColumnType("jsonb"); + + b1.HasKey("MethodId", "Id"); + + b1.ToTable("MethodParameter", "metabase"); + + b1.WithOwner() + .HasForeignKey("MethodId"); + }); + + b.OwnsMany("Metabase.Data.MethodSource", "Sources", b1 => + { + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("MethodId", "Id"); + + b1.ToTable("MethodSource", "metabase"); + + b1.WithOwner() + .HasForeignKey("MethodId"); + }); + + b.Navigation("Manager"); + + b.Navigation("Parameters"); + + b.Navigation("Reference"); + + b.Navigation("Sources"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", b => + { + b.HasOne("Metabase.Data.Institution", "Owner") + .WithMany("OpenIdConnectApplications") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", b => + { + b.HasOne("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectToken", b => + { + b.HasOne("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("Metabase.Data.RoleClaim", b => + { + b.HasOne("Metabase.Data.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserClaim", b => + { + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserLogin", b => + { + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserMethodDeveloper", b => + { + b.HasOne("Metabase.Data.Method", "Method") + .WithMany("UserDeveloperEdges") + .HasForeignKey("MethodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", "User") + .WithMany("DevelopedMethodEdges") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Method"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Metabase.Data.UserRole", b => + { + b.HasOne("Metabase.Data.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserToken", b => + { + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.Component", b => + { + b.Navigation("ConcretizationEdges"); + + b.Navigation("GeneralizationEdges"); + + b.Navigation("ManufacturerEdges"); + + b.Navigation("PartEdges"); + + b.Navigation("PartOfEdges"); + + b.Navigation("VariantEdges"); + + b.Navigation("VariantOfEdges"); + }); + + modelBuilder.Entity("Metabase.Data.Institution", b => + { + b.Navigation("DevelopedMethodEdges"); + + b.Navigation("GnuPgKeyFingerprints"); + + b.Navigation("ManagedComponents"); + + b.Navigation("ManagedDataFormats"); + + b.Navigation("ManagedInstitutions"); + + b.Navigation("ManagedMethods"); + + b.Navigation("ManufacturedComponentEdges"); + + b.Navigation("OpenIdConnectApplications"); + + b.Navigation("OperatedDatabases"); + + b.Navigation("RepresentativeEdges"); + }); + + modelBuilder.Entity("Metabase.Data.Method", b => + { + b.Navigation("InstitutionDeveloperEdges"); + + b.Navigation("UserDeveloperEdges"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", b => + { + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Metabase.Data.User", b => + { + b.Navigation("DevelopedMethodEdges"); + + b.Navigation("GnuPgKeyFingerprints"); + + b.Navigation("RepresentedInstitutionEdges"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Migrations/20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication.cs b/backend/src/Migrations/20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication.cs new file mode 100644 index 000000000..f73d3538b --- /dev/null +++ b/backend/src/Migrations/20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Metabase.Migrations +{ + /// + public partial class AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + UPDATE metabase."OpenIddictApplications" + SET "Permissions" = ("Permissions"::jsonb || '["scp:api:support"]'::jsonb)::text + WHERE "ClientId" = 'metabase'; + """ + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + UPDATE metabase."OpenIddictApplications" + SET "Permissions" = ("Permissions"::jsonb - 'scp:api:support')::text + WHERE "ClientId" = 'metabase'; + """ + ); + } + } +} \ No newline at end of file diff --git a/backend/src/Migrations/20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit.Designer.cs b/backend/src/Migrations/20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit.Designer.cs new file mode 100644 index 000000000..2df6e91d6 --- /dev/null +++ b/backend/src/Migrations/20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit.Designer.cs @@ -0,0 +1,2298 @@ +// +using System; +using System.Text.Json; +using Metabase.Data; +using Metabase.Enumerations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; + +#nullable disable + +namespace Metabase.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit")] + partial class SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("metabase") + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "component_category", new[] { "layer", "material", "unit" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "database_verification_state", new[] { "pending", "verified" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "institution_operating_state", new[] { "not_operating", "operating" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "institution_representative_role", new[] { "assistant", "owner" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "institution_state", new[] { "pending", "verified" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "method_category", new[] { "calculation", "measurement" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "prime_surface", new[] { "inside", "outside" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "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("Metabase.Data.Component", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Abbreviation") + .HasColumnType("text"); + + b.Property?>("Availability") + .HasColumnType("tstzrange"); + + b.PrimitiveCollection("Categories") + .IsRequired() + .HasColumnType("metabase.component_category[]"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extras") + .HasColumnType("jsonb"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("Name") + .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("ManagerId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("component", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentAssembly", b => + { + b.Property("AssembledComponentId") + .HasColumnType("uuid"); + + b.Property("PartComponentId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Index") + .HasColumnType("smallint"); + + b.Property("PrimeSurface") + .HasColumnType("metabase.prime_surface"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("AssembledComponentId", "PartComponentId"); + + b.HasIndex("PartComponentId"); + + b.ToTable("component_assembly", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentConcretizationAndGeneralization", b => + { + b.Property("GeneralComponentId") + .HasColumnType("uuid"); + + b.Property("ConcreteComponentId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("GeneralComponentId", "ConcreteComponentId"); + + b.HasIndex("ConcreteComponentId"); + + b.ToTable("component_concretization_and_generalization", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentManufacturer", b => + { + b.Property("ComponentId") + .HasColumnType("uuid"); + + b.Property("InstitutionId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("ComponentId", "InstitutionId"); + + b.HasIndex("InstitutionId"); + + b.ToTable("component_manufacturer", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentVariant", b => + { + b.Property("OfComponentId") + .HasColumnType("uuid"); + + b.Property("ToComponentId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("OfComponentId", "ToComponentId"); + + b.HasIndex("ToComponentId"); + + b.ToTable("component_variant", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.DataFormat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extension") + .HasColumnType("text"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("MediaType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SchemaLocator") + .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("ManagerId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("data_format", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Database", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Locator") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OperatorId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VerificationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerificationState") + .HasColumnType("metabase.database_verification_state"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("OperatorId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("database", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.GnuPgKeyFingerprint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AllowedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Fingerprint") + .IsRequired() + .HasColumnType("text"); + + b.Property("ForbiddenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("InstitutionId") + .HasColumnType("uuid"); + + 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.HasKey("Id"); + + b.HasIndex("Fingerprint") + .IsUnique(); + + b.HasIndex("InstitutionId"); + + b.HasIndex("UserId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.ToTable("gnu_pg_fingerprint", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Institution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("Abbreviation") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extras") + .HasColumnType("jsonb"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OperatingState") + .HasColumnType("metabase.institution_operating_state"); + + b.Property("State") + .HasColumnType("metabase.institution_state"); + + 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("ManagerId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("institution", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionMethodDeveloper", b => + { + b.Property("InstitutionId") + .HasColumnType("uuid"); + + b.Property("MethodId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("InstitutionId", "MethodId"); + + b.HasIndex("MethodId"); + + b.ToTable("institution_method_developer", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionRepresentative", b => + { + b.Property("InstitutionId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.Property("Role") + .HasColumnType("metabase.institution_representative_role"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("InstitutionId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("institution_representative", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Method", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property?>("Availability") + .HasColumnType("tstzrange"); + + b.Property("CalculationLocator") + .HasColumnType("text"); + + b.PrimitiveCollection("Categories") + .IsRequired() + .HasColumnType("metabase.method_category[]"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property?>("Validity") + .HasColumnType("tstzrange"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("ManagerId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("method", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + 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("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("JsonWebKeySet") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + 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.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.HasIndex("OwnerId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.ToTable("OpenIddictApplications", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + 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.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.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + 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.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.ToTable("OpenIddictScopes", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AuthorizationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + 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.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("role", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.RoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("role_claim", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("PostalAddress") + .HasColumnType("text"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("WebsiteLocator") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("user", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("user_claim", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("user_login", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserMethodDeveloper", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("MethodId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("UserId", "MethodId"); + + b.HasIndex("MethodId"); + + b.ToTable("user_method_developer", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("user_role", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("user_token", "metabase"); + }); + + 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", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Component", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedComponents") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Metabase.Data.DescriptionOrReference", "PrimeDirection", b1 => + { + b1.Property("ComponentId") + .HasColumnType("uuid"); + + b1.Property("Description") + .HasColumnType("text"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("ComponentId"); + + b1.ToTable("component", "metabase"); + + b1.WithOwner() + .HasForeignKey("ComponentId"); + + b1.OwnsOne("Metabase.Data.Reference", "Reference", b2 => + { + b2.Property("DescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DescriptionOrReferenceComponentId"); + + b2.ToTable("component", "metabase"); + + b2.WithOwner() + .HasForeignKey("DescriptionOrReferenceComponentId"); + + b2.OwnsOne("Metabase.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + 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("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + }); + + b2.OwnsOne("Metabase.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.OwnsOne("Metabase.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDescriptionOrReferenceComponentId"); + + b4.ToTable("component", "metabase"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDescriptionOrReferenceComponentId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Reference"); + }); + + b.OwnsOne("Metabase.Data.DescriptionOrReference", "PrimeSurface", b1 => + { + b1.Property("ComponentId") + .HasColumnType("uuid"); + + b1.Property("Description") + .HasColumnType("text"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("ComponentId"); + + b1.ToTable("component", "metabase"); + + b1.WithOwner() + .HasForeignKey("ComponentId"); + + b1.OwnsOne("Metabase.Data.Reference", "Reference", b2 => + { + b2.Property("DescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DescriptionOrReferenceComponentId"); + + b2.ToTable("component", "metabase"); + + b2.WithOwner() + .HasForeignKey("DescriptionOrReferenceComponentId"); + + b2.OwnsOne("Metabase.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + 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("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + }); + + b2.OwnsOne("Metabase.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.OwnsOne("Metabase.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDescriptionOrReferenceComponentId"); + + b4.ToTable("component", "metabase"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDescriptionOrReferenceComponentId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Reference"); + }); + + b.OwnsOne("Metabase.Data.DescriptionOrReference", "SwitchableLayers", b1 => + { + b1.Property("ComponentId") + .HasColumnType("uuid"); + + b1.Property("Description") + .HasColumnType("text"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("ComponentId"); + + b1.ToTable("component", "metabase"); + + b1.WithOwner() + .HasForeignKey("ComponentId"); + + b1.OwnsOne("Metabase.Data.Reference", "Reference", b2 => + { + b2.Property("DescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DescriptionOrReferenceComponentId"); + + b2.ToTable("component", "metabase"); + + b2.WithOwner() + .HasForeignKey("DescriptionOrReferenceComponentId"); + + b2.OwnsOne("Metabase.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + 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("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + }); + + b2.OwnsOne("Metabase.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.OwnsOne("Metabase.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDescriptionOrReferenceComponentId"); + + b4.ToTable("component", "metabase"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDescriptionOrReferenceComponentId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Reference"); + }); + + b.Navigation("Manager"); + + b.Navigation("PrimeDirection"); + + b.Navigation("PrimeSurface"); + + b.Navigation("SwitchableLayers"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentAssembly", b => + { + b.HasOne("Metabase.Data.Component", "AssembledComponent") + .WithMany("PartEdges") + .HasForeignKey("AssembledComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Component", "PartComponent") + .WithMany("PartOfEdges") + .HasForeignKey("PartComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AssembledComponent"); + + b.Navigation("PartComponent"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentConcretizationAndGeneralization", b => + { + b.HasOne("Metabase.Data.Component", "ConcreteComponent") + .WithMany("GeneralizationEdges") + .HasForeignKey("ConcreteComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Component", "GeneralComponent") + .WithMany("ConcretizationEdges") + .HasForeignKey("GeneralComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ConcreteComponent"); + + b.Navigation("GeneralComponent"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentManufacturer", b => + { + b.HasOne("Metabase.Data.Component", "Component") + .WithMany("ManufacturerEdges") + .HasForeignKey("ComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("ManufacturedComponentEdges") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Component"); + + b.Navigation("Institution"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentVariant", b => + { + b.HasOne("Metabase.Data.Component", "OfComponent") + .WithMany("VariantEdges") + .HasForeignKey("OfComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Component", "ToComponent") + .WithMany("VariantOfEdges") + .HasForeignKey("ToComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OfComponent"); + + b.Navigation("ToComponent"); + }); + + modelBuilder.Entity("Metabase.Data.DataFormat", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedDataFormats") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Metabase.Data.Reference", "Reference", b1 => + { + b1.Property("DataFormatId") + .HasColumnType("uuid"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("DataFormatId"); + + b1.ToTable("data_format", "metabase"); + + b1.WithOwner() + .HasForeignKey("DataFormatId"); + + b1.OwnsOne("Metabase.Data.Publication", "Publication", b2 => + { + b2.Property("ReferenceDataFormatId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("ArXiv") + .HasColumnType("text"); + + b2.PrimitiveCollection("Authors") + .HasColumnType("text[]"); + + b2.Property("Doi") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Urn") + .HasColumnType("text"); + + b2.Property("WebAddress") + .HasColumnType("text"); + + b2.HasKey("ReferenceDataFormatId"); + + b2.ToTable("data_format", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceDataFormatId"); + }); + + b1.OwnsOne("Metabase.Data.Standard", "Standard", b2 => + { + b2.Property("ReferenceDataFormatId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Locator") + .HasColumnType("text"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Year") + .HasColumnType("integer"); + + b2.HasKey("ReferenceDataFormatId"); + + b2.ToTable("data_format", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceDataFormatId"); + + b2.OwnsOne("Metabase.Data.Numeration", "Numeration", b3 => + { + b3.Property("StandardReferenceDataFormatId") + .HasColumnType("uuid"); + + b3.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b3.Property("Prefix") + .HasColumnType("text"); + + b3.Property("Suffix") + .HasColumnType("text"); + + b3.HasKey("StandardReferenceDataFormatId"); + + b3.ToTable("data_format", "metabase"); + + b3.WithOwner() + .HasForeignKey("StandardReferenceDataFormatId"); + }); + + b2.Navigation("Numeration") + .IsRequired(); + }); + + b1.Navigation("Publication"); + + b1.Navigation("Standard"); + }); + + b.Navigation("Manager"); + + b.Navigation("Reference"); + }); + + modelBuilder.Entity("Metabase.Data.Database", b => + { + b.HasOne("Metabase.Data.Institution", "Operator") + .WithMany("OperatedDatabases") + .HasForeignKey("OperatorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Operator"); + }); + + modelBuilder.Entity("Metabase.Data.GnuPgKeyFingerprint", b => + { + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("GnuPgKeyFingerprints") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", "User") + .WithMany("GnuPgKeyFingerprints") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Institution"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Metabase.Data.Institution", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedInstitutions") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict); + + b.OwnsOne("Metabase.Data.ContactInformation", "Contact", b1 => + { + b1.Property("InstitutionId") + .HasColumnType("uuid"); + + b1.Property("EmailAddress") + .HasColumnType("text"); + + b1.Property("IsEmailAddressConfirmed") + .HasColumnType("boolean"); + + b1.Property("IsPhoneNumberConfirmed") + .HasColumnType("boolean"); + + b1.Property("PhoneNumber") + .HasColumnType("text"); + + b1.Property("PostalAddress") + .HasColumnType("text"); + + b1.Property("WebsiteLocator") + .HasColumnType("text"); + + b1.HasKey("InstitutionId"); + + b1.ToTable("institution", "metabase"); + + b1.WithOwner() + .HasForeignKey("InstitutionId"); + }); + + b.Navigation("Contact"); + + b.Navigation("Manager"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionMethodDeveloper", b => + { + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("DevelopedMethodEdges") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Method", "Method") + .WithMany("InstitutionDeveloperEdges") + .HasForeignKey("MethodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Institution"); + + b.Navigation("Method"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionRepresentative", b => + { + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("RepresentativeEdges") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", "User") + .WithMany("RepresentedInstitutionEdges") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Institution"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Metabase.Data.Method", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedMethods") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Metabase.Data.Reference", "Reference", b1 => + { + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("MethodId"); + + b1.ToTable("method", "metabase"); + + b1.WithOwner() + .HasForeignKey("MethodId"); + + b1.OwnsOne("Metabase.Data.Publication", "Publication", b2 => + { + b2.Property("ReferenceMethodId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("ArXiv") + .HasColumnType("text"); + + b2.PrimitiveCollection("Authors") + .HasColumnType("text[]"); + + b2.Property("Doi") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Urn") + .HasColumnType("text"); + + b2.Property("WebAddress") + .HasColumnType("text"); + + b2.HasKey("ReferenceMethodId"); + + b2.ToTable("method", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceMethodId"); + }); + + b1.OwnsOne("Metabase.Data.Standard", "Standard", b2 => + { + b2.Property("ReferenceMethodId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Locator") + .HasColumnType("text"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Year") + .HasColumnType("integer"); + + b2.HasKey("ReferenceMethodId"); + + b2.ToTable("method", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceMethodId"); + + b2.OwnsOne("Metabase.Data.Numeration", "Numeration", b3 => + { + b3.Property("StandardReferenceMethodId") + .HasColumnType("uuid"); + + b3.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b3.Property("Prefix") + .HasColumnType("text"); + + b3.Property("Suffix") + .HasColumnType("text"); + + b3.HasKey("StandardReferenceMethodId"); + + b3.ToTable("method", "metabase"); + + b3.WithOwner() + .HasForeignKey("StandardReferenceMethodId"); + }); + + b2.Navigation("Numeration") + .IsRequired(); + }); + + b1.Navigation("Publication"); + + b1.Navigation("Standard"); + }); + + b.OwnsMany("Metabase.Data.MethodParameter", "Parameters", b1 => + { + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Type") + .HasColumnType("jsonb"); + + b1.HasKey("MethodId", "Id"); + + b1.ToTable("MethodParameter", "metabase"); + + b1.WithOwner() + .HasForeignKey("MethodId"); + }); + + b.OwnsMany("Metabase.Data.MethodSource", "Sources", b1 => + { + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("MethodId", "Id"); + + b1.ToTable("MethodSource", "metabase"); + + b1.WithOwner() + .HasForeignKey("MethodId"); + }); + + b.Navigation("Manager"); + + b.Navigation("Parameters"); + + b.Navigation("Reference"); + + b.Navigation("Sources"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", b => + { + b.HasOne("Metabase.Data.Institution", "Owner") + .WithMany("OpenIdConnectApplications") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", b => + { + b.HasOne("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectToken", b => + { + b.HasOne("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("Metabase.Data.RoleClaim", b => + { + b.HasOne("Metabase.Data.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserClaim", b => + { + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserLogin", b => + { + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserMethodDeveloper", b => + { + b.HasOne("Metabase.Data.Method", "Method") + .WithMany("UserDeveloperEdges") + .HasForeignKey("MethodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", "User") + .WithMany("DevelopedMethodEdges") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Method"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Metabase.Data.UserRole", b => + { + b.HasOne("Metabase.Data.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserToken", b => + { + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.Component", b => + { + b.Navigation("ConcretizationEdges"); + + b.Navigation("GeneralizationEdges"); + + b.Navigation("ManufacturerEdges"); + + b.Navigation("PartEdges"); + + b.Navigation("PartOfEdges"); + + b.Navigation("VariantEdges"); + + b.Navigation("VariantOfEdges"); + }); + + modelBuilder.Entity("Metabase.Data.Institution", b => + { + b.Navigation("DevelopedMethodEdges"); + + b.Navigation("GnuPgKeyFingerprints"); + + b.Navigation("ManagedComponents"); + + b.Navigation("ManagedDataFormats"); + + b.Navigation("ManagedInstitutions"); + + b.Navigation("ManagedMethods"); + + b.Navigation("ManufacturedComponentEdges"); + + b.Navigation("OpenIdConnectApplications"); + + b.Navigation("OperatedDatabases"); + + b.Navigation("RepresentativeEdges"); + }); + + modelBuilder.Entity("Metabase.Data.Method", b => + { + b.Navigation("InstitutionDeveloperEdges"); + + b.Navigation("UserDeveloperEdges"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", b => + { + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Metabase.Data.User", b => + { + b.Navigation("DevelopedMethodEdges"); + + b.Navigation("GnuPgKeyFingerprints"); + + b.Navigation("RepresentedInstitutionEdges"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Migrations/20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit.cs b/backend/src/Migrations/20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit.cs new file mode 100644 index 000000000..279f4ef94 --- /dev/null +++ b/backend/src/Migrations/20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Metabase.Migrations +{ + /// + public partial class SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + UPDATE metabase."OpenIddictApplications" + SET "ConsentType" = 'implicit' + WHERE "ClientId" = 'metabase'; + """ + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(""" + UPDATE metabase."OpenIddictApplications" + SET "ConsentType" = 'explicit' + WHERE "ClientId" = 'metabase'; + """ + ); + } + } +} \ No newline at end of file diff --git a/backend/src/Migrations/20260620182947_UseSequentialUuids.Designer.cs b/backend/src/Migrations/20260620182947_UseSequentialUuids.Designer.cs new file mode 100644 index 000000000..593bec3a2 --- /dev/null +++ b/backend/src/Migrations/20260620182947_UseSequentialUuids.Designer.cs @@ -0,0 +1,2298 @@ +// +using System; +using System.Text.Json; +using Metabase.Data; +using Metabase.Enumerations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; + +#nullable disable + +namespace Metabase.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260620182947_UseSequentialUuids")] + partial class UseSequentialUuids + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("metabase") + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "component_category", new[] { "layer", "material", "unit" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "database_verification_state", new[] { "pending", "verified" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "institution_operating_state", new[] { "not_operating", "operating" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "institution_representative_role", new[] { "assistant", "owner" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "institution_state", new[] { "pending", "verified" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "method_category", new[] { "calculation", "measurement" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "prime_surface", new[] { "inside", "outside" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "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("Metabase.Data.Component", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); + + b.Property("Abbreviation") + .HasColumnType("text"); + + b.Property?>("Availability") + .HasColumnType("tstzrange"); + + b.PrimitiveCollection("Categories") + .IsRequired() + .HasColumnType("metabase.component_category[]"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extras") + .HasColumnType("jsonb"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("Name") + .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("ManagerId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("component", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentAssembly", b => + { + b.Property("AssembledComponentId") + .HasColumnType("uuid"); + + b.Property("PartComponentId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Index") + .HasColumnType("smallint"); + + b.Property("PrimeSurface") + .HasColumnType("metabase.prime_surface"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("AssembledComponentId", "PartComponentId"); + + b.HasIndex("PartComponentId"); + + b.ToTable("component_assembly", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentConcretizationAndGeneralization", b => + { + b.Property("GeneralComponentId") + .HasColumnType("uuid"); + + b.Property("ConcreteComponentId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("GeneralComponentId", "ConcreteComponentId"); + + b.HasIndex("ConcreteComponentId"); + + b.ToTable("component_concretization_and_generalization", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentManufacturer", b => + { + b.Property("ComponentId") + .HasColumnType("uuid"); + + b.Property("InstitutionId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("ComponentId", "InstitutionId"); + + b.HasIndex("InstitutionId"); + + b.ToTable("component_manufacturer", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentVariant", b => + { + b.Property("OfComponentId") + .HasColumnType("uuid"); + + b.Property("ToComponentId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("OfComponentId", "ToComponentId"); + + b.HasIndex("ToComponentId"); + + b.ToTable("component_variant", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.DataFormat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extension") + .HasColumnType("text"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("MediaType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SchemaLocator") + .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("ManagerId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("data_format", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Database", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Locator") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OperatorId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VerificationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerificationState") + .HasColumnType("metabase.database_verification_state"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("OperatorId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("database", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.GnuPgKeyFingerprint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); + + b.Property("AllowedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Fingerprint") + .IsRequired() + .HasColumnType("text"); + + b.Property("ForbiddenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("InstitutionId") + .HasColumnType("uuid"); + + 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.HasKey("Id"); + + b.HasIndex("Fingerprint") + .IsUnique(); + + b.HasIndex("InstitutionId"); + + b.HasIndex("UserId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.ToTable("gnu_pg_fingerprint", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Institution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); + + b.Property("Abbreviation") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extras") + .HasColumnType("jsonb"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OperatingState") + .HasColumnType("metabase.institution_operating_state"); + + b.Property("State") + .HasColumnType("metabase.institution_state"); + + 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("ManagerId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("institution", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionMethodDeveloper", b => + { + b.Property("InstitutionId") + .HasColumnType("uuid"); + + b.Property("MethodId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("InstitutionId", "MethodId"); + + b.HasIndex("MethodId"); + + b.ToTable("institution_method_developer", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionRepresentative", b => + { + b.Property("InstitutionId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.Property("Role") + .HasColumnType("metabase.institution_representative_role"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("InstitutionId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("institution_representative", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Method", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); + + b.Property?>("Availability") + .HasColumnType("tstzrange"); + + b.Property("CalculationLocator") + .HasColumnType("text"); + + b.PrimitiveCollection("Categories") + .IsRequired() + .HasColumnType("metabase.method_category[]"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property?>("Validity") + .HasColumnType("tstzrange"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("ManagerId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("method", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); + + 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("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("JsonWebKeySet") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + 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.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.HasIndex("OwnerId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.ToTable("OpenIddictApplications", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + 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.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.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + 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.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.ToTable("OpenIddictScopes", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AuthorizationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + 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.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("role", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.RoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("role_claim", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("PostalAddress") + .HasColumnType("text"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("WebsiteLocator") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("user", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("user_claim", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("user_login", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserMethodDeveloper", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("MethodId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("UserId", "MethodId"); + + b.HasIndex("MethodId"); + + b.ToTable("user_method_developer", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("user_role", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("user_token", "metabase"); + }); + + 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", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Component", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedComponents") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Metabase.Data.DescriptionOrReference", "PrimeDirection", b1 => + { + b1.Property("ComponentId") + .HasColumnType("uuid"); + + b1.Property("Description") + .HasColumnType("text"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("ComponentId"); + + b1.ToTable("component", "metabase"); + + b1.WithOwner() + .HasForeignKey("ComponentId"); + + b1.OwnsOne("Metabase.Data.Reference", "Reference", b2 => + { + b2.Property("DescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DescriptionOrReferenceComponentId"); + + b2.ToTable("component", "metabase"); + + b2.WithOwner() + .HasForeignKey("DescriptionOrReferenceComponentId"); + + b2.OwnsOne("Metabase.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + 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("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + }); + + b2.OwnsOne("Metabase.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.OwnsOne("Metabase.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDescriptionOrReferenceComponentId"); + + b4.ToTable("component", "metabase"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDescriptionOrReferenceComponentId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Reference"); + }); + + b.OwnsOne("Metabase.Data.DescriptionOrReference", "PrimeSurface", b1 => + { + b1.Property("ComponentId") + .HasColumnType("uuid"); + + b1.Property("Description") + .HasColumnType("text"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("ComponentId"); + + b1.ToTable("component", "metabase"); + + b1.WithOwner() + .HasForeignKey("ComponentId"); + + b1.OwnsOne("Metabase.Data.Reference", "Reference", b2 => + { + b2.Property("DescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DescriptionOrReferenceComponentId"); + + b2.ToTable("component", "metabase"); + + b2.WithOwner() + .HasForeignKey("DescriptionOrReferenceComponentId"); + + b2.OwnsOne("Metabase.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + 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("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + }); + + b2.OwnsOne("Metabase.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.OwnsOne("Metabase.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDescriptionOrReferenceComponentId"); + + b4.ToTable("component", "metabase"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDescriptionOrReferenceComponentId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Reference"); + }); + + b.OwnsOne("Metabase.Data.DescriptionOrReference", "SwitchableLayers", b1 => + { + b1.Property("ComponentId") + .HasColumnType("uuid"); + + b1.Property("Description") + .HasColumnType("text"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("ComponentId"); + + b1.ToTable("component", "metabase"); + + b1.WithOwner() + .HasForeignKey("ComponentId"); + + b1.OwnsOne("Metabase.Data.Reference", "Reference", b2 => + { + b2.Property("DescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DescriptionOrReferenceComponentId"); + + b2.ToTable("component", "metabase"); + + b2.WithOwner() + .HasForeignKey("DescriptionOrReferenceComponentId"); + + b2.OwnsOne("Metabase.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + 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("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + }); + + b2.OwnsOne("Metabase.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.OwnsOne("Metabase.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDescriptionOrReferenceComponentId"); + + b4.ToTable("component", "metabase"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDescriptionOrReferenceComponentId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Reference"); + }); + + b.Navigation("Manager"); + + b.Navigation("PrimeDirection"); + + b.Navigation("PrimeSurface"); + + b.Navigation("SwitchableLayers"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentAssembly", b => + { + b.HasOne("Metabase.Data.Component", "AssembledComponent") + .WithMany("PartEdges") + .HasForeignKey("AssembledComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Component", "PartComponent") + .WithMany("PartOfEdges") + .HasForeignKey("PartComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AssembledComponent"); + + b.Navigation("PartComponent"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentConcretizationAndGeneralization", b => + { + b.HasOne("Metabase.Data.Component", "ConcreteComponent") + .WithMany("GeneralizationEdges") + .HasForeignKey("ConcreteComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Component", "GeneralComponent") + .WithMany("ConcretizationEdges") + .HasForeignKey("GeneralComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ConcreteComponent"); + + b.Navigation("GeneralComponent"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentManufacturer", b => + { + b.HasOne("Metabase.Data.Component", "Component") + .WithMany("ManufacturerEdges") + .HasForeignKey("ComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("ManufacturedComponentEdges") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Component"); + + b.Navigation("Institution"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentVariant", b => + { + b.HasOne("Metabase.Data.Component", "OfComponent") + .WithMany("VariantEdges") + .HasForeignKey("OfComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Component", "ToComponent") + .WithMany("VariantOfEdges") + .HasForeignKey("ToComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OfComponent"); + + b.Navigation("ToComponent"); + }); + + modelBuilder.Entity("Metabase.Data.DataFormat", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedDataFormats") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Metabase.Data.Reference", "Reference", b1 => + { + b1.Property("DataFormatId") + .HasColumnType("uuid"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("DataFormatId"); + + b1.ToTable("data_format", "metabase"); + + b1.WithOwner() + .HasForeignKey("DataFormatId"); + + b1.OwnsOne("Metabase.Data.Publication", "Publication", b2 => + { + b2.Property("ReferenceDataFormatId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("ArXiv") + .HasColumnType("text"); + + b2.PrimitiveCollection("Authors") + .HasColumnType("text[]"); + + b2.Property("Doi") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Urn") + .HasColumnType("text"); + + b2.Property("WebAddress") + .HasColumnType("text"); + + b2.HasKey("ReferenceDataFormatId"); + + b2.ToTable("data_format", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceDataFormatId"); + }); + + b1.OwnsOne("Metabase.Data.Standard", "Standard", b2 => + { + b2.Property("ReferenceDataFormatId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Locator") + .HasColumnType("text"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Year") + .HasColumnType("integer"); + + b2.HasKey("ReferenceDataFormatId"); + + b2.ToTable("data_format", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceDataFormatId"); + + b2.OwnsOne("Metabase.Data.Numeration", "Numeration", b3 => + { + b3.Property("StandardReferenceDataFormatId") + .HasColumnType("uuid"); + + b3.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b3.Property("Prefix") + .HasColumnType("text"); + + b3.Property("Suffix") + .HasColumnType("text"); + + b3.HasKey("StandardReferenceDataFormatId"); + + b3.ToTable("data_format", "metabase"); + + b3.WithOwner() + .HasForeignKey("StandardReferenceDataFormatId"); + }); + + b2.Navigation("Numeration") + .IsRequired(); + }); + + b1.Navigation("Publication"); + + b1.Navigation("Standard"); + }); + + b.Navigation("Manager"); + + b.Navigation("Reference"); + }); + + modelBuilder.Entity("Metabase.Data.Database", b => + { + b.HasOne("Metabase.Data.Institution", "Operator") + .WithMany("OperatedDatabases") + .HasForeignKey("OperatorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Operator"); + }); + + modelBuilder.Entity("Metabase.Data.GnuPgKeyFingerprint", b => + { + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("GnuPgKeyFingerprints") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", "User") + .WithMany("GnuPgKeyFingerprints") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Institution"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Metabase.Data.Institution", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedInstitutions") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict); + + b.OwnsOne("Metabase.Data.ContactInformation", "Contact", b1 => + { + b1.Property("InstitutionId") + .HasColumnType("uuid"); + + b1.Property("EmailAddress") + .HasColumnType("text"); + + b1.Property("IsEmailAddressConfirmed") + .HasColumnType("boolean"); + + b1.Property("IsPhoneNumberConfirmed") + .HasColumnType("boolean"); + + b1.Property("PhoneNumber") + .HasColumnType("text"); + + b1.Property("PostalAddress") + .HasColumnType("text"); + + b1.Property("WebsiteLocator") + .HasColumnType("text"); + + b1.HasKey("InstitutionId"); + + b1.ToTable("institution", "metabase"); + + b1.WithOwner() + .HasForeignKey("InstitutionId"); + }); + + b.Navigation("Contact"); + + b.Navigation("Manager"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionMethodDeveloper", b => + { + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("DevelopedMethodEdges") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Method", "Method") + .WithMany("InstitutionDeveloperEdges") + .HasForeignKey("MethodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Institution"); + + b.Navigation("Method"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionRepresentative", b => + { + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("RepresentativeEdges") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", "User") + .WithMany("RepresentedInstitutionEdges") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Institution"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Metabase.Data.Method", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedMethods") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Metabase.Data.Reference", "Reference", b1 => + { + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("MethodId"); + + b1.ToTable("method", "metabase"); + + b1.WithOwner() + .HasForeignKey("MethodId"); + + b1.OwnsOne("Metabase.Data.Publication", "Publication", b2 => + { + b2.Property("ReferenceMethodId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("ArXiv") + .HasColumnType("text"); + + b2.PrimitiveCollection("Authors") + .HasColumnType("text[]"); + + b2.Property("Doi") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Urn") + .HasColumnType("text"); + + b2.Property("WebAddress") + .HasColumnType("text"); + + b2.HasKey("ReferenceMethodId"); + + b2.ToTable("method", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceMethodId"); + }); + + b1.OwnsOne("Metabase.Data.Standard", "Standard", b2 => + { + b2.Property("ReferenceMethodId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Locator") + .HasColumnType("text"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Year") + .HasColumnType("integer"); + + b2.HasKey("ReferenceMethodId"); + + b2.ToTable("method", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceMethodId"); + + b2.OwnsOne("Metabase.Data.Numeration", "Numeration", b3 => + { + b3.Property("StandardReferenceMethodId") + .HasColumnType("uuid"); + + b3.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b3.Property("Prefix") + .HasColumnType("text"); + + b3.Property("Suffix") + .HasColumnType("text"); + + b3.HasKey("StandardReferenceMethodId"); + + b3.ToTable("method", "metabase"); + + b3.WithOwner() + .HasForeignKey("StandardReferenceMethodId"); + }); + + b2.Navigation("Numeration") + .IsRequired(); + }); + + b1.Navigation("Publication"); + + b1.Navigation("Standard"); + }); + + b.OwnsMany("Metabase.Data.MethodParameter", "Parameters", b1 => + { + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Type") + .HasColumnType("jsonb"); + + b1.HasKey("MethodId", "Id"); + + b1.ToTable("MethodParameter", "metabase"); + + b1.WithOwner() + .HasForeignKey("MethodId"); + }); + + b.OwnsMany("Metabase.Data.MethodSource", "Sources", b1 => + { + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("MethodId", "Id"); + + b1.ToTable("MethodSource", "metabase"); + + b1.WithOwner() + .HasForeignKey("MethodId"); + }); + + b.Navigation("Manager"); + + b.Navigation("Parameters"); + + b.Navigation("Reference"); + + b.Navigation("Sources"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", b => + { + b.HasOne("Metabase.Data.Institution", "Owner") + .WithMany("OpenIdConnectApplications") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", b => + { + b.HasOne("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectToken", b => + { + b.HasOne("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("Metabase.Data.RoleClaim", b => + { + b.HasOne("Metabase.Data.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserClaim", b => + { + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserLogin", b => + { + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserMethodDeveloper", b => + { + b.HasOne("Metabase.Data.Method", "Method") + .WithMany("UserDeveloperEdges") + .HasForeignKey("MethodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", "User") + .WithMany("DevelopedMethodEdges") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Method"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Metabase.Data.UserRole", b => + { + b.HasOne("Metabase.Data.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserToken", b => + { + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.Component", b => + { + b.Navigation("ConcretizationEdges"); + + b.Navigation("GeneralizationEdges"); + + b.Navigation("ManufacturerEdges"); + + b.Navigation("PartEdges"); + + b.Navigation("PartOfEdges"); + + b.Navigation("VariantEdges"); + + b.Navigation("VariantOfEdges"); + }); + + modelBuilder.Entity("Metabase.Data.Institution", b => + { + b.Navigation("DevelopedMethodEdges"); + + b.Navigation("GnuPgKeyFingerprints"); + + b.Navigation("ManagedComponents"); + + b.Navigation("ManagedDataFormats"); + + b.Navigation("ManagedInstitutions"); + + b.Navigation("ManagedMethods"); + + b.Navigation("ManufacturedComponentEdges"); + + b.Navigation("OpenIdConnectApplications"); + + b.Navigation("OperatedDatabases"); + + b.Navigation("RepresentativeEdges"); + }); + + modelBuilder.Entity("Metabase.Data.Method", b => + { + b.Navigation("InstitutionDeveloperEdges"); + + b.Navigation("UserDeveloperEdges"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", b => + { + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Metabase.Data.User", b => + { + b.Navigation("DevelopedMethodEdges"); + + b.Navigation("GnuPgKeyFingerprints"); + + b.Navigation("RepresentedInstitutionEdges"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Migrations/20260620182947_UseSequentialUuids.cs b/backend/src/Migrations/20260620182947_UseSequentialUuids.cs new file mode 100644 index 000000000..b1ecd832d --- /dev/null +++ b/backend/src/Migrations/20260620182947_UseSequentialUuids.cs @@ -0,0 +1,261 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Metabase.Migrations +{ + /// + public partial class UseSequentialUuids : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "user", + type: "uuid", + nullable: false, + defaultValueSql: "uuidv7()", + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "gen_random_uuid()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "OpenIddictTokens", + type: "uuid", + nullable: false, + defaultValueSql: "uuidv7()", + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "gen_random_uuid()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "OpenIddictScopes", + type: "uuid", + nullable: false, + defaultValueSql: "uuidv7()", + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "gen_random_uuid()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "OpenIddictAuthorizations", + type: "uuid", + nullable: false, + defaultValueSql: "uuidv7()", + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "gen_random_uuid()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "OpenIddictApplications", + type: "uuid", + nullable: false, + defaultValueSql: "uuidv7()", + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "gen_random_uuid()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "method", + type: "uuid", + nullable: false, + defaultValueSql: "uuidv7()", + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "gen_random_uuid()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "institution", + type: "uuid", + nullable: false, + defaultValueSql: "uuidv7()", + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "gen_random_uuid()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "gnu_pg_fingerprint", + type: "uuid", + nullable: false, + defaultValueSql: "uuidv7()", + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "gen_random_uuid()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "database", + type: "uuid", + nullable: false, + defaultValueSql: "uuidv7()", + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "gen_random_uuid()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "data_format", + type: "uuid", + nullable: false, + defaultValueSql: "uuidv7()", + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "gen_random_uuid()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "component", + type: "uuid", + nullable: false, + defaultValueSql: "uuidv7()", + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "gen_random_uuid()"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "user", + type: "uuid", + nullable: false, + defaultValueSql: "gen_random_uuid()", + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "uuidv7()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "OpenIddictTokens", + type: "uuid", + nullable: false, + defaultValueSql: "gen_random_uuid()", + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "uuidv7()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "OpenIddictScopes", + type: "uuid", + nullable: false, + defaultValueSql: "gen_random_uuid()", + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "uuidv7()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "OpenIddictAuthorizations", + type: "uuid", + nullable: false, + defaultValueSql: "gen_random_uuid()", + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "uuidv7()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "OpenIddictApplications", + type: "uuid", + nullable: false, + defaultValueSql: "gen_random_uuid()", + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "uuidv7()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "method", + type: "uuid", + nullable: false, + defaultValueSql: "gen_random_uuid()", + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "uuidv7()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "institution", + type: "uuid", + nullable: false, + defaultValueSql: "gen_random_uuid()", + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "uuidv7()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "gnu_pg_fingerprint", + type: "uuid", + nullable: false, + defaultValueSql: "gen_random_uuid()", + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "uuidv7()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "database", + type: "uuid", + nullable: false, + defaultValueSql: "gen_random_uuid()", + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "uuidv7()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "data_format", + type: "uuid", + nullable: false, + defaultValueSql: "gen_random_uuid()", + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "uuidv7()"); + + migrationBuilder.AlterColumn( + name: "Id", + schema: "metabase", + table: "component", + type: "uuid", + nullable: false, + defaultValueSql: "gen_random_uuid()", + oldClrType: typeof(Guid), + oldType: "uuid", + oldDefaultValueSql: "uuidv7()"); + } + } +} diff --git a/backend/src/Migrations/20260625115427_DropContactExistsColumnFromInstitution.Designer.cs b/backend/src/Migrations/20260625115427_DropContactExistsColumnFromInstitution.Designer.cs new file mode 100644 index 000000000..3b9d54f63 --- /dev/null +++ b/backend/src/Migrations/20260625115427_DropContactExistsColumnFromInstitution.Designer.cs @@ -0,0 +1,2298 @@ +// +using System; +using System.Text.Json; +using Metabase.Data; +using Metabase.Enumerations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; + +#nullable disable + +namespace Metabase.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260625115427_DropContactExistsColumnFromInstitution")] + partial class DropContactExistsColumnFromInstitution + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("metabase") + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "component_category", new[] { "layer", "material", "unit" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "database_verification_state", new[] { "pending", "verified" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "institution_operating_state", new[] { "not_operating", "operating" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "institution_representative_role", new[] { "assistant", "owner" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "institution_state", new[] { "pending", "verified" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "method_category", new[] { "calculation", "measurement" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "prime_surface", new[] { "inside", "outside" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "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("Metabase.Data.Component", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); + + b.Property("Abbreviation") + .HasColumnType("text"); + + b.Property?>("Availability") + .HasColumnType("tstzrange"); + + b.PrimitiveCollection("Categories") + .IsRequired() + .HasColumnType("metabase.component_category[]"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extras") + .HasColumnType("jsonb"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("Name") + .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("ManagerId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("component", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentAssembly", b => + { + b.Property("AssembledComponentId") + .HasColumnType("uuid"); + + b.Property("PartComponentId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Index") + .HasColumnType("smallint"); + + b.Property("PrimeSurface") + .HasColumnType("metabase.prime_surface"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("AssembledComponentId", "PartComponentId"); + + b.HasIndex("PartComponentId"); + + b.ToTable("component_assembly", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentConcretizationAndGeneralization", b => + { + b.Property("GeneralComponentId") + .HasColumnType("uuid"); + + b.Property("ConcreteComponentId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("GeneralComponentId", "ConcreteComponentId"); + + b.HasIndex("ConcreteComponentId"); + + b.ToTable("component_concretization_and_generalization", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentManufacturer", b => + { + b.Property("ComponentId") + .HasColumnType("uuid"); + + b.Property("InstitutionId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("ComponentId", "InstitutionId"); + + b.HasIndex("InstitutionId"); + + b.ToTable("component_manufacturer", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentVariant", b => + { + b.Property("OfComponentId") + .HasColumnType("uuid"); + + b.Property("ToComponentId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("OfComponentId", "ToComponentId"); + + b.HasIndex("ToComponentId"); + + b.ToTable("component_variant", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.DataFormat", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extension") + .HasColumnType("text"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("MediaType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("SchemaLocator") + .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("ManagerId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("data_format", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Database", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Locator") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OperatorId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("VerificationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerificationState") + .HasColumnType("metabase.database_verification_state"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("OperatorId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("database", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.GnuPgKeyFingerprint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); + + b.Property("AllowedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Fingerprint") + .IsRequired() + .HasColumnType("text"); + + b.Property("ForbiddenAt") + .HasColumnType("timestamp with time zone"); + + b.Property("InstitutionId") + .HasColumnType("uuid"); + + 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.HasKey("Id"); + + b.HasIndex("Fingerprint") + .IsUnique(); + + b.HasIndex("InstitutionId"); + + b.HasIndex("UserId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.ToTable("gnu_pg_fingerprint", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Institution", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); + + b.Property("Abbreviation") + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Extras") + .HasColumnType("jsonb"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OperatingState") + .HasColumnType("metabase.institution_operating_state"); + + b.Property("State") + .HasColumnType("metabase.institution_state"); + + 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("ManagerId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("institution", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionMethodDeveloper", b => + { + b.Property("InstitutionId") + .HasColumnType("uuid"); + + b.Property("MethodId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("InstitutionId", "MethodId"); + + b.HasIndex("MethodId"); + + b.ToTable("institution_method_developer", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionRepresentative", b => + { + b.Property("InstitutionId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.Property("Role") + .HasColumnType("metabase.institution_representative_role"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("InstitutionId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("institution_representative", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Method", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); + + b.Property?>("Availability") + .HasColumnType("tstzrange"); + + b.Property("CalculationLocator") + .HasColumnType("text"); + + b.PrimitiveCollection("Categories") + .IsRequired() + .HasColumnType("metabase.method_category[]"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("ManagerId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property?>("Validity") + .HasColumnType("tstzrange"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("ManagerId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("method", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); + + 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("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("DisplayName") + .HasColumnType("text"); + + b.Property("DisplayNames") + .HasColumnType("text"); + + b.Property("JsonWebKeySet") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + 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.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("ClientId") + .IsUnique(); + + b.HasIndex("OwnerId"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.ToTable("OpenIddictApplications", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + 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.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.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictAuthorizations", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectScope", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + 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.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.ToTable("OpenIddictScopes", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); + + b.Property("ApplicationId") + .HasColumnType("uuid"); + + b.Property("AuthorizationId") + .HasColumnType("uuid"); + + b.Property("ConcurrencyToken") + .IsConcurrencyToken() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + 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.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("AuthorizationId"); + + b.HasIndex("ReferenceId") + .IsUnique(); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); + + b.ToTable("OpenIddictTokens", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("role", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.RoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("role_claim", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("PostalAddress") + .HasColumnType("text"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.Property("WebsiteLocator") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + + b.ToTable("user", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("user_claim", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("user_login", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserMethodDeveloper", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("MethodId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Pending") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("UserId", "MethodId"); + + b.HasIndex("MethodId"); + + b.ToTable("user_method_developer", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("user_role", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.UserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("user_token", "metabase"); + }); + + 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", "metabase"); + }); + + modelBuilder.Entity("Metabase.Data.Component", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedComponents") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Metabase.Data.DescriptionOrReference", "PrimeDirection", b1 => + { + b1.Property("ComponentId") + .HasColumnType("uuid"); + + b1.Property("Description") + .HasColumnType("text"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("ComponentId"); + + b1.ToTable("component", "metabase"); + + b1.WithOwner() + .HasForeignKey("ComponentId"); + + b1.OwnsOne("Metabase.Data.Reference", "Reference", b2 => + { + b2.Property("DescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DescriptionOrReferenceComponentId"); + + b2.ToTable("component", "metabase"); + + b2.WithOwner() + .HasForeignKey("DescriptionOrReferenceComponentId"); + + b2.OwnsOne("Metabase.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + 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("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + }); + + b2.OwnsOne("Metabase.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.OwnsOne("Metabase.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDescriptionOrReferenceComponentId"); + + b4.ToTable("component", "metabase"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDescriptionOrReferenceComponentId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Reference"); + }); + + b.OwnsOne("Metabase.Data.DescriptionOrReference", "PrimeSurface", b1 => + { + b1.Property("ComponentId") + .HasColumnType("uuid"); + + b1.Property("Description") + .HasColumnType("text"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("ComponentId"); + + b1.ToTable("component", "metabase"); + + b1.WithOwner() + .HasForeignKey("ComponentId"); + + b1.OwnsOne("Metabase.Data.Reference", "Reference", b2 => + { + b2.Property("DescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DescriptionOrReferenceComponentId"); + + b2.ToTable("component", "metabase"); + + b2.WithOwner() + .HasForeignKey("DescriptionOrReferenceComponentId"); + + b2.OwnsOne("Metabase.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + 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("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + }); + + b2.OwnsOne("Metabase.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.OwnsOne("Metabase.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDescriptionOrReferenceComponentId"); + + b4.ToTable("component", "metabase"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDescriptionOrReferenceComponentId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Reference"); + }); + + b.OwnsOne("Metabase.Data.DescriptionOrReference", "SwitchableLayers", b1 => + { + b1.Property("ComponentId") + .HasColumnType("uuid"); + + b1.Property("Description") + .HasColumnType("text"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("ComponentId"); + + b1.ToTable("component", "metabase"); + + b1.WithOwner() + .HasForeignKey("ComponentId"); + + b1.OwnsOne("Metabase.Data.Reference", "Reference", b2 => + { + b2.Property("DescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.HasKey("DescriptionOrReferenceComponentId"); + + b2.ToTable("component", "metabase"); + + b2.WithOwner() + .HasForeignKey("DescriptionOrReferenceComponentId"); + + b2.OwnsOne("Metabase.Data.Publication", "Publication", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + 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("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + }); + + b2.OwnsOne("Metabase.Data.Standard", "Standard", b3 => + { + b3.Property("ReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b3.Property("Abstract") + .HasColumnType("text"); + + b3.Property("Exists") + .HasColumnType("boolean"); + + b3.Property("Locator") + .HasColumnType("text"); + + b3.Property("Section") + .HasColumnType("text"); + + b3.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b3.Property("Title") + .HasColumnType("text"); + + b3.Property("Year") + .HasColumnType("integer"); + + b3.HasKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.ToTable("component", "metabase"); + + b3.WithOwner() + .HasForeignKey("ReferenceDescriptionOrReferenceComponentId"); + + b3.OwnsOne("Metabase.Data.Numeration", "Numeration", b4 => + { + b4.Property("StandardReferenceDescriptionOrReferenceComponentId") + .HasColumnType("uuid"); + + b4.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b4.Property("Prefix") + .HasColumnType("text"); + + b4.Property("Suffix") + .HasColumnType("text"); + + b4.HasKey("StandardReferenceDescriptionOrReferenceComponentId"); + + b4.ToTable("component", "metabase"); + + b4.WithOwner() + .HasForeignKey("StandardReferenceDescriptionOrReferenceComponentId"); + }); + + b3.Navigation("Numeration") + .IsRequired(); + }); + + b2.Navigation("Publication"); + + b2.Navigation("Standard"); + }); + + b1.Navigation("Reference"); + }); + + b.Navigation("Manager"); + + b.Navigation("PrimeDirection"); + + b.Navigation("PrimeSurface"); + + b.Navigation("SwitchableLayers"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentAssembly", b => + { + b.HasOne("Metabase.Data.Component", "AssembledComponent") + .WithMany("PartEdges") + .HasForeignKey("AssembledComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Component", "PartComponent") + .WithMany("PartOfEdges") + .HasForeignKey("PartComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("AssembledComponent"); + + b.Navigation("PartComponent"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentConcretizationAndGeneralization", b => + { + b.HasOne("Metabase.Data.Component", "ConcreteComponent") + .WithMany("GeneralizationEdges") + .HasForeignKey("ConcreteComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Component", "GeneralComponent") + .WithMany("ConcretizationEdges") + .HasForeignKey("GeneralComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ConcreteComponent"); + + b.Navigation("GeneralComponent"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentManufacturer", b => + { + b.HasOne("Metabase.Data.Component", "Component") + .WithMany("ManufacturerEdges") + .HasForeignKey("ComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("ManufacturedComponentEdges") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Component"); + + b.Navigation("Institution"); + }); + + modelBuilder.Entity("Metabase.Data.ComponentVariant", b => + { + b.HasOne("Metabase.Data.Component", "OfComponent") + .WithMany("VariantEdges") + .HasForeignKey("OfComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Component", "ToComponent") + .WithMany("VariantOfEdges") + .HasForeignKey("ToComponentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OfComponent"); + + b.Navigation("ToComponent"); + }); + + modelBuilder.Entity("Metabase.Data.DataFormat", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedDataFormats") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Metabase.Data.Reference", "Reference", b1 => + { + b1.Property("DataFormatId") + .HasColumnType("uuid"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("DataFormatId"); + + b1.ToTable("data_format", "metabase"); + + b1.WithOwner() + .HasForeignKey("DataFormatId"); + + b1.OwnsOne("Metabase.Data.Publication", "Publication", b2 => + { + b2.Property("ReferenceDataFormatId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("ArXiv") + .HasColumnType("text"); + + b2.PrimitiveCollection("Authors") + .HasColumnType("text[]"); + + b2.Property("Doi") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Urn") + .HasColumnType("text"); + + b2.Property("WebAddress") + .HasColumnType("text"); + + b2.HasKey("ReferenceDataFormatId"); + + b2.ToTable("data_format", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceDataFormatId"); + }); + + b1.OwnsOne("Metabase.Data.Standard", "Standard", b2 => + { + b2.Property("ReferenceDataFormatId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Locator") + .HasColumnType("text"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Year") + .HasColumnType("integer"); + + b2.HasKey("ReferenceDataFormatId"); + + b2.ToTable("data_format", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceDataFormatId"); + + b2.OwnsOne("Metabase.Data.Numeration", "Numeration", b3 => + { + b3.Property("StandardReferenceDataFormatId") + .HasColumnType("uuid"); + + b3.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b3.Property("Prefix") + .HasColumnType("text"); + + b3.Property("Suffix") + .HasColumnType("text"); + + b3.HasKey("StandardReferenceDataFormatId"); + + b3.ToTable("data_format", "metabase"); + + b3.WithOwner() + .HasForeignKey("StandardReferenceDataFormatId"); + }); + + b2.Navigation("Numeration") + .IsRequired(); + }); + + b1.Navigation("Publication"); + + b1.Navigation("Standard"); + }); + + b.Navigation("Manager"); + + b.Navigation("Reference"); + }); + + modelBuilder.Entity("Metabase.Data.Database", b => + { + b.HasOne("Metabase.Data.Institution", "Operator") + .WithMany("OperatedDatabases") + .HasForeignKey("OperatorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Operator"); + }); + + modelBuilder.Entity("Metabase.Data.GnuPgKeyFingerprint", b => + { + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("GnuPgKeyFingerprints") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", "User") + .WithMany("GnuPgKeyFingerprints") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Institution"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Metabase.Data.Institution", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedInstitutions") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict); + + b.OwnsOne("Metabase.Data.ContactInformation", "Contact", b1 => + { + b1.Property("InstitutionId") + .HasColumnType("uuid"); + + b1.Property("EmailAddress") + .HasColumnType("text"); + + b1.Property("IsEmailAddressConfirmed") + .HasColumnType("boolean"); + + b1.Property("IsPhoneNumberConfirmed") + .HasColumnType("boolean"); + + b1.Property("PhoneNumber") + .HasColumnType("text"); + + b1.Property("PostalAddress") + .HasColumnType("text"); + + b1.Property("WebsiteLocator") + .HasColumnType("text"); + + b1.HasKey("InstitutionId"); + + b1.ToTable("institution", "metabase"); + + b1.WithOwner() + .HasForeignKey("InstitutionId"); + }); + + b.Navigation("Contact"); + + b.Navigation("Manager"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionMethodDeveloper", b => + { + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("DevelopedMethodEdges") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.Method", "Method") + .WithMany("InstitutionDeveloperEdges") + .HasForeignKey("MethodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Institution"); + + b.Navigation("Method"); + }); + + modelBuilder.Entity("Metabase.Data.InstitutionRepresentative", b => + { + b.HasOne("Metabase.Data.Institution", "Institution") + .WithMany("RepresentativeEdges") + .HasForeignKey("InstitutionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", "User") + .WithMany("RepresentedInstitutionEdges") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Institution"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Metabase.Data.Method", b => + { + b.HasOne("Metabase.Data.Institution", "Manager") + .WithMany("ManagedMethods") + .HasForeignKey("ManagerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Metabase.Data.Reference", "Reference", b1 => + { + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.Property("Exists") + .HasColumnType("boolean"); + + b1.HasKey("MethodId"); + + b1.ToTable("method", "metabase"); + + b1.WithOwner() + .HasForeignKey("MethodId"); + + b1.OwnsOne("Metabase.Data.Publication", "Publication", b2 => + { + b2.Property("ReferenceMethodId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("ArXiv") + .HasColumnType("text"); + + b2.PrimitiveCollection("Authors") + .HasColumnType("text[]"); + + b2.Property("Doi") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Urn") + .HasColumnType("text"); + + b2.Property("WebAddress") + .HasColumnType("text"); + + b2.HasKey("ReferenceMethodId"); + + b2.ToTable("method", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceMethodId"); + }); + + b1.OwnsOne("Metabase.Data.Standard", "Standard", b2 => + { + b2.Property("ReferenceMethodId") + .HasColumnType("uuid"); + + b2.Property("Abstract") + .HasColumnType("text"); + + b2.Property("Exists") + .HasColumnType("boolean"); + + b2.Property("Locator") + .HasColumnType("text"); + + b2.Property("Section") + .HasColumnType("text"); + + b2.PrimitiveCollection("Standardizers") + .IsRequired() + .HasColumnType("metabase.standardizer[]"); + + b2.Property("Title") + .HasColumnType("text"); + + b2.Property("Year") + .HasColumnType("integer"); + + b2.HasKey("ReferenceMethodId"); + + b2.ToTable("method", "metabase"); + + b2.WithOwner() + .HasForeignKey("ReferenceMethodId"); + + b2.OwnsOne("Metabase.Data.Numeration", "Numeration", b3 => + { + b3.Property("StandardReferenceMethodId") + .HasColumnType("uuid"); + + b3.Property("MainNumber") + .IsRequired() + .HasColumnType("text"); + + b3.Property("Prefix") + .HasColumnType("text"); + + b3.Property("Suffix") + .HasColumnType("text"); + + b3.HasKey("StandardReferenceMethodId"); + + b3.ToTable("method", "metabase"); + + b3.WithOwner() + .HasForeignKey("StandardReferenceMethodId"); + }); + + b2.Navigation("Numeration") + .IsRequired(); + }); + + b1.Navigation("Publication"); + + b1.Navigation("Standard"); + }); + + b.OwnsMany("Metabase.Data.MethodParameter", "Parameters", b1 => + { + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Type") + .HasColumnType("jsonb"); + + b1.HasKey("MethodId", "Id"); + + b1.ToTable("MethodParameter", "metabase"); + + b1.WithOwner() + .HasForeignKey("MethodId"); + }); + + b.OwnsMany("Metabase.Data.MethodSource", "Sources", b1 => + { + b1.Property("MethodId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("MethodId", "Id"); + + b1.ToTable("MethodSource", "metabase"); + + b1.WithOwner() + .HasForeignKey("MethodId"); + }); + + b.Navigation("Manager"); + + b.Navigation("Parameters"); + + b.Navigation("Reference"); + + b.Navigation("Sources"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", b => + { + b.HasOne("Metabase.Data.Institution", "Owner") + .WithMany("OpenIdConnectApplications") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", b => + { + b.HasOne("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", "Application") + .WithMany("Authorizations") + .HasForeignKey("ApplicationId"); + + b.Navigation("Application"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectToken", b => + { + b.HasOne("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", "Application") + .WithMany("Tokens") + .HasForeignKey("ApplicationId"); + + b.HasOne("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", "Authorization") + .WithMany("Tokens") + .HasForeignKey("AuthorizationId"); + + b.Navigation("Application"); + + b.Navigation("Authorization"); + }); + + modelBuilder.Entity("Metabase.Data.RoleClaim", b => + { + b.HasOne("Metabase.Data.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserClaim", b => + { + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserLogin", b => + { + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserMethodDeveloper", b => + { + b.HasOne("Metabase.Data.Method", "Method") + .WithMany("UserDeveloperEdges") + .HasForeignKey("MethodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", "User") + .WithMany("DevelopedMethodEdges") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Method"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Metabase.Data.UserRole", b => + { + b.HasOne("Metabase.Data.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.UserToken", b => + { + b.HasOne("Metabase.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Metabase.Data.Component", b => + { + b.Navigation("ConcretizationEdges"); + + b.Navigation("GeneralizationEdges"); + + b.Navigation("ManufacturerEdges"); + + b.Navigation("PartEdges"); + + b.Navigation("PartOfEdges"); + + b.Navigation("VariantEdges"); + + b.Navigation("VariantOfEdges"); + }); + + modelBuilder.Entity("Metabase.Data.Institution", b => + { + b.Navigation("DevelopedMethodEdges"); + + b.Navigation("GnuPgKeyFingerprints"); + + b.Navigation("ManagedComponents"); + + b.Navigation("ManagedDataFormats"); + + b.Navigation("ManagedInstitutions"); + + b.Navigation("ManagedMethods"); + + b.Navigation("ManufacturedComponentEdges"); + + b.Navigation("OpenIdConnectApplications"); + + b.Navigation("OperatedDatabases"); + + b.Navigation("RepresentativeEdges"); + }); + + modelBuilder.Entity("Metabase.Data.Method", b => + { + b.Navigation("InstitutionDeveloperEdges"); + + b.Navigation("UserDeveloperEdges"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectApplication", b => + { + b.Navigation("Authorizations"); + + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Metabase.Data.OpenIdConnect.OpenIdConnectAuthorization", b => + { + b.Navigation("Tokens"); + }); + + modelBuilder.Entity("Metabase.Data.User", b => + { + b.Navigation("DevelopedMethodEdges"); + + b.Navigation("GnuPgKeyFingerprints"); + + b.Navigation("RepresentedInstitutionEdges"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Migrations/20260625115427_DropContactExistsColumnFromInstitution.cs b/backend/src/Migrations/20260625115427_DropContactExistsColumnFromInstitution.cs new file mode 100644 index 000000000..656be1909 --- /dev/null +++ b/backend/src/Migrations/20260625115427_DropContactExistsColumnFromInstitution.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Metabase.Migrations +{ + /// + public partial class DropContactExistsColumnFromInstitution : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Contact_Exists", + schema: "metabase", + table: "institution"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Contact_Exists", + schema: "metabase", + table: "institution", + type: "boolean", + nullable: true); + } + } +} \ No newline at end of file diff --git a/backend/src/Migrations/ApplicationDbContextModelSnapshot.cs b/backend/src/Migrations/ApplicationDbContextModelSnapshot.cs index 1d34b3a59..795fcbaca 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("metabase") - .HasAnnotation("ProductVersion", "10.0.5") + .HasAnnotation("ProductVersion", "10.0.9") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "metabase", "component_category", new[] { "layer", "material", "unit" }); @@ -41,7 +41,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasDefaultValueSql("gen_random_uuid()"); + .HasDefaultValueSql("uuidv7()"); b.Property("Abbreviation") .HasColumnType("text"); @@ -53,6 +53,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("metabase.component_category[]"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("Description") .IsRequired() .HasColumnType("text"); @@ -67,6 +72,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() @@ -77,6 +87,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ManagerId"); + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + b.ToTable("component", "metabase"); }); @@ -88,12 +104,28 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("PartComponentId") .HasColumnType("uuid"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("Index") .HasColumnType("smallint"); b.Property("PrimeSurface") .HasColumnType("metabase.prime_surface"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.HasKey("AssembledComponentId", "PartComponentId"); b.HasIndex("PartComponentId"); @@ -109,6 +141,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ConcreteComponentId") .HasColumnType("uuid"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.HasKey("GeneralComponentId", "ConcreteComponentId"); b.HasIndex("ConcreteComponentId"); @@ -124,9 +172,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("InstitutionId") .HasColumnType("uuid"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("Pending") .HasColumnType("boolean"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.HasKey("ComponentId", "InstitutionId"); b.HasIndex("InstitutionId"); @@ -142,6 +206,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ToComponentId") .HasColumnType("uuid"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.HasKey("OfComponentId", "ToComponentId"); b.HasIndex("ToComponentId"); @@ -154,7 +234,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasDefaultValueSql("gen_random_uuid()"); + .HasDefaultValueSql("uuidv7()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("Description") .IsRequired() @@ -177,6 +262,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("SchemaLocator") .HasColumnType("text"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("Version") .IsConcurrencyToken() .ValueGeneratedOnAddOrUpdate() @@ -187,6 +277,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ManagerId"); + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + b.ToTable("data_format", "metabase"); }); @@ -195,7 +291,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasDefaultValueSql("gen_random_uuid()"); + .HasDefaultValueSql("uuidv7()"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("Description") .IsRequired() @@ -212,6 +313,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("OperatorId") .HasColumnType("uuid"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("VerificationCode") .IsRequired() .HasColumnType("text"); @@ -229,6 +335,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("OperatorId"); + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + b.ToTable("database", "metabase"); }); @@ -237,13 +349,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasDefaultValueSql("gen_random_uuid()"); + .HasDefaultValueSql("uuidv7()"); b.Property("AllowedAt") .HasColumnType("timestamp with time zone"); - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); b.Property("Fingerprint") .IsRequired() @@ -255,6 +369,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("InstitutionId") .HasColumnType("uuid"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("UserId") .HasColumnType("uuid"); @@ -273,6 +392,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("UserId"); + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + b.ToTable("gnu_pg_fingerprint", "metabase"); }); @@ -281,11 +403,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasDefaultValueSql("gen_random_uuid()"); + .HasDefaultValueSql("uuidv7()"); b.Property("Abbreviation") .HasColumnType("text"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("Description") .IsRequired() .HasColumnType("text"); @@ -306,6 +433,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("State") .HasColumnType("metabase.institution_state"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("Version") .IsConcurrencyToken() .ValueGeneratedOnAddOrUpdate() @@ -316,6 +448,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ManagerId"); + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + b.ToTable("institution", "metabase"); }); @@ -327,9 +465,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("MethodId") .HasColumnType("uuid"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("Pending") .HasColumnType("boolean"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.HasKey("InstitutionId", "MethodId"); b.HasIndex("MethodId"); @@ -345,12 +499,28 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("UserId") .HasColumnType("uuid"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("Pending") .HasColumnType("boolean"); b.Property("Role") .HasColumnType("metabase.institution_representative_role"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.HasKey("InstitutionId", "UserId"); b.HasIndex("UserId"); @@ -363,7 +533,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid") - .HasDefaultValueSql("gen_random_uuid()"); + .HasDefaultValueSql("uuidv7()"); b.Property?>("Availability") .HasColumnType("tstzrange"); @@ -375,6 +545,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("metabase.method_category[]"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("Description") .IsRequired() .HasColumnType("text"); @@ -386,6 +561,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property?>("Validity") .HasColumnType("tstzrange"); @@ -399,6 +579,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ManagerId"); + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + b.ToTable("method", "metabase"); }); @@ -406,7 +592,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); b.Property("ApplicationType") .HasMaxLength(50) @@ -432,6 +619,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(50) .HasColumnType("character varying(50)"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("DisplayName") .HasColumnType("text"); @@ -462,6 +654,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Settings") .HasColumnType("text"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("Version") .IsConcurrencyToken() .ValueGeneratedOnAddOrUpdate() @@ -475,6 +672,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("OwnerId"); + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + b.ToTable("OpenIddictApplications", "metabase"); }); @@ -482,7 +682,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); b.Property("ApplicationId") .HasColumnType("uuid"); @@ -492,6 +693,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(50) .HasColumnType("character varying(50)"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("CreationDate") .HasColumnType("timestamp with time zone"); @@ -513,6 +719,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(50) .HasColumnType("character varying(50)"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("Version") .IsConcurrencyToken() .ValueGeneratedOnAddOrUpdate() @@ -521,6 +732,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); b.ToTable("OpenIddictAuthorizations", "metabase"); @@ -530,13 +744,19 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); b.Property("ConcurrencyToken") .IsConcurrencyToken() .HasMaxLength(50) .HasColumnType("character varying(50)"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("Description") .HasColumnType("text"); @@ -559,6 +779,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Resources") .HasColumnType("text"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("Version") .IsConcurrencyToken() .ValueGeneratedOnAddOrUpdate() @@ -570,6 +795,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("Name") .IsUnique(); + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + b.ToTable("OpenIddictScopes", "metabase"); }); @@ -577,7 +805,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); b.Property("ApplicationId") .HasColumnType("uuid"); @@ -590,6 +819,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(50) .HasColumnType("character varying(50)"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("CreationDate") .HasColumnType("timestamp with time zone"); @@ -621,6 +855,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(150) .HasColumnType("character varying(150)"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("Version") .IsConcurrencyToken() .ValueGeneratedOnAddOrUpdate() @@ -634,6 +873,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ReferenceId") .IsUnique(); + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + b.HasIndex("ApplicationId", "Status", "Subject", "Type"); b.ToTable("OpenIddictTokens", "metabase"); @@ -694,7 +936,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasDefaultValueSql("uuidv7()"); b.Property("AccessFailedCount") .HasColumnType("integer"); @@ -703,6 +946,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsConcurrencyToken() .HasColumnType("text"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("Email") .HasMaxLength(256) .HasColumnType("character varying(256)"); @@ -746,6 +994,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("TwoFactorEnabled") .HasColumnType("boolean"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("UserName") .HasMaxLength(256) .HasColumnType("character varying(256)"); @@ -768,6 +1021,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsUnique() .HasDatabaseName("UserNameIndex"); + b.HasIndex("CreatedAt", "Id") + .IsUnique(); + + b.HasIndex("Name", "Id") + .IsUnique(); + b.ToTable("user", "metabase"); }); @@ -824,9 +1083,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("MethodId") .HasColumnType("uuid"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("Pending") .HasColumnType("boolean"); + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + b.HasKey("UserId", "MethodId"); b.HasIndex("MethodId"); diff --git a/backend/src/Migrations/migrate.sql b/backend/src/Migrations/migrate.sql index 6809f8f15..35187c4c0 100644 --- a/backend/src/Migrations/migrate.sql +++ b/backend/src/Migrations/migrate.sql @@ -1,5 +1,3 @@ -\set ON_ERROR_STOP on - CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" ( "MigrationId" character varying(150) NOT NULL, "ProductVersion" character varying(32) NOT NULL, @@ -1968,3 +1966,919 @@ BEGIN END $EF$; COMMIT; +START TRANSACTION; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260328153447_CorrectExistsFlagsOfReferencesSecondAttempt') THEN + + UPDATE metabase.method + SET + "Reference_Standard_Exists" = CASE WHEN + "Reference_Standard_Title" IS NOT NULL OR + "Reference_Standard_Abstract" IS NOT NULL OR + "Reference_Standard_Section" IS NOT NULL OR + "Reference_Standard_Year" IS NOT NULL OR + "Reference_Standard_Numeration_Prefix" IS NOT NULL OR + "Reference_Standard_Numeration_MainNumber" IS NOT NULL OR + "Reference_Standard_Numeration_Suffix" IS NOT NULL OR + "Reference_Standard_Standardizers" IS NOT NULL OR + "Reference_Standard_Locator" IS NOT NULL + THEN TRUE + ELSE NULL + END, + "Reference_Publication_Exists" = CASE WHEN + "Reference_Publication_Title" IS NOT NULL OR + "Reference_Publication_Abstract" IS NOT NULL OR + "Reference_Publication_Section" IS NOT NULL OR + "Reference_Publication_Authors" IS NOT NULL OR + "Reference_Publication_Doi" IS NOT NULL OR + "Reference_Publication_ArXiv" IS NOT NULL OR + "Reference_Publication_Urn" IS NOT NULL OR + "Reference_Publication_WebAddress" IS NOT NULL + THEN TRUE + ELSE NULL + END, + "Reference_Exists" = CASE WHEN + "Reference_Standard_Title" IS NOT NULL OR + "Reference_Standard_Abstract" IS NOT NULL OR + "Reference_Standard_Section" IS NOT NULL OR + "Reference_Standard_Year" IS NOT NULL OR + "Reference_Standard_Numeration_Prefix" IS NOT NULL OR + "Reference_Standard_Numeration_MainNumber" IS NOT NULL OR + "Reference_Standard_Numeration_Suffix" IS NOT NULL OR + "Reference_Standard_Standardizers" IS NOT NULL OR + "Reference_Standard_Locator" IS NOT NULL OR + "Reference_Publication_Title" IS NOT NULL OR + "Reference_Publication_Abstract" IS NOT NULL OR + "Reference_Publication_Section" IS NOT NULL OR + "Reference_Publication_Authors" IS NOT NULL OR + "Reference_Publication_Doi" IS NOT NULL OR + "Reference_Publication_ArXiv" IS NOT NULL OR + "Reference_Publication_Urn" IS NOT NULL OR + "Reference_Publication_WebAddress" IS NOT NULL + THEN TRUE + ELSE NULL + END; + + UPDATE metabase.data_format + SET + "Reference_Standard_Exists" = CASE WHEN + "Reference_Standard_Title" IS NOT NULL OR + "Reference_Standard_Abstract" IS NOT NULL OR + "Reference_Standard_Section" IS NOT NULL OR + "Reference_Standard_Year" IS NOT NULL OR + "Reference_Standard_Numeration_Prefix" IS NOT NULL OR + "Reference_Standard_Numeration_MainNumber" IS NOT NULL OR + "Reference_Standard_Numeration_Suffix" IS NOT NULL OR + "Reference_Standard_Standardizers" IS NOT NULL OR + "Reference_Standard_Locator" IS NOT NULL + THEN TRUE + ELSE NULL + END, + "Reference_Publication_Exists" = CASE WHEN + "Reference_Publication_Title" IS NOT NULL OR + "Reference_Publication_Abstract" IS NOT NULL OR + "Reference_Publication_Section" IS NOT NULL OR + "Reference_Publication_Authors" IS NOT NULL OR + "Reference_Publication_Doi" IS NOT NULL OR + "Reference_Publication_ArXiv" IS NOT NULL OR + "Reference_Publication_Urn" IS NOT NULL OR + "Reference_Publication_WebAddress" IS NOT NULL + THEN TRUE + ELSE NULL + END, + "Reference_Exists" = CASE WHEN + "Reference_Standard_Title" IS NOT NULL OR + "Reference_Standard_Abstract" IS NOT NULL OR + "Reference_Standard_Section" IS NOT NULL OR + "Reference_Standard_Year" IS NOT NULL OR + "Reference_Standard_Numeration_Prefix" IS NOT NULL OR + "Reference_Standard_Numeration_MainNumber" IS NOT NULL OR + "Reference_Standard_Numeration_Suffix" IS NOT NULL OR + "Reference_Standard_Standardizers" IS NOT NULL OR + "Reference_Standard_Locator" IS NOT NULL OR + "Reference_Publication_Title" IS NOT NULL OR + "Reference_Publication_Abstract" IS NOT NULL OR + "Reference_Publication_Section" IS NOT NULL OR + "Reference_Publication_Authors" IS NOT NULL OR + "Reference_Publication_Doi" IS NOT NULL OR + "Reference_Publication_ArXiv" IS NOT NULL OR + "Reference_Publication_Urn" IS NOT NULL OR + "Reference_Publication_WebAddress" IS NOT NULL + THEN TRUE + ELSE NULL + END; + + UPDATE metabase.component + SET + "PrimeSurface_Reference_Standard_Exists" = CASE WHEN + "PrimeSurface_Reference_Standard_Title" IS NOT NULL OR + "PrimeSurface_Reference_Standard_Abstract" IS NOT NULL OR + "PrimeSurface_Reference_Standard_Section" IS NOT NULL OR + "PrimeSurface_Reference_Standard_Year" IS NOT NULL OR + "PrimeSurface_Reference_Standard_Numeration_Prefix" IS NOT NULL OR + "PrimeSurface_Reference_Standard_Numeration_MainNumber" IS NOT NULL OR + "PrimeSurface_Reference_Standard_Numeration_Suffix" IS NOT NULL OR + "PrimeSurface_Reference_Standard_Standardizers" IS NOT NULL OR + "PrimeSurface_Reference_Standard_Locator" IS NOT NULL + THEN TRUE + ELSE NULL + END, + "PrimeSurface_Reference_Publication_Exists" = CASE WHEN + "PrimeSurface_Reference_Publication_Title" IS NOT NULL OR + "PrimeSurface_Reference_Publication_Abstract" IS NOT NULL OR + "PrimeSurface_Reference_Publication_Section" IS NOT NULL OR + "PrimeSurface_Reference_Publication_Authors" IS NOT NULL OR + "PrimeSurface_Reference_Publication_Doi" IS NOT NULL OR + "PrimeSurface_Reference_Publication_ArXiv" IS NOT NULL OR + "PrimeSurface_Reference_Publication_Urn" IS NOT NULL OR + "PrimeSurface_Reference_Publication_WebAddress" IS NOT NULL + THEN TRUE + ELSE NULL + END, + "PrimeSurface_Reference_Exists" = CASE WHEN + "PrimeSurface_Reference_Standard_Title" IS NOT NULL OR + "PrimeSurface_Reference_Standard_Abstract" IS NOT NULL OR + "PrimeSurface_Reference_Standard_Section" IS NOT NULL OR + "PrimeSurface_Reference_Standard_Year" IS NOT NULL OR + "PrimeSurface_Reference_Standard_Numeration_Prefix" IS NOT NULL OR + "PrimeSurface_Reference_Standard_Numeration_MainNumber" IS NOT NULL OR + "PrimeSurface_Reference_Standard_Numeration_Suffix" IS NOT NULL OR + "PrimeSurface_Reference_Standard_Standardizers" IS NOT NULL OR + "PrimeSurface_Reference_Standard_Locator" IS NOT NULL OR + "PrimeSurface_Reference_Publication_Title" IS NOT NULL OR + "PrimeSurface_Reference_Publication_Abstract" IS NOT NULL OR + "PrimeSurface_Reference_Publication_Section" IS NOT NULL OR + "PrimeSurface_Reference_Publication_Authors" IS NOT NULL OR + "PrimeSurface_Reference_Publication_Doi" IS NOT NULL OR + "PrimeSurface_Reference_Publication_ArXiv" IS NOT NULL OR + "PrimeSurface_Reference_Publication_Urn" IS NOT NULL OR + "PrimeSurface_Reference_Publication_WebAddress" IS NOT NULL + THEN TRUE + ELSE NULL + END, + "PrimeSurface_Exists" = CASE WHEN + "PrimeSurface_Description" IS NOT NULL OR + "PrimeSurface_Reference_Standard_Title" IS NOT NULL OR + "PrimeSurface_Reference_Standard_Abstract" IS NOT NULL OR + "PrimeSurface_Reference_Standard_Section" IS NOT NULL OR + "PrimeSurface_Reference_Standard_Year" IS NOT NULL OR + "PrimeSurface_Reference_Standard_Numeration_Prefix" IS NOT NULL OR + "PrimeSurface_Reference_Standard_Numeration_MainNumber" IS NOT NULL OR + "PrimeSurface_Reference_Standard_Numeration_Suffix" IS NOT NULL OR + "PrimeSurface_Reference_Standard_Standardizers" IS NOT NULL OR + "PrimeSurface_Reference_Standard_Locator" IS NOT NULL OR + "PrimeSurface_Reference_Publication_Title" IS NOT NULL OR + "PrimeSurface_Reference_Publication_Abstract" IS NOT NULL OR + "PrimeSurface_Reference_Publication_Section" IS NOT NULL OR + "PrimeSurface_Reference_Publication_Authors" IS NOT NULL OR + "PrimeSurface_Reference_Publication_Doi" IS NOT NULL OR + "PrimeSurface_Reference_Publication_ArXiv" IS NOT NULL OR + "PrimeSurface_Reference_Publication_Urn" IS NOT NULL OR + "PrimeSurface_Reference_Publication_WebAddress" IS NOT NULL + THEN TRUE + ELSE NULL + END, + + "PrimeDirection_Reference_Standard_Exists" = CASE WHEN + "PrimeDirection_Reference_Standard_Title" IS NOT NULL OR + "PrimeDirection_Reference_Standard_Abstract" IS NOT NULL OR + "PrimeDirection_Reference_Standard_Section" IS NOT NULL OR + "PrimeDirection_Reference_Standard_Year" IS NOT NULL OR + "PrimeDirection_Reference_Standard_Numeration_Prefix" IS NOT NULL OR + "PrimeDirection_Reference_Standard_Numeration_MainNumber" IS NOT NULL OR + "PrimeDirection_Reference_Standard_Numeration_Suffix" IS NOT NULL OR + "PrimeDirection_Reference_Standard_Standardizers" IS NOT NULL OR + "PrimeDirection_Reference_Standard_Locator" IS NOT NULL + THEN TRUE + ELSE NULL + END, + "PrimeDirection_Reference_Publication_Exists" = CASE WHEN + "PrimeDirection_Reference_Publication_Title" IS NOT NULL OR + "PrimeDirection_Reference_Publication_Abstract" IS NOT NULL OR + "PrimeDirection_Reference_Publication_Section" IS NOT NULL OR + "PrimeDirection_Reference_Publication_Authors" IS NOT NULL OR + "PrimeDirection_Reference_Publication_Doi" IS NOT NULL OR + "PrimeDirection_Reference_Publication_ArXiv" IS NOT NULL OR + "PrimeDirection_Reference_Publication_Urn" IS NOT NULL OR + "PrimeDirection_Reference_Publication_WebAddress" IS NOT NULL + THEN TRUE + ELSE NULL + END, + "PrimeDirection_Reference_Exists" = CASE WHEN + "PrimeDirection_Reference_Standard_Title" IS NOT NULL OR + "PrimeDirection_Reference_Standard_Abstract" IS NOT NULL OR + "PrimeDirection_Reference_Standard_Section" IS NOT NULL OR + "PrimeDirection_Reference_Standard_Year" IS NOT NULL OR + "PrimeDirection_Reference_Standard_Numeration_Prefix" IS NOT NULL OR + "PrimeDirection_Reference_Standard_Numeration_MainNumber" IS NOT NULL OR + "PrimeDirection_Reference_Standard_Numeration_Suffix" IS NOT NULL OR + "PrimeDirection_Reference_Standard_Standardizers" IS NOT NULL OR + "PrimeDirection_Reference_Standard_Locator" IS NOT NULL OR + "PrimeDirection_Reference_Publication_Title" IS NOT NULL OR + "PrimeDirection_Reference_Publication_Abstract" IS NOT NULL OR + "PrimeDirection_Reference_Publication_Section" IS NOT NULL OR + "PrimeDirection_Reference_Publication_Authors" IS NOT NULL OR + "PrimeDirection_Reference_Publication_Doi" IS NOT NULL OR + "PrimeDirection_Reference_Publication_ArXiv" IS NOT NULL OR + "PrimeDirection_Reference_Publication_Urn" IS NOT NULL OR + "PrimeDirection_Reference_Publication_WebAddress" IS NOT NULL + THEN TRUE + ELSE NULL + END, + "PrimeDirection_Exists" = CASE WHEN + "PrimeDirection_Description" IS NOT NULL OR + "PrimeDirection_Reference_Standard_Title" IS NOT NULL OR + "PrimeDirection_Reference_Standard_Abstract" IS NOT NULL OR + "PrimeDirection_Reference_Standard_Section" IS NOT NULL OR + "PrimeDirection_Reference_Standard_Year" IS NOT NULL OR + "PrimeDirection_Reference_Standard_Numeration_Prefix" IS NOT NULL OR + "PrimeDirection_Reference_Standard_Numeration_MainNumber" IS NOT NULL OR + "PrimeDirection_Reference_Standard_Numeration_Suffix" IS NOT NULL OR + "PrimeDirection_Reference_Standard_Standardizers" IS NOT NULL OR + "PrimeDirection_Reference_Standard_Locator" IS NOT NULL OR + "PrimeDirection_Reference_Publication_Title" IS NOT NULL OR + "PrimeDirection_Reference_Publication_Abstract" IS NOT NULL OR + "PrimeDirection_Reference_Publication_Section" IS NOT NULL OR + "PrimeDirection_Reference_Publication_Authors" IS NOT NULL OR + "PrimeDirection_Reference_Publication_Doi" IS NOT NULL OR + "PrimeDirection_Reference_Publication_ArXiv" IS NOT NULL OR + "PrimeDirection_Reference_Publication_Urn" IS NOT NULL OR + "PrimeDirection_Reference_Publication_WebAddress" IS NOT NULL + THEN TRUE + ELSE NULL + END, + + "SwitchableLayers_Reference_Standard_Exists" = CASE WHEN + "SwitchableLayers_Reference_Standard_Title" IS NOT NULL OR + "SwitchableLayers_Reference_Standard_Abstract" IS NOT NULL OR + "SwitchableLayers_Reference_Standard_Section" IS NOT NULL OR + "SwitchableLayers_Reference_Standard_Year" IS NOT NULL OR + "SwitchableLayers_Reference_Standard_Numeration_Prefix" IS NOT NULL OR + "SwitchableLayers_Reference_Standard_Numeration_MainNumber" IS NOT NULL OR + "SwitchableLayers_Reference_Standard_Numeration_Suffix" IS NOT NULL OR + "SwitchableLayers_Reference_Standard_Standardizers" IS NOT NULL OR + "SwitchableLayers_Reference_Standard_Locator" IS NOT NULL + THEN TRUE + ELSE NULL + END, + "SwitchableLayers_Reference_Publication_Exists" = CASE WHEN + "SwitchableLayers_Reference_Publication_Title" IS NOT NULL OR + "SwitchableLayers_Reference_Publication_Abstract" IS NOT NULL OR + "SwitchableLayers_Reference_Publication_Section" IS NOT NULL OR + "SwitchableLayers_Reference_Publication_Authors" IS NOT NULL OR + "SwitchableLayers_Reference_Publication_Doi" IS NOT NULL OR + "SwitchableLayers_Reference_Publication_ArXiv" IS NOT NULL OR + "SwitchableLayers_Reference_Publication_Urn" IS NOT NULL OR + "SwitchableLayers_Reference_Publication_WebAddress" IS NOT NULL + THEN TRUE + ELSE NULL + END, + "SwitchableLayers_Reference_Exists" = CASE WHEN + "SwitchableLayers_Reference_Standard_Title" IS NOT NULL OR + "SwitchableLayers_Reference_Standard_Abstract" IS NOT NULL OR + "SwitchableLayers_Reference_Standard_Section" IS NOT NULL OR + "SwitchableLayers_Reference_Standard_Year" IS NOT NULL OR + "SwitchableLayers_Reference_Standard_Numeration_Prefix" IS NOT NULL OR + "SwitchableLayers_Reference_Standard_Numeration_MainNumber" IS NOT NULL OR + "SwitchableLayers_Reference_Standard_Numeration_Suffix" IS NOT NULL OR + "SwitchableLayers_Reference_Standard_Standardizers" IS NOT NULL OR + "SwitchableLayers_Reference_Standard_Locator" IS NOT NULL OR + "SwitchableLayers_Reference_Publication_Title" IS NOT NULL OR + "SwitchableLayers_Reference_Publication_Abstract" IS NOT NULL OR + "SwitchableLayers_Reference_Publication_Section" IS NOT NULL OR + "SwitchableLayers_Reference_Publication_Authors" IS NOT NULL OR + "SwitchableLayers_Reference_Publication_Doi" IS NOT NULL OR + "SwitchableLayers_Reference_Publication_ArXiv" IS NOT NULL OR + "SwitchableLayers_Reference_Publication_Urn" IS NOT NULL OR + "SwitchableLayers_Reference_Publication_WebAddress" IS NOT NULL + THEN TRUE + ELSE NULL + END, + "SwitchableLayers_Exists" = CASE WHEN + "SwitchableLayers_Description" IS NOT NULL OR + "SwitchableLayers_Reference_Standard_Title" IS NOT NULL OR + "SwitchableLayers_Reference_Standard_Abstract" IS NOT NULL OR + "SwitchableLayers_Reference_Standard_Section" IS NOT NULL OR + "SwitchableLayers_Reference_Standard_Year" IS NOT NULL OR + "SwitchableLayers_Reference_Standard_Numeration_Prefix" IS NOT NULL OR + "SwitchableLayers_Reference_Standard_Numeration_MainNumber" IS NOT NULL OR + "SwitchableLayers_Reference_Standard_Numeration_Suffix" IS NOT NULL OR + "SwitchableLayers_Reference_Standard_Standardizers" IS NOT NULL OR + "SwitchableLayers_Reference_Standard_Locator" IS NOT NULL OR + "SwitchableLayers_Reference_Publication_Title" IS NOT NULL OR + "SwitchableLayers_Reference_Publication_Abstract" IS NOT NULL OR + "SwitchableLayers_Reference_Publication_Section" IS NOT NULL OR + "SwitchableLayers_Reference_Publication_Authors" IS NOT NULL OR + "SwitchableLayers_Reference_Publication_Doi" IS NOT NULL OR + "SwitchableLayers_Reference_Publication_ArXiv" IS NOT NULL OR + "SwitchableLayers_Reference_Publication_Urn" IS NOT NULL OR + "SwitchableLayers_Reference_Publication_WebAddress" IS NOT NULL + THEN TRUE + ELSE NULL + END; + + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260328153447_CorrectExistsFlagsOfReferencesSecondAttempt') THEN + INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") + VALUES ('20260328153447_CorrectExistsFlagsOfReferencesSecondAttempt', '10.0.5'); + END IF; +END $EF$; +COMMIT; + +START TRANSACTION; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.user_method_developer 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.user_method_developer 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."user" ALTER COLUMN "Id" SET DEFAULT (gen_random_uuid()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictTokens" ALTER COLUMN "Id" SET DEFAULT (gen_random_uuid()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictTokens" 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictTokens" 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictScopes" ALTER COLUMN "Id" SET DEFAULT (gen_random_uuid()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictScopes" ADD "CreatedAt" timestamp with time zone NOT NULL DEFAULT TIMESTAMPTZ '1970-01-01T00:00:00Z'; + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictScopes" ADD "UpdatedAt" timestamp with time zone NOT NULL DEFAULT TIMESTAMPTZ '1970-01-01T00:00:00Z'; + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictAuthorizations" ALTER COLUMN "Id" SET DEFAULT (gen_random_uuid()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictAuthorizations" 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictAuthorizations" 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictApplications" ALTER COLUMN "Id" SET DEFAULT (gen_random_uuid()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictApplications" 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictApplications" 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.method 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.method 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.institution_representative 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.institution_representative 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.institution_method_developer 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.institution_method_developer 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.institution 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.institution 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.gnu_pg_fingerprint ALTER COLUMN "CreatedAt" SET DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.gnu_pg_fingerprint 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.database 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.database 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.data_format 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.data_format 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component_variant 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component_variant 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component_manufacturer 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component_manufacturer 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component_concretization_and_generalization 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component_concretization_and_generalization 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component_assembly 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component_assembly 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") + VALUES ('20260402204629_AddUpdatedAndCreatedAtTimestamps', '10.0.5'); + END IF; +END $EF$; +COMMIT; + +START TRANSACTION; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + ALTER TABLE metabase."OpenIddictScopes" ALTER COLUMN "UpdatedAt" SET DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + ALTER TABLE metabase."OpenIddictScopes" ALTER COLUMN "CreatedAt" SET DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_user_CreatedAt_Id" ON metabase."user" ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_user_Name_Id" ON metabase."user" ("Name", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_OpenIddictTokens_CreatedAt_Id" ON metabase."OpenIddictTokens" ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_OpenIddictScopes_CreatedAt_Id" ON metabase."OpenIddictScopes" ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_OpenIddictAuthorizations_CreatedAt_Id" ON metabase."OpenIddictAuthorizations" ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_OpenIddictApplications_CreatedAt_Id" ON metabase."OpenIddictApplications" ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_method_CreatedAt_Id" ON metabase.method ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_method_Name_Id" ON metabase.method ("Name", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_institution_CreatedAt_Id" ON metabase.institution ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_institution_Name_Id" ON metabase.institution ("Name", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_gnu_pg_fingerprint_CreatedAt_Id" ON metabase.gnu_pg_fingerprint ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_database_CreatedAt_Id" ON metabase.database ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_database_Name_Id" ON metabase.database ("Name", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_data_format_CreatedAt_Id" ON metabase.data_format ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_data_format_Name_Id" ON metabase.data_format ("Name", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_component_CreatedAt_Id" ON metabase.component ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_component_Name_Id" ON metabase.component ("Name", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") + VALUES ('20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations', '10.0.7'); + END IF; +END $EF$; +COMMIT; + +START TRANSACTION; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication') THEN + UPDATE metabase."OpenIddictApplications" + SET "Permissions" = ("Permissions"::jsonb || '["scp:api:support"]'::jsonb)::text + WHERE "ClientId" = 'metabase'; + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication') THEN + INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") + VALUES ('20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication', '10.0.7'); + END IF; +END $EF$; + +COMMIT; + +START TRANSACTION; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit') THEN + UPDATE metabase."OpenIddictApplications" + SET "ConsentType" = 'implicit' + WHERE "ClientId" = 'metabase'; + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit') THEN + INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") + VALUES ('20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit', '10.0.8'); + END IF; +END $EF$; + +COMMIT; + +START TRANSACTION; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase."user" ALTER COLUMN "Id" SET DEFAULT (uuidv7()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase."OpenIddictTokens" ALTER COLUMN "Id" SET DEFAULT (uuidv7()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase."OpenIddictScopes" ALTER COLUMN "Id" SET DEFAULT (uuidv7()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase."OpenIddictAuthorizations" ALTER COLUMN "Id" SET DEFAULT (uuidv7()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase."OpenIddictApplications" ALTER COLUMN "Id" SET DEFAULT (uuidv7()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase.method ALTER COLUMN "Id" SET DEFAULT (uuidv7()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase.institution ALTER COLUMN "Id" SET DEFAULT (uuidv7()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase.gnu_pg_fingerprint ALTER COLUMN "Id" SET DEFAULT (uuidv7()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase.database ALTER COLUMN "Id" SET DEFAULT (uuidv7()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase.data_format ALTER COLUMN "Id" SET DEFAULT (uuidv7()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase.component ALTER COLUMN "Id" SET DEFAULT (uuidv7()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") + VALUES ('20260620182947_UseSequentialUuids', '10.0.9'); + END IF; +END $EF$; +COMMIT; + +START TRANSACTION; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260625115427_DropContactExistsColumnFromInstitution') THEN + ALTER TABLE metabase.institution DROP COLUMN IF EXISTS "Contact_Exists"; + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260625115427_DropContactExistsColumnFromInstitution') THEN + INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") + VALUES ('20260625115427_DropContactExistsColumnFromInstitution', '10.0.9'); + END IF; +END $EF$; + +COMMIT; + diff --git a/backend/src/Migrations/migrate_from_20260328153447_CorrectExistsFlagsOfReferencesSecondAttempt_to_20260402204629_AddUpdatedAndCreatedAtTimestamps.sql b/backend/src/Migrations/migrate_from_20260328153447_CorrectExistsFlagsOfReferencesSecondAttempt_to_20260402204629_AddUpdatedAndCreatedAtTimestamps.sql new file mode 100644 index 000000000..196721d63 --- /dev/null +++ b/backend/src/Migrations/migrate_from_20260328153447_CorrectExistsFlagsOfReferencesSecondAttempt_to_20260402204629_AddUpdatedAndCreatedAtTimestamps.sql @@ -0,0 +1,298 @@ +START TRANSACTION; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.user_method_developer 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.user_method_developer 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."user" ALTER COLUMN "Id" SET DEFAULT (gen_random_uuid()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictTokens" ALTER COLUMN "Id" SET DEFAULT (gen_random_uuid()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictTokens" 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictTokens" 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictScopes" ALTER COLUMN "Id" SET DEFAULT (gen_random_uuid()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictScopes" ADD "CreatedAt" timestamp with time zone NOT NULL DEFAULT TIMESTAMPTZ '1970-01-01T00:00:00Z'; + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictScopes" ADD "UpdatedAt" timestamp with time zone NOT NULL DEFAULT TIMESTAMPTZ '1970-01-01T00:00:00Z'; + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictAuthorizations" ALTER COLUMN "Id" SET DEFAULT (gen_random_uuid()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictAuthorizations" 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictAuthorizations" 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictApplications" ALTER COLUMN "Id" SET DEFAULT (gen_random_uuid()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictApplications" 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictApplications" 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.method 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.method 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.institution_representative 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.institution_representative 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.institution_method_developer 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.institution_method_developer 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.institution 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.institution 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.gnu_pg_fingerprint ALTER COLUMN "CreatedAt" SET DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.gnu_pg_fingerprint 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.database 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.database 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.data_format 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.data_format 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component_variant 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component_variant 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component_manufacturer 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component_manufacturer 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component_concretization_and_generalization 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component_concretization_and_generalization 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component_assembly 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component_assembly 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component 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" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") + VALUES ('20260402204629_AddUpdatedAndCreatedAtTimestamps', '10.0.5'); + END IF; +END $EF$; +COMMIT; + diff --git a/backend/src/Migrations/migrate_from_20260402204629_AddUpdatedAndCreatedAtTimestamps_to_20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations.sql b/backend/src/Migrations/migrate_from_20260402204629_AddUpdatedAndCreatedAtTimestamps_to_20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations.sql new file mode 100644 index 000000000..41b1621d3 --- /dev/null +++ b/backend/src/Migrations/migrate_from_20260402204629_AddUpdatedAndCreatedAtTimestamps_to_20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations.sql @@ -0,0 +1,144 @@ +START TRANSACTION; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + ALTER TABLE metabase."OpenIddictScopes" ALTER COLUMN "UpdatedAt" SET DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + ALTER TABLE metabase."OpenIddictScopes" ALTER COLUMN "CreatedAt" SET DEFAULT (now()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_user_CreatedAt_Id" ON metabase."user" ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_user_Name_Id" ON metabase."user" ("Name", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_OpenIddictTokens_CreatedAt_Id" ON metabase."OpenIddictTokens" ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_OpenIddictScopes_CreatedAt_Id" ON metabase."OpenIddictScopes" ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_OpenIddictAuthorizations_CreatedAt_Id" ON metabase."OpenIddictAuthorizations" ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_OpenIddictApplications_CreatedAt_Id" ON metabase."OpenIddictApplications" ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_method_CreatedAt_Id" ON metabase.method ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_method_Name_Id" ON metabase.method ("Name", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_institution_CreatedAt_Id" ON metabase.institution ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_institution_Name_Id" ON metabase.institution ("Name", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_gnu_pg_fingerprint_CreatedAt_Id" ON metabase.gnu_pg_fingerprint ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_database_CreatedAt_Id" ON metabase.database ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_database_Name_Id" ON metabase.database ("Name", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_data_format_CreatedAt_Id" ON metabase.data_format ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_data_format_Name_Id" ON metabase.data_format ("Name", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_component_CreatedAt_Id" ON metabase.component ("CreatedAt", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + CREATE UNIQUE INDEX "IX_component_Name_Id" ON metabase.component ("Name", "Id"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") + VALUES ('20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations', '10.0.7'); + END IF; +END $EF$; +COMMIT; + diff --git a/backend/src/Migrations/migrate_from_20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations_to_20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication.sql b/backend/src/Migrations/migrate_from_20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations_to_20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication.sql new file mode 100644 index 000000000..bda8484f9 --- /dev/null +++ b/backend/src/Migrations/migrate_from_20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations_to_20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication.sql @@ -0,0 +1,21 @@ +START TRANSACTION; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication') THEN + UPDATE metabase."OpenIddictApplications" + SET "Permissions" = ("Permissions"::jsonb || '["scp:api:support"]'::jsonb)::text + WHERE "ClientId" = 'metabase'; + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication') THEN + INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") + VALUES ('20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication', '10.0.7'); + END IF; +END $EF$; + +COMMIT; + diff --git a/backend/src/Migrations/migrate_from_20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication_to_20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit.sql b/backend/src/Migrations/migrate_from_20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication_to_20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit.sql new file mode 100644 index 000000000..36554cd1d --- /dev/null +++ b/backend/src/Migrations/migrate_from_20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication_to_20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit.sql @@ -0,0 +1,20 @@ +START TRANSACTION; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit') THEN + UPDATE metabase."OpenIddictApplications" + SET "ConsentType" = 'implicit' + WHERE "ClientId" = 'metabase'; + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit') THEN + INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") + VALUES ('20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit', '10.0.8'); + END IF; +END $EF$; +COMMIT; + diff --git a/backend/src/Migrations/migrate_from_20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit_to_20260620182947_UseSequentialUuids.sql b/backend/src/Migrations/migrate_from_20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit_to_20260620182947_UseSequentialUuids.sql new file mode 100644 index 000000000..a3ea354e8 --- /dev/null +++ b/backend/src/Migrations/migrate_from_20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit_to_20260620182947_UseSequentialUuids.sql @@ -0,0 +1,88 @@ +START TRANSACTION; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase."user" ALTER COLUMN "Id" SET DEFAULT (uuidv7()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase."OpenIddictTokens" ALTER COLUMN "Id" SET DEFAULT (uuidv7()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase."OpenIddictScopes" ALTER COLUMN "Id" SET DEFAULT (uuidv7()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase."OpenIddictAuthorizations" ALTER COLUMN "Id" SET DEFAULT (uuidv7()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase."OpenIddictApplications" ALTER COLUMN "Id" SET DEFAULT (uuidv7()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase.method ALTER COLUMN "Id" SET DEFAULT (uuidv7()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase.institution ALTER COLUMN "Id" SET DEFAULT (uuidv7()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase.gnu_pg_fingerprint ALTER COLUMN "Id" SET DEFAULT (uuidv7()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase.database ALTER COLUMN "Id" SET DEFAULT (uuidv7()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase.data_format ALTER COLUMN "Id" SET DEFAULT (uuidv7()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase.component ALTER COLUMN "Id" SET DEFAULT (uuidv7()); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") + VALUES ('20260620182947_UseSequentialUuids', '10.0.9'); + END IF; +END $EF$; +COMMIT; + diff --git a/backend/src/Migrations/migrate_from_20260620182947_UseSequentialUuids_to_20260625115427_DropContactExistsColumnFromInstitution.sql b/backend/src/Migrations/migrate_from_20260620182947_UseSequentialUuids_to_20260625115427_DropContactExistsColumnFromInstitution.sql new file mode 100644 index 000000000..0a29c53b8 --- /dev/null +++ b/backend/src/Migrations/migrate_from_20260620182947_UseSequentialUuids_to_20260625115427_DropContactExistsColumnFromInstitution.sql @@ -0,0 +1,19 @@ +START TRANSACTION; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260625115427_DropContactExistsColumnFromInstitution') THEN + ALTER TABLE metabase.institution DROP COLUMN IF EXISTS "Contact_Exists"; + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260625115427_DropContactExistsColumnFromInstitution') THEN + INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") + VALUES ('20260625115427_DropContactExistsColumnFromInstitution', '10.0.9'); + END IF; +END $EF$; + +COMMIT; + diff --git a/backend/src/Migrations/rollback_from_20260402204629_AddUpdatedAndCreatedAtTimestamps_to_20260328153447_CorrectExistsFlagsOfReferencesSecondAttempt.sql b/backend/src/Migrations/rollback_from_20260402204629_AddUpdatedAndCreatedAtTimestamps_to_20260328153447_CorrectExistsFlagsOfReferencesSecondAttempt.sql new file mode 100644 index 000000000..b4f10ea43 --- /dev/null +++ b/backend/src/Migrations/rollback_from_20260402204629_AddUpdatedAndCreatedAtTimestamps_to_20260328153447_CorrectExistsFlagsOfReferencesSecondAttempt.sql @@ -0,0 +1,256 @@ +START TRANSACTION; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.user_method_developer DROP COLUMN "CreatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.user_method_developer DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."user" DROP COLUMN "CreatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."user" DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictTokens" DROP COLUMN "CreatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictTokens" DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictScopes" DROP COLUMN "CreatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictScopes" DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictAuthorizations" DROP COLUMN "CreatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictAuthorizations" DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictApplications" DROP COLUMN "CreatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictApplications" DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.method DROP COLUMN "CreatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.method DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.institution_representative DROP COLUMN "CreatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.institution_representative DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.institution_method_developer DROP COLUMN "CreatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.institution_method_developer DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.institution DROP COLUMN "CreatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.institution DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.gnu_pg_fingerprint DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.database DROP COLUMN "CreatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.database DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.data_format DROP COLUMN "CreatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.data_format DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component_variant DROP COLUMN "CreatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component_variant DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component_manufacturer DROP COLUMN "CreatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component_manufacturer DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component_concretization_and_generalization DROP COLUMN "CreatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component_concretization_and_generalization DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component_assembly DROP COLUMN "CreatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component_assembly DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component DROP COLUMN "CreatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.component DROP COLUMN "UpdatedAt"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."user" ALTER COLUMN "Id" DROP DEFAULT; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictTokens" ALTER COLUMN "Id" DROP DEFAULT; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictScopes" ALTER COLUMN "Id" DROP DEFAULT; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictAuthorizations" ALTER COLUMN "Id" DROP DEFAULT; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase."OpenIddictApplications" ALTER COLUMN "Id" DROP DEFAULT; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + ALTER TABLE metabase.gnu_pg_fingerprint ALTER COLUMN "CreatedAt" DROP DEFAULT; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps') THEN + DELETE FROM "__EFMigrationsHistory" + WHERE "MigrationId" = '20260402204629_AddUpdatedAndCreatedAtTimestamps'; + END IF; +END $EF$; +COMMIT; + diff --git a/backend/src/Migrations/rollback_from_20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations_to_20260402204629_AddUpdatedAndCreatedAtTimestamps.sql b/backend/src/Migrations/rollback_from_20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations_to_20260402204629_AddUpdatedAndCreatedAtTimestamps.sql new file mode 100644 index 000000000..119d9f8b9 --- /dev/null +++ b/backend/src/Migrations/rollback_from_20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations_to_20260402204629_AddUpdatedAndCreatedAtTimestamps.sql @@ -0,0 +1,124 @@ +START TRANSACTION; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + DROP INDEX metabase."IX_user_CreatedAt_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + DROP INDEX metabase."IX_user_Name_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + DROP INDEX metabase."IX_OpenIddictTokens_CreatedAt_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + DROP INDEX metabase."IX_OpenIddictScopes_CreatedAt_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + DROP INDEX metabase."IX_OpenIddictAuthorizations_CreatedAt_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + DROP INDEX metabase."IX_OpenIddictApplications_CreatedAt_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + DROP INDEX metabase."IX_method_CreatedAt_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + DROP INDEX metabase."IX_method_Name_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + DROP INDEX metabase."IX_institution_CreatedAt_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + DROP INDEX metabase."IX_institution_Name_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + DROP INDEX metabase."IX_gnu_pg_fingerprint_CreatedAt_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + DROP INDEX metabase."IX_database_CreatedAt_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + DROP INDEX metabase."IX_database_Name_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + DROP INDEX metabase."IX_data_format_CreatedAt_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + DROP INDEX metabase."IX_data_format_Name_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + DROP INDEX metabase."IX_component_CreatedAt_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + DROP INDEX metabase."IX_component_Name_Id"; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + ALTER TABLE metabase."OpenIddictScopes" ALTER COLUMN "UpdatedAt" DROP DEFAULT; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + ALTER TABLE metabase."OpenIddictScopes" ALTER COLUMN "CreatedAt" DROP DEFAULT; + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations') THEN + DELETE FROM "__EFMigrationsHistory" + WHERE "MigrationId" = '20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations'; + END IF; +END $EF$; +COMMIT; + diff --git a/backend/src/Migrations/rollback_from_20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication_to_20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations.sql b/backend/src/Migrations/rollback_from_20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication_to_20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations.sql new file mode 100644 index 000000000..922ee8a1d --- /dev/null +++ b/backend/src/Migrations/rollback_from_20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication_to_20260508172623_AddIndicesForNameAndCreatedAtAndAtDefaultValuesForAuditableEntitiesAndAssociations.sql @@ -0,0 +1,21 @@ +START TRANSACTION; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication') THEN + UPDATE metabase."OpenIddictApplications" + SET "Permissions" = ("Permissions"::jsonb - 'scp:api:support')::text + WHERE "ClientId" = 'metabase'; + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication') THEN + DELETE FROM "__EFMigrationsHistory" + WHERE "MigrationId" = '20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication'; + END IF; +END $EF$; + +COMMIT; + diff --git a/backend/src/Migrations/rollback_from_20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit_to_20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication.sql b/backend/src/Migrations/rollback_from_20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit_to_20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication.sql new file mode 100644 index 000000000..ac05aceb9 --- /dev/null +++ b/backend/src/Migrations/rollback_from_20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit_to_20260511201930_AddCustomerSupportScopeToMetabaseOpenIdConnectClientApplication.sql @@ -0,0 +1,21 @@ +START TRANSACTION; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit') THEN + UPDATE metabase."OpenIddictApplications" + SET "ConsentType" = 'explicit' + WHERE "ClientId" = 'metabase'; + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit') THEN + DELETE FROM "__EFMigrationsHistory" + WHERE "MigrationId" = '20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit'; + END IF; +END $EF$; + +COMMIT; + diff --git a/backend/src/Migrations/rollback_from_20260620182947_UseSequentialUuids_to_20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit.sql b/backend/src/Migrations/rollback_from_20260620182947_UseSequentialUuids_to_20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit.sql new file mode 100644 index 000000000..74d1d8394 --- /dev/null +++ b/backend/src/Migrations/rollback_from_20260620182947_UseSequentialUuids_to_20260513180044_SetConsentTypeOfMetabaseOpenIdConnectClientToImplicit.sql @@ -0,0 +1,76 @@ +START TRANSACTION; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase."user" ALTER COLUMN "Id" SET DEFAULT (gen_random_uuid()); + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase."OpenIddictTokens" ALTER COLUMN "Id" SET DEFAULT (gen_random_uuid()); + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase."OpenIddictScopes" ALTER COLUMN "Id" SET DEFAULT (gen_random_uuid()); + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase."OpenIddictAuthorizations" ALTER COLUMN "Id" SET DEFAULT (gen_random_uuid()); + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase."OpenIddictApplications" ALTER COLUMN "Id" SET DEFAULT (gen_random_uuid()); + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase.method ALTER COLUMN "Id" SET DEFAULT (gen_random_uuid()); + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase.institution ALTER COLUMN "Id" SET DEFAULT (gen_random_uuid()); + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase.gnu_pg_fingerprint ALTER COLUMN "Id" SET DEFAULT (gen_random_uuid()); + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase.database ALTER COLUMN "Id" SET DEFAULT (gen_random_uuid()); + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase.data_format ALTER COLUMN "Id" SET DEFAULT (gen_random_uuid()); + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + ALTER TABLE metabase.component ALTER COLUMN "Id" SET DEFAULT (gen_random_uuid()); + END IF; +END $EF$; +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260620182947_UseSequentialUuids') THEN + DELETE FROM "__EFMigrationsHistory" + WHERE "MigrationId" = '20260620182947_UseSequentialUuids'; + END IF; +END $EF$; +COMMIT; + diff --git a/backend/src/Migrations/rollback_from_20260625115427_DropContactExistsColumnFromInstitution_to_20260620182947_UseSequentialUuids.sql b/backend/src/Migrations/rollback_from_20260625115427_DropContactExistsColumnFromInstitution_to_20260620182947_UseSequentialUuids.sql new file mode 100644 index 000000000..adb801aa7 --- /dev/null +++ b/backend/src/Migrations/rollback_from_20260625115427_DropContactExistsColumnFromInstitution_to_20260620182947_UseSequentialUuids.sql @@ -0,0 +1,19 @@ +START TRANSACTION; + +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260625115427_DropContactExistsColumnFromInstitution') THEN + ALTER TABLE metabase.institution ADD "Contact_Exists" boolean; + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20260625115427_DropContactExistsColumnFromInstitution') THEN + DELETE FROM "__EFMigrationsHistory" + WHERE "MigrationId" = '20260625115427_DropContactExistsColumnFromInstitution'; + END IF; +END $EF$; + +COMMIT; + diff --git a/backend/src/Program.cs b/backend/src/Program.cs index 87aafa794..1e30caa1c 100644 --- a/backend/src/Program.cs +++ b/backend/src/Program.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Metabase.Data; using Microsoft.AspNetCore.Builder; @@ -45,7 +46,9 @@ Exception exception public sealed class Program { public const string TestEnvironment = "test"; + public const string DevelopmentEnvironment = "development"; private const string ProductionEnvironment = "production"; + private const string LogsPath = "./logs/serilog.json"; public static async Task Main( @@ -79,20 +82,25 @@ string[] commandLineArguments startup.Configure(application); using (var scope = application.Services.CreateScope()) { + var lifetime = scope.ServiceProvider.GetRequiredService(); if (!builder.Environment.IsEnvironment(TestEnvironment)) { - EnsureDatabaseIsUpToDate(scope.ServiceProvider); + await EnsureDatabaseIsUpToDateAsync(scope.ServiceProvider, lifetime.ApplicationStopping); // Inspired by https://docs.microsoft.com/en-us/aspnet/core/data/ef-mvc/intro#initialize-db-with-test-data } if (builder.Environment.IsEnvironment(TestEnvironment)) { - await CreateDatabase(scope.ServiceProvider); + await CreateDatabaseAsync(scope.ServiceProvider, lifetime.ApplicationStopping); } - await SeedDatabase(scope.ServiceProvider); + await SeedDatabaseAsync(scope.ServiceProvider, lifetime.ApplicationStopping); } - - await application.RunAsync(); - return 0; + // dotnet run -- schema export --output ./schema.graphql + return await application.RunWithGraphQLCommandsAsync(commandLineArguments); + } + catch (OperationCanceledException exception) + { + Serilog.Log.Information(exception, "[System] App execution cancelled cleanly."); + return 1; } catch (Exception exception) when (exception is not HostAbortedException && exception.Source != "Microsoft.EntityFrameworkCore.Design") // see https://github.com/dotnet/efcore/issues/29923 { @@ -154,30 +162,32 @@ Uri openTelemetryHost } } - private static void EnsureDatabaseIsUpToDate( - IServiceProvider services + private static async Task EnsureDatabaseIsUpToDateAsync( + IServiceProvider services, + CancellationToken cancellationToken ) { - using var dbContext = - services.GetRequiredService>() - .CreateDbContext(); - var pendingMigrations = dbContext.Database.GetPendingMigrations(); + await using var databaseContext = + await services.GetRequiredService>() + .CreateDbContextAsync(cancellationToken); + var pendingMigrations = await databaseContext.Database.GetPendingMigrationsAsync(cancellationToken); if (pendingMigrations.Any()) { - throw new InvalidOperationException($"The database is not up to date. The pending migrations are: {string.Join(", ", pendingMigrations)}"); + throw new InvalidOperationException($"The database is not up to date. The pending migrations are: {string.Join(", ", pendingMigrations)}. Apply them by running `./database.mk migrate`."); } } - private static async Task CreateDatabase( - IServiceProvider services + private static async Task CreateDatabaseAsync( + IServiceProvider services, + CancellationToken cancellationToken ) { try { - using var dbContext = - services.GetRequiredService>() - .CreateDbContext(); - await dbContext.Database.EnsureCreatedAsync(); + await using var databaseContext = + await services.GetRequiredService>() + .CreateDbContextAsync(cancellationToken); + await databaseContext.Database.EnsureCreatedAsync(cancellationToken); } catch (Exception exception) { @@ -186,13 +196,14 @@ IServiceProvider services } } - private static async Task SeedDatabase( - IServiceProvider services + private static async Task SeedDatabaseAsync( + IServiceProvider services, + CancellationToken cancellationToken ) { try { - await DbSeeder.DoAsync(services); + await DbSeeder.DoAsync(services, cancellationToken); } catch (Exception exception) { @@ -241,8 +252,8 @@ Uri openTelemetryHost return builder; } - public static void ConfigureAppConfiguration( - IConfigurationBuilder configuration, + private static void ConfigureAppConfiguration( + ConfigurationManager configuration, IHostEnvironment environment, string[] commandLineArguments ) @@ -261,8 +272,7 @@ string[] commandLineArguments !environment.IsEnvironment(TestEnvironment) ) .AddEnvironmentVariables() - .AddEnvironmentVariables( - "XBASE_") // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1#environment-variables + .AddEnvironmentVariables("XBASE_") // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1#environment-variables .AddCommandLine(commandLineArguments); } } \ No newline at end of file diff --git a/backend/src/Services/EmailSender.cs b/backend/src/Services/EmailSender.cs index b0e2c00b5..1d554f90f 100644 --- a/backend/src/Services/EmailSender.cs +++ b/backend/src/Services/EmailSender.cs @@ -36,7 +36,7 @@ string body message.From.Add( new MailboxAddress( "Metabase", - $"metabase@{appSettings.Uri.Host}" + appSettings.EmailAddress ) ); message.To.Add( diff --git a/backend/src/Services/GnuPgService.cs b/backend/src/Services/GnuPgService.cs index 9fc38b819..da4d6733c 100644 --- a/backend/src/Services/GnuPgService.cs +++ b/backend/src/Services/GnuPgService.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Quartz.Util; @@ -62,17 +63,20 @@ ILogger logger [GeneratedRegex(@"<([^>]+)>")] private static partial Regex ExtractEmailAddressRegex(); - public async Task VerifyGnuPgKey( + public async Task VerifyGnuPgKeyAsync( string fingerprint, - string emailAddress + string emailAddress, + CancellationToken cancellationToken ) { if (fingerprint.IsNullOrWhiteSpace()) { return GnuPgKeyVerificationResult.MALFORMED_FINGERPRINT; } - var (exitCode, output, diagnostics) = await ExecuteCommand( - $"gpg --batch --with-colons --keyserver '{KeyServerUrl}' --search-keys '{fingerprint}'" + var (exitCode, output, diagnostics) = await ExecuteCommandAsync( + $"gpg --batch --with-colons --keyserver '{KeyServerUrl}' --search-keys '{fingerprint}'", + null, + cancellationToken ); if (exitCode is 2) { @@ -119,9 +123,10 @@ string output return match.Groups[1].Value; } - private async Task<(int ExitCode, string Output, string Diagnostics)> ExecuteCommand( + private async Task<(int ExitCode, string Output, string Diagnostics)> ExecuteCommandAsync( string command, - string? input = null + string? input, + CancellationToken cancellationToken ) { logger.ExecuteCommand(command); @@ -143,16 +148,16 @@ string output RedirectStandardOutput = true, RedirectStandardError = true, } - ) ?? throw new InvalidOperationException($"The process for command '${command}' with the input '${input}' failed to start."); + ) ?? throw new InvalidOperationException($"The process for command '{command}' with the input '{input}' failed to start."); if (input is not null) { - await process.StandardInput.WriteLineAsync(input); - await process.StandardInput.FlushAsync(); + await process.StandardInput.WriteLineAsync(input.AsMemory(), cancellationToken); + await process.StandardInput.FlushAsync(cancellationToken); } process.StandardInput.Close(); - var output = process.StandardOutput.ReadToEnd(); - var diagnostics = process.StandardError.ReadToEnd(); - await process.WaitForExitAsync(); + var output = await process.StandardOutput.ReadToEndAsync(cancellationToken); + var diagnostics = await process.StandardError.ReadToEndAsync(cancellationToken); + await process.WaitForExitAsync(cancellationToken); logger.ExecuteCommandOutput(output); logger.ExecuteCommandDiagnostics(diagnostics); logger.ExecuteCommandExitCode(process.ExitCode); diff --git a/backend/src/Startup.cs b/backend/src/Startup.cs index c057cd723..a28ace327 100644 --- a/backend/src/Startup.cs +++ b/backend/src/Startup.cs @@ -6,13 +6,13 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; -using HotChocolate.AspNetCore; using Metabase.Configuration; using Metabase.Data; using Metabase.Data.Extensions; using Metabase.Data.OpenIdConnect; using Metabase.Enumerations; using Metabase.GraphQl; +using Metabase.GraphQl.Requests; using Metabase.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.DataProtection; @@ -28,6 +28,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.OpenApi; +using NodaTime; using Npgsql; using OpenTelemetry.Exporter; using OpenTelemetry.Logs; @@ -41,7 +42,7 @@ namespace Metabase; public sealed class Startup( IWebHostEnvironment environment, IConfiguration configuration - ) +) { private readonly AppSettings _appSettings = configuration.Get(_ => @@ -51,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); services.AddScoped(); ConfigureRequestResponseServices(services); @@ -76,8 +77,11 @@ public void ConfigureServices(IServiceCollection services) // .AddOpenIdConnectServer(_appSettings.Uri, isDynamicOpenIdProvider: false) services.AddSingleton(_appSettings); services.AddSingleton(environment); + services.AddSingleton(_clock); // services.AddDatabaseDeveloperPageExceptionFilter(); ConfigureCustomServices(services); + AuthConfiguration.ConfigureServices(services, environment, _appSettings, _clock); + GraphQlConfiguration.ConfigureServices(services, environment); } private static void ConfigureRequestResponseServices(IServiceCollection services) @@ -85,7 +89,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 +172,7 @@ IServiceCollection services // { // _.AddAspNetCoreInstrumentation(); // _.AddHttpClientInstrumentation(); + // _.AddHotChocolateInstrumentation(); // _.AddOtlpExporter(_ => // { // _.Endpoint = _appSettings.OpenTelemetry.GrpcUri; @@ -203,19 +207,12 @@ private void ConfigureDatabaseContext( DbContextOptionsBuilder options ) { - var connectionStringBuilder = new NpgsqlConnectionStringBuilder(); - connectionStringBuilder.Host = _appSettings.Database.Host; - connectionStringBuilder.Port = _appSettings.Database.Port; - connectionStringBuilder.Database = _appSettings.Database.Name; - connectionStringBuilder.Username = _appSettings.Database.UserName; - connectionStringBuilder.Password = _appSettings.Database.Password; - connectionStringBuilder.MaxPoolSize = 90; options .UseNpgsql( - connectionStringBuilder.ConnectionString, + _appSettings.Database.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 @@ -283,7 +280,9 @@ private void ConfigureHttpClientServices(IServiceCollection services) public static void ConfigureCustomServices(IServiceCollection services) { services.AddScoped(); + services.AddScoped(); services.AddScoped(); + services.AddScoped(); } public void Configure(WebApplication app) @@ -309,17 +308,17 @@ public void Configure(WebApplication app) // app.UseHsts(); // Done by the reverse proxy, see https://www.nginx.com/blog/http-strict-transport-security-hsts-and-nginx/ } - app.UseStatusCodePages(); // [UseStatusCodePages](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-9.0#usestatuscodepages) + app.UseStatusCodePagesWithReExecute("/error"); //[UseStatusCodePages](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-9.0#usestatuscodepages) // app.UseHttpsRedirection(); // Done by the reverse proxy app.UseSerilogRequestLogging(); 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(); @@ -342,25 +341,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/ViewModels/Authorization/AuthorizeViewModel.cs b/backend/src/ViewModels/Authorization/AuthorizeViewModel.cs index 800803d0d..d12acbbf6 100644 --- a/backend/src/ViewModels/Authorization/AuthorizeViewModel.cs +++ b/backend/src/ViewModels/Authorization/AuthorizeViewModel.cs @@ -1,10 +1,9 @@ using System.ComponentModel.DataAnnotations; +using Metabase.GraphQl.OpenIdConnect; namespace Metabase.ViewModels.Authorization; -public sealed record AuthorizeViewModel -{ - [Display(Name = "Application")] public string? ApplicationName { get; init; } - - [Display(Name = "Scope")] public string? Scope { get; init; } -} \ No newline at end of file +public sealed record AuthorizeViewModel( + [property: Display(Name = "Application")] string ApplicationName, + [property: Display(Name = "Scopes")] OpenIdConnectScope[] Scopes +); \ No newline at end of file diff --git a/backend/src/Views/Authorization/Authorize.cshtml b/backend/src/Views/Authorization/Authorize.cshtml index a219051de..d7efb0e4f 100644 --- a/backend/src/Views/Authorization/Authorize.cshtml +++ b/backend/src/Views/Authorization/Authorize.cshtml @@ -1,11 +1,18 @@ @using Microsoft.Extensions.Primitives @model AuthorizeViewModel +@using Metabase.Extensions

Authorize

- Do you want to grant @Model.ApplicationName access to your - data associated with the scopes @Model.Scope? + Do you want to grant @Model.ApplicationName access to some + of your data and to act on your behalf? The application asks for

+
    + @foreach (var scope in @Model.Scopes) + { +
  • @scope.GetDescription()
  • + } +
@* Flow the request parameters so they can be received by the Accept/Reject actions: *@ @foreach (var parameter in Context.Request.HasFormContentType ? (IEnumerable - \ No newline at end of file + diff --git a/backend/test/AuditableTests.cs b/backend/test/AuditableTests.cs new file mode 100644 index 000000000..fd7e5eaa1 --- /dev/null +++ b/backend/test/AuditableTests.cs @@ -0,0 +1,75 @@ +using NodaTime; +using NodaTime.Testing; +using Microsoft.EntityFrameworkCore; +using NUnit.Framework; +using Metabase.Data; +using System; +using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; + +namespace Metabase.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 Method("Name", "Description", null, null, null, [], [], []); + // 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", "New Description", null, null, null, [], [], []); + 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/Integration/CustomWebApplicationFactory.cs b/backend/test/Integration/CustomWebApplicationFactory.cs index 011da73e1..f67b72885 100644 --- a/backend/test/Integration/CustomWebApplicationFactory.cs +++ b/backend/test/Integration/CustomWebApplicationFactory.cs @@ -70,34 +70,12 @@ public AppSettings AppSettings } } - protected override IHost CreateHost(IHostBuilder builder) - { - builder.UseEnvironment(Metabase.Program.TestEnvironment); - builder.UseSerilog((context, services, configuration) => - { - configuration - .ReadFrom.Configuration(context.Configuration) // appsettings.test.json - .WriteTo.NUnitOutput(formatProvider: CultureInfo.InvariantCulture); - }); - return base.CreateHost(builder); - } - protected override void ConfigureWebHost(IWebHostBuilder builder) { var databaseName = Guid.NewGuid().ToString().Replace("-", ""); // var schemaName = $"metabase_{Guid.NewGuid().ToString().Replace("-", "")}"; // builder.ConfigureAppConfiguration(_ => _.AddInMemoryCollection([new KeyValuePair("Database__Name", databaseName)])); // "Database__SchemaName" builder.UseEnvironment(Metabase.Program.TestEnvironment); - builder.ConfigureAppConfiguration((webHostBuilderContext, configurationBuilder) => - { - configurationBuilder.Sources.Clear(); - Metabase.Program.ConfigureAppConfiguration( - configurationBuilder, - webHostBuilderContext.HostingEnvironment, - [] - ); - } - ); builder.ConfigureServices(serviceCollection => { using var scope = serviceCollection.BuildServiceProvider().CreateScope(); @@ -143,6 +121,17 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) ); } + protected override IHost CreateHost(IHostBuilder builder) + { + builder.UseSerilog((context, services, configuration) => + { + configuration + .ReadFrom.Configuration(context.Configuration) // appsettings.test.json + .WriteTo.NUnitOutput(formatProvider: CultureInfo.InvariantCulture); + }); + return base.CreateHost(builder); + } + // https://docs.microsoft.com/en-us/dotnet/standard/managed-code // https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose ~CustomWebApplicationFactory() diff --git a/backend/test/Integration/DatabaseSchemaTests.cs b/backend/test/Integration/DatabaseSchemaTests.cs new file mode 100644 index 000000000..a913c5e3a --- /dev/null +++ b/backend/test/Integration/DatabaseSchemaTests.cs @@ -0,0 +1,149 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Npgsql; +using NUnit.Framework; + +namespace Metabase.Tests.Integration; + +[TestFixture] +public sealed class DatabaseSchemaTests + : IntegrationTests +{ + /// + /// Compare the database schema wanted by `ApplicationDbContext` to the + /// database schema constructed by `./database.mk create migrate`. The + /// former is the database schema that matches the domain model and the + /// latter is the actual database schema being used. They can get + /// out-of-sync if manual changes to `migrate.sql` introduce + /// inconsistencies or if changes to the domain model were made but no + /// `make migration NAME=...` run yet. + /// + /// Do not use + /// [`EfCore.SchemaCompare`](https://github.com/JonPSmith/EfCore.SchemaCompare) + /// to reverse-engineer the domain model from the PostgreSQL database schema + /// and compare it with the actual one because of its + /// [limitations](https://github.com/JonPSmith/EfCore.SchemaCompare#List-of-limitations). + /// An alternative to + /// [`pg-schema-diff`](https://github.com/stripe/pg-schema-diff) + /// it + /// [`liquibase diff`](https://docs.liquibase.com/community/reference-guide-5-0-3/database-inspection-change-tracking-and-utility-commands/diff) + /// which can be used as follows + /// ``` + /// liquibase diff \ + /// --url="jdbc:postgresql://database:5432/xbase" --username="postgres" --password="postgres" --default-schema-name="metabase" \ + /// --reference-url="jdbc:postgresql://database:5432/test" --reference-username="postgres" --reference-password="postgres" --reference-default-schema-name="metabase" + /// ``` + /// and installed as explained on + /// [Install Liquibase on Linux with Debian or Ubuntu](https://docs.liquibase.com/community/get-started-5-0-3/install-liquibase-on-linux-with-debian-or-ubuntu) + /// + [Test] + public async Task EnsureDomainModelMatchesDatabaseSchema() + { + // Arrange + var cancellationToken = TestContext.CurrentContext.CancellationToken; + var migrateSqlBasedDatabaseName = Guid.NewGuid().ToString().Replace("-", ""); + await ExecuteSqlScriptAsync( + AppSettings.Database.ConnectionString(), + $"""CREATE DATABASE "{migrateSqlBasedDatabaseName}";""", + cancellationToken + ); + await ExecuteSqlScriptAsync( + AppSettings.Database.ConnectionString(migrateSqlBasedDatabaseName), + // the current directory, that is, `./`, is `/home/me/app/test/bin/Debug/net*.0/` + await ReadSqlScriptAsync(Path.Combine("..", "..", "..", "..", "src", "Migrations", "migrate.sql"), cancellationToken), + cancellationToken + ); + // Act + var differenceResponse = await ExecuteCommandAsync( + $""" + pg-schema-diff plan \ + --from-dsn "postgres://{AppSettings.Database.UserName}:{AppSettings.Database.Password}@database/{AppSettings.Database.Name}?search_path={AppSettings.Database.SchemaName}" \ + --to-dsn "postgres://{AppSettings.Database.UserName}:{AppSettings.Database.Password}@database/{migrateSqlBasedDatabaseName}?search_path={AppSettings.Database.SchemaName}" + """, + null, + cancellationToken + ); + // Assert + differenceResponse.ExitCode.Should().Be( + 0, + because: differenceResponse.Output + "\n" + differenceResponse.Diagnostics + "\nIf the test `EnsureMigrationsAreUpToDate` also fails, then run `make migration NAME='...' in the shell `./docker.mk shell SERVICE=backend`. Otherwise, most probably, manual changes to ./migrate.sql introduced inconsistencies: Resolve them by creating an empty migration with `make migration NAME='...' in the shell `./docker.mk shell SERVICE=backend` and adapting the 4 files in ./backend/src/Migrations as needed." + ); + } + + private static Task ReadSqlScriptAsync( + string scriptPath, + CancellationToken cancellationToken + ) + { + var absoluteScriptPath = Path.GetFullPath(scriptPath); + if (!File.Exists(absoluteScriptPath)) + { + throw new FileNotFoundException($"The script file was not found at: {absoluteScriptPath}"); + } + return File.ReadAllTextAsync(absoluteScriptPath, cancellationToken); + } + + private static async Task ExecuteSqlScriptAsync( + string connectionString, + string script, + CancellationToken cancellationToken + ) + { + await using var connection = new NpgsqlConnection(connectionString); + await connection.OpenAsync(cancellationToken); + // If necessary, the transactions can be started and committed within the script. + // await using var transaction = await connection.BeginTransactionAsync(cancellationToken); + try + { + await using var command = new NpgsqlCommand(script, connection); // , transaction + await command.ExecuteNonQueryAsync(cancellationToken); + // await transaction.CommitAsync(cancellationToken); + } + catch + { + // await transaction.RollbackAsync(cancellationToken); + throw; + } + } + + private static async Task<(int ExitCode, string Output, string Diagnostics)> ExecuteCommandAsync( + string command, + string? input, + CancellationToken cancellationToken + ) + { + var process = Process.Start( + new ProcessStartInfo() + { + FileName = "bash", + ArgumentList = { + "-o", "errexit", + "-o", "errtrace", + "-o", "nounset", + "-o", "pipefail", + "-c", command + }, + ErrorDialog = false, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + } + ) ?? throw new InvalidOperationException($"The process for command '{command}' with the input '{input}' failed to start."); + if (input is not null) + { + await process.StandardInput.WriteLineAsync(input.AsMemory(), cancellationToken); + await process.StandardInput.FlushAsync(cancellationToken); + } + process.StandardInput.Close(); + var output = await process.StandardOutput.ReadToEndAsync(cancellationToken); + var diagnostics = await process.StandardError.ReadToEndAsync(cancellationToken); + await process.WaitForExitAsync(cancellationToken); + return (process.ExitCode, output, diagnostics); + } +} \ No newline at end of file diff --git a/backend/test/Integration/GraphQl/Components/__snapshots__/CreateComponentTests.AnonymousUser_IsAuthenticationError.snap b/backend/test/Integration/GraphQl/Components/__snapshots__/CreateComponentTests.AnonymousUser_IsAuthenticationError.snap index dbee716b3..f00bf8f70 100644 --- a/backend/test/Integration/GraphQl/Components/__snapshots__/CreateComponentTests.AnonymousUser_IsAuthenticationError.snap +++ b/backend/test/Integration/GraphQl/Components/__snapshots__/CreateComponentTests.AnonymousUser_IsAuthenticationError.snap @@ -1 +1 @@ -{"errors":[{"message":"The current user is not authorized to access this resource.","locations":[{"line":4,"column":3}],"path":["createComponent"],"extensions":{"code":"AUTH_NOT_AUTHENTICATED"}}],"data":null} +{"errors":[{"message":"The current user is not authorized to access this resource.","path":["createComponent"],"extensions":{"code":"AUTH_NOT_AUTHENTICATED"}}],"data":null} diff --git a/backend/test/Integration/GraphQl/GraphQlQueryValidationTests.cs b/backend/test/Integration/GraphQl/GraphQlQueryValidationTests.cs new file mode 100644 index 000000000..92ab6fcbb --- /dev/null +++ b/backend/test/Integration/GraphQl/GraphQlQueryValidationTests.cs @@ -0,0 +1,113 @@ +// using System; +// using System.Collections.Generic; +// using System.IO; +// using System.Linq; +// using System.Threading.Tasks; +// using HotChocolate; +// using HotChocolate.Validation; +// using HotChocolate.Language; +// using Microsoft.Extensions.DependencyInjection; +// using NUnit.Framework; +// using FluentAssertions; +// using System.Diagnostics.CodeAnalysis; +// +// namespace Metabase.Tests.Integration.GraphQl; +// +// [TestFixture] +// public class GraphQlQueryValidationTests +// { +// private static readonly string MetabaseSchemaPath = System.IO.Path.GetFullPath( +// System.IO.Path.Combine("..", "..", "..", "Integration", "GraphQl", "__snapshots__", "GraphQlSchemaTests.IsUnchanged.snap") +// ); +// +// private static readonly string DatabaseSchemaPath = System.IO.Path.GetFullPath( +// System.IO.Path.Combine("GraphQl", "Databases", "Queries", "database.graphqls") +// ); +// +// private static readonly string DatabaseGraphQlQueryFolder = System.IO.Path.GetFullPath( +// System.IO.Path.Combine("src", "GraphQl", "Databases", "Queries") +// ); +// +// // Code Smell: The variables are properly set in `OneTimeSetUp` +// private (Schema, DocumentValidator) metabaseValidator = default!; +// private (Schema, DocumentValidator) databaseValidator = default!; +// +// [OneTimeSetUp] +// public async Task OneTimeSetUp() +// { +// metabaseValidator = await CreateValidator(MetabaseSchemaPath); +// databaseValidator = await CreateValidator(DatabaseSchemaPath); +// } +// +// private static async Task<(Schema, DocumentValidator)> CreateValidator(string schemaPath) +// { +// if (!File.Exists(schemaPath)) +// { +// Assert.Fail($"GraphQL schema file not found at: {schemaPath}"); +// } +// var schema = SchemaBuilder.New() +// .AddDocumentFromString(await File.ReadAllTextAsync(schemaPath)) +// .Use(_ => _) +// .Create(); +// var validator = new ServiceCollection() +// .AddValidation() +// .BuildServiceProvider() +// .GetRequiredService() +// .Build(); +// return (schema, validator); +// } +// +// [Test] +// [TestCaseSource(nameof(GetMetabaseGraphQLQueryFiles))] +// [SuppressMessage("Naming", "CA1707")] +// public Task QueryFile_MatchesMetabaseSchema(string queryFilePath) +// { +// return AssertQueryFileMatchesSchema(queryFilePath, metabaseValidator); +// } +// +// [Test] +// [TestCaseSource(nameof(GetDatabaseGraphQLQueryFiles))] +// [SuppressMessage("Naming", "CA1707")] +// public Task QueryFile_MatchesDatabaseSchema(string queryFilePath) +// { +// return AssertQueryFileMatchesSchema(queryFilePath, databaseValidator); +// } +// +// private static IEnumerable GetMetabaseGraphQLQueryFiles() +// { +// var queriesFolderPath = System.IO.Path.GetFullPath("."); +// return Directory.GetFiles(queriesFolderPath, "*.graphql", SearchOption.AllDirectories) +// .Where(filePath => !filePath.Contains(DatabaseGraphQlQueryFolder, StringComparison.OrdinalIgnoreCase)); +// } +// +// private static string[] GetDatabaseGraphQLQueryFiles() +// { +// return Directory.GetFiles(DatabaseGraphQlQueryFolder, "*.graphql", SearchOption.AllDirectories); +// } +// +// public static async Task AssertQueryFileMatchesSchema(string queryFilePath, (Schema schema, DocumentValidator validator) schemaAndValidator) +// { +// var queryText = await File.ReadAllTextAsync(queryFilePath); +// DocumentNode queryDocument; +// try +// { +// queryDocument = Utf8GraphQLParser.Parse(queryText); +// } +// catch (SyntaxException exception) +// { +// Assert.Fail($"GraphQL Syntax Error in file '{System.IO.Path.GetFileName(queryFilePath)}': {exception.Message}"); +// return; +// } +// var validationResult = schemaAndValidator.validator.Validate(schemaAndValidator.schema, queryDocument); +// if (validationResult.HasErrors) +// { +// var formattedErrors = validationResult.Errors.Select(error => +// $"[{error.Code}] {error.Message} (Line: {(error.Locations?.Count > 0 ? error.Locations[0].Line : -1)}, Column: {(error.Locations?.Count > 0 ? error.Locations[0].Column : -1)})" +// ); +// validationResult.Errors.Should().BeEmpty($""" +// because the GraphQL query file '{queryFilePath}' must comply with the schema definition. Found issues: +// {string.Join(Environment.NewLine, formattedErrors)} +// """); +// } +// } +// } \ No newline at end of file diff --git a/backend/test/Integration/GraphQl/Institutions/InstitutionIntegrationTests.cs b/backend/test/Integration/GraphQl/Institutions/InstitutionIntegrationTests.cs index 238d932ca..205ccccd5 100644 --- a/backend/test/Integration/GraphQl/Institutions/InstitutionIntegrationTests.cs +++ b/backend/test/Integration/GraphQl/Institutions/InstitutionIntegrationTests.cs @@ -17,7 +17,7 @@ public abstract class InstitutionIntegrationTests "I!A", "Best institution ever!", new ContactInformationInput( - PhoneNumber: "(999) 9999-9999", + PhoneNumber: "+99999999999", PostalAddress: "Street 9, 7777 Town", EmailAddress: "aaa@institution.com", WebsiteLocator: new Uri("https://institution-a.com", UriKind.Absolute) diff --git a/backend/test/Integration/GraphQl/Institutions/__snapshots__/CreateInstitutionTests.AnonymousUser_CannotCreateInstitution.snap b/backend/test/Integration/GraphQl/Institutions/__snapshots__/CreateInstitutionTests.AnonymousUser_CannotCreateInstitution.snap index 2e29402a5..2e39af242 100644 --- a/backend/test/Integration/GraphQl/Institutions/__snapshots__/CreateInstitutionTests.AnonymousUser_CannotCreateInstitution.snap +++ b/backend/test/Integration/GraphQl/Institutions/__snapshots__/CreateInstitutionTests.AnonymousUser_CannotCreateInstitution.snap @@ -1 +1 @@ -{"data":{"institutions":{"edges":[{"node":{"id":"SW5zdGl0dXRpb246QAnd/RDrMEegz76vPWQQMg==","uuid":"fddd0940-eb10-4730-a0cf-beaf3d641032","name":"Fraunhofer ISE","abbreviation":"ISE","description":"Fraunhofer Institute for Solar Energy Systems (ISE)","contact":{"phoneNumber":"+49 761 45880","postalAddress":"Heidenhofstraße 2, 79110 Freiburg im Breisgau","emailAddress":null,"websiteLocator":"https://www.ise.fraunhofer.de/"},"state":"VERIFIED"}}]}}} +{"data":{"institutions":{"edges":[{"node":{"id":"SW5zdGl0dXRpb2467/V6wR0vc0y8yfz7ciQg8w==","uuid":"c17af5ef-2f1d-4c73-bcc9-fcfb722420f3","name":"LBNL","abbreviation":"LBNL","description":"Lawrence Berkeley National Laboratory","contact":{"phoneNumber":"+5104864000","postalAddress":"1 Cyclotron Road, Berkeley, CA 94720","emailAddress":null,"websiteLocator":"https://www.lbl.gov/"},"state":"VERIFIED"}},{"node":{"id":"SW5zdGl0dXRpb246RwBYhNvRdUK9KUIiyQfKvg==","uuid":"84580047-d1db-4275-bd29-4222c907cabe","name":"EPEA - Part of Drees & Sommer","abbreviation":"EPEA","description":"Sustainability","contact":{"phoneNumber":null,"postalAddress":null,"emailAddress":null,"websiteLocator":null},"state":"VERIFIED"}},{"node":{"id":"SW5zdGl0dXRpb246XPm5gmEyOkaQ/g6dpwevFw==","uuid":"82b9f95c-3261-463a-90fe-0e9da707af17","name":"TestLab Solar Facades","abbreviation":"TLSF","description":"This institution represents the TestLab Solar Facades of Fraunhofer ISE","contact":{"phoneNumber":"+4976145885673","postalAddress":"Heidenhofstraße 2, 79110 Freiburg im Breisgau","emailAddress":null,"websiteLocator":"https://www.ise.fraunhofer.de/en/rd-infrastructure/accredited-labs/testlab-solar-facades.html"},"state":"VERIFIED"}},{"node":{"id":"SW5zdGl0dXRpb246+9YgU22560qiTOtwNtNDeg==","uuid":"5320d6fb-b96d-4aeb-a24c-eb7036d3437a","name":"Fraunhofer ISE","abbreviation":"ISE","description":"Fraunhofer Institute for Solar Energy Systems (ISE)","contact":{"phoneNumber":"+4976145880","postalAddress":"Heidenhofstraße 2, 79110 Freiburg im Breisgau","emailAddress":null,"websiteLocator":"https://www.ise.fraunhofer.de/"},"state":"VERIFIED"}}]}}} diff --git a/backend/test/Integration/GraphQl/Institutions/__snapshots__/CreateInstitutionTests.AnonymousUser_IsAuthenticationError.snap b/backend/test/Integration/GraphQl/Institutions/__snapshots__/CreateInstitutionTests.AnonymousUser_IsAuthenticationError.snap index 91ebf7903..57270f8ce 100644 --- a/backend/test/Integration/GraphQl/Institutions/__snapshots__/CreateInstitutionTests.AnonymousUser_IsAuthenticationError.snap +++ b/backend/test/Integration/GraphQl/Institutions/__snapshots__/CreateInstitutionTests.AnonymousUser_IsAuthenticationError.snap @@ -1 +1 @@ -{"errors":[{"message":"The current user is not authorized to access this resource.","locations":[{"line":4,"column":3}],"path":["createInstitution"],"extensions":{"code":"AUTH_NOT_AUTHENTICATED"}}],"data":null} +{"errors":[{"message":"The current user is not authorized to access this resource.","path":["createInstitution"],"extensions":{"code":"AUTH_NOT_AUTHENTICATED"}}],"data":null} diff --git a/backend/test/Integration/GraphQl/Institutions/__snapshots__/CreateInstitutionTests.LoggedInUser_CreatesInstitution_PendingInstitutionInput.snap b/backend/test/Integration/GraphQl/Institutions/__snapshots__/CreateInstitutionTests.LoggedInUser_CreatesInstitution_PendingInstitutionInput.snap index 77614da29..9d5472d8a 100644 --- a/backend/test/Integration/GraphQl/Institutions/__snapshots__/CreateInstitutionTests.LoggedInUser_CreatesInstitution_PendingInstitutionInput.snap +++ b/backend/test/Integration/GraphQl/Institutions/__snapshots__/CreateInstitutionTests.LoggedInUser_CreatesInstitution_PendingInstitutionInput.snap @@ -1 +1 @@ -{"data":{"pendingInstitutions":{"edges":[{"node":{"id":"SW5zdGl0dXRpb246D6120A8JYk+AlOlveA/B9g==","uuid":"d076ad0f-090f-4f62-8094-e96f780fc1f6","name":"Institution A","abbreviation":"I!A","description":"Best institution ever!","contact":{"phoneNumber":"(999) 9999-9999","postalAddress":"Street 9, 7777 Town","emailAddress":"aaa@institution.com","websiteLocator":"https://institution-a.com/"},"state":"PENDING"}}]}}} +{"data":{"pendingInstitutions":{"edges":[{"node":{"id":"SW5zdGl0dXRpb246D6120A8JYk+AlOlveA/B9g==","uuid":"d076ad0f-090f-4f62-8094-e96f780fc1f6","name":"Institution A","abbreviation":"I!A","description":"Best institution ever!","contact":{"phoneNumber":"+99999999999","postalAddress":"Street 9, 7777 Town","emailAddress":"aaa@institution.com","websiteLocator":"https://institution-a.com/"},"state":"PENDING"}}]}}} diff --git a/backend/test/Integration/GraphQl/Institutions/__snapshots__/CreateInstitutionTests.LoggedInUser_IsSuccess_PendingInstitutionInput.snap b/backend/test/Integration/GraphQl/Institutions/__snapshots__/CreateInstitutionTests.LoggedInUser_IsSuccess_PendingInstitutionInput.snap index ad2bf5370..03bedd3a2 100644 --- a/backend/test/Integration/GraphQl/Institutions/__snapshots__/CreateInstitutionTests.LoggedInUser_IsSuccess_PendingInstitutionInput.snap +++ b/backend/test/Integration/GraphQl/Institutions/__snapshots__/CreateInstitutionTests.LoggedInUser_IsSuccess_PendingInstitutionInput.snap @@ -1 +1 @@ -{"data":{"createInstitution":{"institution":{"id":"SW5zdGl0dXRpb246LDGUD8ervECBGlVtMnnXFg==","uuid":"0f94312c-abc7-40bc-811a-556d3279d716","name":"Institution A","abbreviation":"I!A","description":"Best institution ever!","contact":{"phoneNumber":"(999) 9999-9999","postalAddress":"Street 9, 7777 Town","emailAddress":"aaa@institution.com","websiteLocator":"https://institution-a.com/"}},"errors":null}}} +{"data":{"createInstitution":{"institution":{"id":"SW5zdGl0dXRpb246LDGUD8ervECBGlVtMnnXFg==","uuid":"0f94312c-abc7-40bc-811a-556d3279d716","name":"Institution A","abbreviation":"I!A","description":"Best institution ever!","contact":{"phoneNumber":"+99999999999","postalAddress":"Street 9, 7777 Town","emailAddress":"aaa@institution.com","websiteLocator":"https://institution-a.com/"}},"errors":null}}} diff --git a/backend/test/Integration/GraphQl/Users/ChangeUserEmail.graphql b/backend/test/Integration/GraphQl/Users/ChangeUserEmail.graphql index 58fbc2786..af46436d6 100644 --- a/backend/test/Integration/GraphQl/Users/ChangeUserEmail.graphql +++ b/backend/test/Integration/GraphQl/Users/ChangeUserEmail.graphql @@ -1,5 +1,5 @@ mutation ChangeUserEmail( - $newEmail: String! + $newEmail: EmailAddress! ) { changeUserEmail(input: { newEmail: $newEmail diff --git a/backend/test/Integration/GraphQl/Users/ChangeUserEmailTests.cs b/backend/test/Integration/GraphQl/Users/ChangeUserEmailTests.cs index 0cfa41961..c9037b465 100644 --- a/backend/test/Integration/GraphQl/Users/ChangeUserEmailTests.cs +++ b/backend/test/Integration/GraphQl/Users/ChangeUserEmailTests.cs @@ -103,7 +103,7 @@ await RegisterAndConfirmAndLoginUser( [Test] [SuppressMessage("Naming", "CA1707")] - public async Task InvalidEmail_IsUserError() + public async Task InvalidEmail_IsSyntaxError() { // Arrange const string email = "john.doe@ise.fraunhofer.de"; @@ -115,18 +115,13 @@ await RegisterAndConfirmAndLoginUser( const string newEmail = "@invalid@" + email; // Act var response = await ChangeUserEmail( - AssertHttpSuccess, + AssertHttpBadRequest, ReadAsString, AssertNothing, newEmail ); // Assert - Snapshot.Match( - response, - matchOptions => matchOptions.Assert(fieldOptions => - fieldOptions.Field("data.changeUserEmail.user.id").Should().NotBeNullOrWhiteSpace() - ) - ); + Snapshot.Match(response); await LoginUser(); } } \ No newline at end of file diff --git a/backend/test/Integration/GraphQl/Users/ConfirmUserEmail.graphql b/backend/test/Integration/GraphQl/Users/ConfirmUserEmail.graphql index 43f75b814..bfdee5dd9 100644 --- a/backend/test/Integration/GraphQl/Users/ConfirmUserEmail.graphql +++ b/backend/test/Integration/GraphQl/Users/ConfirmUserEmail.graphql @@ -1,5 +1,5 @@ mutation ConfirmUserEmail( - $email: String!, + $email: EmailAddress!, $confirmationCode: String! ) { confirmUserEmail(input: { diff --git a/backend/test/Integration/GraphQl/Users/ConfirmUserEmailChange.graphql b/backend/test/Integration/GraphQl/Users/ConfirmUserEmailChange.graphql index e363698e2..f5a9ad16e 100644 --- a/backend/test/Integration/GraphQl/Users/ConfirmUserEmailChange.graphql +++ b/backend/test/Integration/GraphQl/Users/ConfirmUserEmailChange.graphql @@ -1,6 +1,6 @@ mutation ConfirmUserEmailChange( - $currentEmail: String!, - $newEmail: String!, + $currentEmail: EmailAddress!, + $newEmail: EmailAddress!, $confirmationCode: String! ) { confirmUserEmailChange(input: { diff --git a/backend/test/Integration/GraphQl/Users/LoginUser.graphql b/backend/test/Integration/GraphQl/Users/LoginUser.graphql index 5442eb982..a2c9b9dfc 100644 --- a/backend/test/Integration/GraphQl/Users/LoginUser.graphql +++ b/backend/test/Integration/GraphQl/Users/LoginUser.graphql @@ -1,4 +1,4 @@ -mutation LoginUser($email: String!, $password: String!) { +mutation LoginUser($email: EmailAddress!, $password: String!) { loginUser( input: { email: $email, password: $password } ) { diff --git a/backend/test/Integration/GraphQl/Users/RegisterUser.graphql b/backend/test/Integration/GraphQl/Users/RegisterUser.graphql index d6dae7bfd..be03c8a41 100644 --- a/backend/test/Integration/GraphQl/Users/RegisterUser.graphql +++ b/backend/test/Integration/GraphQl/Users/RegisterUser.graphql @@ -1,6 +1,6 @@ mutation RegisterUser( $name: String! - $email: String! + $email: EmailAddress! $password: String! $passwordConfirmation: String! ) { diff --git a/backend/test/Integration/GraphQl/Users/RegisterUserTests.cs b/backend/test/Integration/GraphQl/Users/RegisterUserTests.cs index 093a77688..1fb0175d9 100644 --- a/backend/test/Integration/GraphQl/Users/RegisterUserTests.cs +++ b/backend/test/Integration/GraphQl/Users/RegisterUserTests.cs @@ -91,11 +91,11 @@ await RegisterUser( [Test] [SuppressMessage("Naming", "CA1707")] - public async Task InvalidEmail_IsUserError() + public async Task InvalidEmail_IsSyntaxError() { // Act var response = await RegisterUser( - AssertHttpSuccess, + AssertHttpBadRequest, ReadAsString, AssertNothing, email: "john.doeise.fraunhofer.de", @@ -193,11 +193,11 @@ public async Task PasswordTooShort_IsUserError() [Test] [SuppressMessage("Naming", "CA1707")] - public async Task NullOrEmptyEmail_IsUserError() + public async Task NullOrEmptyEmail_IsSyntaxError() { // Act var response = await RegisterUser( - AssertHttpSuccess, + AssertHttpBadRequest, ReadAsString, AssertNothing, email: "", diff --git a/backend/test/Integration/GraphQl/Users/RequestUserPasswordReset.graphql b/backend/test/Integration/GraphQl/Users/RequestUserPasswordReset.graphql index f8a5132e4..cf6e63c89 100644 --- a/backend/test/Integration/GraphQl/Users/RequestUserPasswordReset.graphql +++ b/backend/test/Integration/GraphQl/Users/RequestUserPasswordReset.graphql @@ -1,5 +1,5 @@ mutation RequestUserPasswordReset( - $email: String! + $email: EmailAddress! ) { requestUserPasswordReset(input: { email: $email diff --git a/backend/test/Integration/GraphQl/Users/ResendUserEmailConfirmation.graphql b/backend/test/Integration/GraphQl/Users/ResendUserEmailConfirmation.graphql index a67a95e66..f26f1881f 100644 --- a/backend/test/Integration/GraphQl/Users/ResendUserEmailConfirmation.graphql +++ b/backend/test/Integration/GraphQl/Users/ResendUserEmailConfirmation.graphql @@ -1,5 +1,5 @@ mutation ResendUserEmailConfirmation( - $email: String! + $email: EmailAddress! ) { resendUserEmailConfirmation(input: { email: $email diff --git a/backend/test/Integration/GraphQl/Users/ResetUserPassword.graphql b/backend/test/Integration/GraphQl/Users/ResetUserPassword.graphql index b4a97f89b..42418d308 100644 --- a/backend/test/Integration/GraphQl/Users/ResetUserPassword.graphql +++ b/backend/test/Integration/GraphQl/Users/ResetUserPassword.graphql @@ -1,5 +1,5 @@ mutation ResetUserPassword( - $email: String!, + $email: EmailAddress!, $password: String!, $passwordConfirmation: String!, $resetCode: String! diff --git a/backend/test/Integration/GraphQl/Users/__snapshots__/ChangeUserEmailTests.InvalidEmail_IsSyntaxError.snap b/backend/test/Integration/GraphQl/Users/__snapshots__/ChangeUserEmailTests.InvalidEmail_IsSyntaxError.snap new file mode 100644 index 000000000..a00871d06 --- /dev/null +++ b/backend/test/Integration/GraphQl/Users/__snapshots__/ChangeUserEmailTests.InvalidEmail_IsSyntaxError.snap @@ -0,0 +1 @@ +{"errors":[{"message":"EmailAddressType cannot parse the provided value. The provided value does not meet the RFC 5322 specification.","path":["newEmail"],"extensions":{"code":"HC0002","actualType":"EmailAddress"}}]} diff --git a/backend/test/Integration/GraphQl/Users/__snapshots__/ChangeUserEmailTests.InvalidEmail_IsUserError.snap b/backend/test/Integration/GraphQl/Users/__snapshots__/ChangeUserEmailTests.InvalidEmail_IsUserError.snap deleted file mode 100644 index efdf60e71..000000000 --- a/backend/test/Integration/GraphQl/Users/__snapshots__/ChangeUserEmailTests.InvalidEmail_IsUserError.snap +++ /dev/null @@ -1 +0,0 @@ -{"data":{"changeUserEmail":{"user":{"id":"VXNlcjpv6poBsoOieYEHv2iSs1yM","contact":{"emailAddress":"john.doe@ise.fraunhofer.de"}},"errors":null}}} diff --git a/backend/test/Integration/GraphQl/Users/__snapshots__/ChangeUserEmailTests.NonLoggedInUser_IsAuthenticationError.snap b/backend/test/Integration/GraphQl/Users/__snapshots__/ChangeUserEmailTests.NonLoggedInUser_IsAuthenticationError.snap index 53c977494..9959c92e8 100644 --- a/backend/test/Integration/GraphQl/Users/__snapshots__/ChangeUserEmailTests.NonLoggedInUser_IsAuthenticationError.snap +++ b/backend/test/Integration/GraphQl/Users/__snapshots__/ChangeUserEmailTests.NonLoggedInUser_IsAuthenticationError.snap @@ -1 +1 @@ -{"errors":[{"message":"The current user is not authorized to access this resource.","locations":[{"line":4,"column":3}],"path":["changeUserEmail"],"extensions":{"code":"AUTH_NOT_AUTHENTICATED"}}],"data":null} +{"errors":[{"message":"The current user is not authorized to access this resource.","path":["changeUserEmail"],"extensions":{"code":"AUTH_NOT_AUTHENTICATED"}}],"data":null} diff --git a/backend/test/Integration/GraphQl/Users/__snapshots__/ChangeUserPasswordTests.NonLoggedInUser_IsAuthenticationError.snap b/backend/test/Integration/GraphQl/Users/__snapshots__/ChangeUserPasswordTests.NonLoggedInUser_IsAuthenticationError.snap index 0a60e5e9c..083c68952 100644 --- a/backend/test/Integration/GraphQl/Users/__snapshots__/ChangeUserPasswordTests.NonLoggedInUser_IsAuthenticationError.snap +++ b/backend/test/Integration/GraphQl/Users/__snapshots__/ChangeUserPasswordTests.NonLoggedInUser_IsAuthenticationError.snap @@ -1 +1 @@ -{"errors":[{"message":"The current user is not authorized to access this resource.","locations":[{"line":6,"column":3}],"path":["changeUserPassword"],"extensions":{"code":"AUTH_NOT_AUTHENTICATED"}}],"data":null} +{"errors":[{"message":"The current user is not authorized to access this resource.","path":["changeUserPassword"],"extensions":{"code":"AUTH_NOT_AUTHENTICATED"}}],"data":null} diff --git a/backend/test/Integration/GraphQl/Users/__snapshots__/DeletePersonalUserDataTests.NonLoggedInUser_IsAuthenticationError.snap b/backend/test/Integration/GraphQl/Users/__snapshots__/DeletePersonalUserDataTests.NonLoggedInUser_IsAuthenticationError.snap index c6a014f0f..55d84a56f 100644 --- a/backend/test/Integration/GraphQl/Users/__snapshots__/DeletePersonalUserDataTests.NonLoggedInUser_IsAuthenticationError.snap +++ b/backend/test/Integration/GraphQl/Users/__snapshots__/DeletePersonalUserDataTests.NonLoggedInUser_IsAuthenticationError.snap @@ -1 +1 @@ -{"errors":[{"message":"The current user is not authorized to access this resource.","locations":[{"line":4,"column":3}],"path":["deletePersonalUserData"],"extensions":{"code":"AUTH_NOT_AUTHENTICATED"}}],"data":null} +{"errors":[{"message":"The current user is not authorized to access this resource.","path":["deletePersonalUserData"],"extensions":{"code":"AUTH_NOT_AUTHENTICATED"}}],"data":null} diff --git a/backend/test/Integration/GraphQl/Users/__snapshots__/RegisterUserTests.InvalidEmail_IsSyntaxError.snap b/backend/test/Integration/GraphQl/Users/__snapshots__/RegisterUserTests.InvalidEmail_IsSyntaxError.snap new file mode 100644 index 000000000..b7695ec6d --- /dev/null +++ b/backend/test/Integration/GraphQl/Users/__snapshots__/RegisterUserTests.InvalidEmail_IsSyntaxError.snap @@ -0,0 +1 @@ +{"errors":[{"message":"EmailAddressType cannot parse the provided value. The provided value does not meet the RFC 5322 specification.","path":["email"],"extensions":{"code":"HC0002","actualType":"EmailAddress"}}]} diff --git a/backend/test/Integration/GraphQl/Users/__snapshots__/RegisterUserTests.InvalidEmail_IsUserError.snap b/backend/test/Integration/GraphQl/Users/__snapshots__/RegisterUserTests.InvalidEmail_IsUserError.snap deleted file mode 100644 index 9a2429bd1..000000000 --- a/backend/test/Integration/GraphQl/Users/__snapshots__/RegisterUserTests.InvalidEmail_IsUserError.snap +++ /dev/null @@ -1 +0,0 @@ -{"data":{"registerUser":{"user":null,"errors":[{"code":"INVALID_EMAIL","message":"Email \u0027john.doeise.fraunhofer.de\u0027 is invalid.","path":["input","email"]}]}}} diff --git a/backend/test/Integration/GraphQl/Users/__snapshots__/RegisterUserTests.NullOrEmptyEmail_IsSyntaxError.snap b/backend/test/Integration/GraphQl/Users/__snapshots__/RegisterUserTests.NullOrEmptyEmail_IsSyntaxError.snap new file mode 100644 index 000000000..b7695ec6d --- /dev/null +++ b/backend/test/Integration/GraphQl/Users/__snapshots__/RegisterUserTests.NullOrEmptyEmail_IsSyntaxError.snap @@ -0,0 +1 @@ +{"errors":[{"message":"EmailAddressType cannot parse the provided value. The provided value does not meet the RFC 5322 specification.","path":["email"],"extensions":{"code":"HC0002","actualType":"EmailAddress"}}]} diff --git a/backend/test/Integration/GraphQl/Users/__snapshots__/RegisterUserTests.NullOrEmptyEmail_IsUserError.snap b/backend/test/Integration/GraphQl/Users/__snapshots__/RegisterUserTests.NullOrEmptyEmail_IsUserError.snap deleted file mode 100644 index 817eff74d..000000000 --- a/backend/test/Integration/GraphQl/Users/__snapshots__/RegisterUserTests.NullOrEmptyEmail_IsUserError.snap +++ /dev/null @@ -1 +0,0 @@ -{"data":{"registerUser":{"user":null,"errors":[{"code":"INVALID_EMAIL","message":"Email \u0027\u0027 is invalid.","path":["input","email"]}]}}} diff --git a/backend/test/Integration/GraphQl/Users/__snapshots__/ResendUserEmailVerificationTests.NonLoggedInUser_IsAuthenticationError.snap b/backend/test/Integration/GraphQl/Users/__snapshots__/ResendUserEmailVerificationTests.NonLoggedInUser_IsAuthenticationError.snap index d19c76523..d4e756273 100644 --- a/backend/test/Integration/GraphQl/Users/__snapshots__/ResendUserEmailVerificationTests.NonLoggedInUser_IsAuthenticationError.snap +++ b/backend/test/Integration/GraphQl/Users/__snapshots__/ResendUserEmailVerificationTests.NonLoggedInUser_IsAuthenticationError.snap @@ -1 +1 @@ -{"errors":[{"message":"The current user is not authorized to access this resource.","locations":[{"line":2,"column":5}],"path":["resendUserEmailVerification"],"extensions":{"code":"AUTH_NOT_AUTHENTICATED"}}],"data":null} +{"errors":[{"message":"The current user is not authorized to access this resource.","path":["resendUserEmailVerification"],"extensions":{"code":"AUTH_NOT_AUTHENTICATED"}}],"data":null} diff --git a/backend/test/Integration/GraphQl/__snapshots__/GraphQlSchemaTests.IsUnchanged.snap b/backend/test/Integration/GraphQl/__snapshots__/GraphQlSchemaTests.IsUnchanged.snap index dd06ce132..f3ac716aa 100644 --- a/backend/test/Integration/GraphQl/__snapshots__/GraphQlSchemaTests.IsUnchanged.snap +++ b/backend/test/Integration/GraphQl/__snapshots__/GraphQlSchemaTests.IsUnchanged.snap @@ -3,46 +3,556 @@ schema { mutation: Mutation } -interface Approval { - approverId: Uuid! - keyFingerprint: String! - message: String! - query: String! - signature: String! - timestamp: DateTime! - variables: Any! -} - -interface Data { - appliedMethod: AppliedMethod! - approvals: [DataApproval!]! - componentId: Uuid! - createdAt: DateTime! - creatorId: Uuid! - databaseId: Uuid! - description: String - kind: DataKind! - locale: Locale! - name: String - resources: [GetHttpsResource!]! - resourceTree: GetHttpsResourceTree! - timestamp: DateTime! - uuid: Uuid! - warnings: [String!]! -} - -interface GetHttpsResourceTreeVertex { - value: GetHttpsResource! -} - -"The node interface is implemented by entities that have a global unique identifier." -interface Node { - id: ID! +type Query { + allCalorimetricData( + after: String + before: String + first: Int + last: Int + locale: Locale + where: CalorimetricDataPropositionInput + ): CalorimetricDataConnection! + @listSize( + assumedSize: 99 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 99 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allGeometricData( + after: String + before: String + first: Int + last: Int + locale: Locale + where: GeometricDataPropositionInput + ): GeometricDataConnection! + @listSize( + assumedSize: 99 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 99 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allHygrothermalData( + after: String + before: String + first: Int + last: Int + locale: Locale + where: HygrothermalDataPropositionInput + ): HygrothermalDataConnection! + @listSize( + assumedSize: 99 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 99 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allLifeCycleData( + after: String + before: String + first: Int + last: Int + locale: Locale + where: LifeCycleDataPropositionInput + ): LifeCycleDataConnection! + @listSize( + assumedSize: 99 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 99 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allOpticalData( + after: String + before: String + first: Int + last: Int + locale: Locale + where: OpticalDataPropositionInput + ): OpticalDataConnection! + @listSize( + assumedSize: 99 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 99 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allPhotovoltaicData( + after: String + before: String + first: Int + last: Int + locale: Locale + where: PhotovoltaicDataPropositionInput + ): PhotovoltaicDataConnection! + @listSize( + assumedSize: 99 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 99 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + anyDatabases( + "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: [DatabaseSortInput!] @cost(weight: "10") + where: DatabaseFilterInput @cost(weight: "10") + ): AnyDatabasesConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + calorimetricData(databaseId: Uuid!, id: Uuid!, locale: Locale): CalorimetricData + @cost(weight: "10") + component(id: Uuid!): Component @cost(weight: "10") + components( + "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: [ComponentSortInput!] @cost(weight: "10") + where: ComponentFilterInput @cost(weight: "10") + ): ComponentsConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + currentInstitution: Institution @cost(weight: "10") + currentOpenIdConnectApplication: OpenIdConnectApplication @cost(weight: "10") + currentOpenIdConnectTokenClientId: String + currentUser: User @cost(weight: "10") + database(id: Uuid!): Database @cost(weight: "10") + databases( + "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: [DatabaseSortInput!] @cost(weight: "10") + where: DatabaseFilterInput @cost(weight: "10") + ): DatabasesConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + dataFormat(id: Uuid!): DataFormat @cost(weight: "10") + dataFormats( + "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: [DataFormatSortInput!] @cost(weight: "10") + where: DataFormatFilterInput @cost(weight: "10") + ): DataFormatsConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + geometricData(databaseId: Uuid!, id: Uuid!, locale: Locale): GeometricData + @cost(weight: "10") + gnuPgKeyFingerprint(fingerprint: String!): GnuPgKeyFingerprint + @cost(weight: "10") + gnuPgKeyFingerprints( + "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: [GnuPgKeyFingerprintSortInput!] @cost(weight: "10") + where: GnuPgKeyFingerprintFilterInput @cost(weight: "10") + ): GnuPgKeyFingerprintsConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + hasCalorimetricData(locale: Locale, where: CalorimetricDataPropositionInput): Boolean! + @cost(weight: "10") + hasGeometricData(locale: Locale, where: GeometricDataPropositionInput): Boolean! + @cost(weight: "10") + hasHygrothermalData(locale: Locale, where: HygrothermalDataPropositionInput): Boolean! + @cost(weight: "10") + hasLifeCycleData(locale: Locale, where: LifeCycleDataPropositionInput): Boolean! + @cost(weight: "10") + hasOpticalData(locale: Locale, where: OpticalDataPropositionInput): Boolean! + @cost(weight: "10") + hasPhotovoltaicData(locale: Locale, where: PhotovoltaicDataPropositionInput): Boolean! + @cost(weight: "10") + hygrothermalData(databaseId: Uuid!, id: Uuid!, locale: Locale): HygrothermalData + @cost(weight: "10") + institution(id: Uuid!): Institution @cost(weight: "10") + institutions( + "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: [InstitutionSortInput!] @cost(weight: "10") + where: InstitutionFilterInput @cost(weight: "10") + ): InstitutionsConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + lifeCycleData(databaseId: Uuid!, id: Uuid!, locale: Locale): LifeCycleData + @cost(weight: "10") + method(id: Uuid!): Method @cost(weight: "10") + methods( + "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: [MethodSortInput!] @cost(weight: "10") + where: MethodFilterInput @cost(weight: "10") + ): MethodsConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @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") + openIdConnectApplication(id: Uuid!): OpenIdConnectApplication + @cost(weight: "10") + openIdConnectApplicationByClientId(clientId: String!): OpenIdConnectApplication + @cost(weight: "10") + openIdConnectApplications( + "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: [OpenIdConnectApplicationSortInput!] @cost(weight: "10") + where: OpenIdConnectApplicationFilterInput @cost(weight: "10") + ): OpenIdConnectApplicationsConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + openIdConnectAuthorization(id: Uuid!): OpenIdConnectAuthorization + @cost(weight: "10") + openIdConnectAuthorizations( + "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: [OpenIdConnectAuthorizationSortInput!] @cost(weight: "10") + where: OpenIdConnectAuthorizationFilterInput @cost(weight: "10") + ): OpenIdConnectAuthorizationsConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + openIdConnectToken(id: Uuid!): OpenIdConnectToken @cost(weight: "10") + openIdConnectTokens( + "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: [OpenIdConnectTokenSortInput!] @cost(weight: "10") + where: OpenIdConnectTokenFilterInput @cost(weight: "10") + ): OpenIdConnectTokensConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + opticalData(databaseId: Uuid!, id: Uuid!, locale: Locale): OpticalData + @cost(weight: "10") + pendingDatabases( + "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: [DatabaseSortInput!] @cost(weight: "10") + where: DatabaseFilterInput @cost(weight: "10") + ): PendingDatabasesConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + pendingInstitutions( + "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: [InstitutionSortInput!] @cost(weight: "10") + where: InstitutionFilterInput @cost(weight: "10") + ): PendingInstitutionsConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + photovoltaicData(databaseId: Uuid!, id: Uuid!, locale: Locale): PhotovoltaicData + @cost(weight: "10") + user(id: Uuid!): User @cost(weight: "10") + users( + "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: [UserSortInput!] @cost(weight: "10") + where: UserFilterInput @cost(weight: "10") + ): UsersConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") } -interface UserError { - message: String! - path: [String!]! +type Mutation { + addComponentAssembly(input: AddComponentAssemblyInput!): AddComponentAssemblyPayload! + @cost(weight: "10") + addComponentGeneralization(input: AddComponentGeneralizationInput!): AddComponentGeneralizationPayload! + @cost(weight: "10") + addComponentManufacturer(input: AddComponentManufacturerInput!): AddComponentManufacturerPayload! + @cost(weight: "10") + addComponentVariant(input: AddComponentVariantInput!): AddComponentVariantPayload! + @cost(weight: "10") + addGnuPgKeyFingerprint(input: AddGnuPgKeyFingerprintInput!): AddGnuPgKeyFingerprintPayload! + @cost(weight: "10") + addInstitutionMethodDeveloper(input: AddInstitutionMethodDeveloperInput!): AddInstitutionMethodDeveloperPayload! + @cost(weight: "10") + addInstitutionRepresentative(input: AddInstitutionRepresentativeInput!): AddInstitutionRepresentativePayload! + @cost(weight: "10") + addUserMethodDeveloper(input: AddUserMethodDeveloperInput!): AddUserMethodDeveloperPayload! + @cost(weight: "10") + addUserRole(input: AddUserRoleInput!): AddUserRolePayload! @cost(weight: "10") + allowGnuPgKeyFingerprint(input: AllowGnuPgKeyFingerprintInput!): AllowGnuPgKeyFingerprintPayload! + @cost(weight: "10") + changeInstitutionRepresentativeRole( + input: ChangeInstitutionRepresentativeRoleInput! + ): ChangeInstitutionRepresentativeRolePayload! @cost(weight: "10") + changeUserEmail(input: ChangeUserEmailInput!): ChangeUserEmailPayload! + @cost(weight: "10") + changeUserPassword(input: ChangeUserPasswordInput!): ChangeUserPasswordPayload! + @cost(weight: "10") + confirmComponentManufacturer(input: ConfirmComponentManufacturerInput!): ConfirmComponentManufacturerPayload! + @cost(weight: "10") + confirmInstitutionMethodDeveloper( + input: ConfirmInstitutionMethodDeveloperInput! + ): ConfirmInstitutionMethodDeveloperPayload! @cost(weight: "10") + confirmInstitutionRepresentative( + input: ConfirmInstitutionRepresentativeInput! + ): ConfirmInstitutionRepresentativePayload! @cost(weight: "10") + confirmUserEmail(input: ConfirmUserEmailInput!): ConfirmUserEmailPayload! + @cost(weight: "10") + confirmUserEmailChange(input: ConfirmUserEmailChangeInput!): ConfirmUserEmailChangePayload! + @cost(weight: "10") + confirmUserMethodDeveloper(input: ConfirmUserMethodDeveloperInput!): ConfirmUserMethodDeveloperPayload! + @cost(weight: "10") + createComponent(input: CreateComponentInput!): CreateComponentPayload! + @cost(weight: "10") + createDatabase(input: CreateDatabaseInput!): CreateDatabasePayload! + @cost(weight: "10") + createDataFormat(input: CreateDataFormatInput!): CreateDataFormatPayload! + @cost(weight: "10") + createInstitution(input: CreateInstitutionInput!): CreateInstitutionPayload! + @cost(weight: "10") + createMethod(input: CreateMethodInput!): CreateMethodPayload! + @cost(weight: "10") + createOpenIdConnectApplication(input: CreateOpenIdConnectApplicationInput!): CreateOpenIdConnectApplicationPayload! + @cost(weight: "10") + deleteInstitution(input: DeleteInstitutionInput!): DeleteInstitutionPayload! + @cost(weight: "10") + deleteOpenIdConnectApplication(input: DeleteOpenIdConnectApplicationInput!): DeleteOpenIdConnectApplicationPayload! + @cost(weight: "10") + deleteOpenIdConnectAuthorization( + input: DeleteOpenIdConnectAuthorizationInput! + ): DeleteOpenIdConnectAuthorizationPayload! @cost(weight: "10") + deletePersonalUserData(input: DeletePersonalUserDataInput!): DeletePersonalUserDataPayload! + @cost(weight: "10") + deleteUser(input: DeleteUserInput!): DeleteUserPayload! @cost(weight: "10") + disableUserTwoFactorAuthentication: DisableUserTwoFactorAuthenticationPayload! + @cost(weight: "10") + enableUserTwoFactorAuthenticator( + input: EnableUserTwoFactorAuthenticatorInput! + ): EnableUserTwoFactorAuthenticatorPayload! @cost(weight: "10") + forbidGnuPgKeyFingerprint(input: ForbidGnuPgKeyFingerprintInput!): ForbidGnuPgKeyFingerprintPayload! + @cost(weight: "10") + forgetUserTwoFactorAuthenticationClient: ForgetUserTwoFactorAuthenticationClientPayload! + @cost(weight: "10") + generateUserTwoFactorAuthenticatorSharedKeyAndQrCodeUri: GenerateUserTwoFactorAuthenticatorSharedKeyAndQrCodeUriPayload! + @cost(weight: "10") + generateUserTwoFactorRecoveryCodes: GenerateUserTwoFactorRecoveryCodesPayload! + @cost(weight: "10") + loginUser(input: LoginUserInput!): LoginUserPayload! @cost(weight: "10") + loginUserWithRecoveryCode(input: LoginUserWithRecoveryCodeInput!): LoginUserWithRecoveryCodePayload! + @cost(weight: "10") + loginUserWithTwoFactorCode(input: LoginUserWithTwoFactorCodeInput!): LoginUserWithTwoFactorCodePayload! + @cost(weight: "10") + logoutUser: LogoutUserPayload! @cost(weight: "10") + registerUser(input: RegisterUserInput!): RegisterUserPayload! + @cost(weight: "10") + removeComponentAssembly(input: RemoveComponentAssemblyInput!): RemoveComponentAssemblyPayload! + @cost(weight: "10") + removeComponentGeneralization(input: RemoveComponentGeneralizationInput!): RemoveComponentGeneralizationPayload! + @cost(weight: "10") + removeComponentManufacturer(input: RemoveComponentManufacturerInput!): RemoveComponentManufacturerPayload! + @cost(weight: "10") + removeComponentVariant(input: RemoveComponentVariantInput!): RemoveComponentVariantPayload! + @cost(weight: "10") + removeInstitutionMethodDeveloper( + input: RemoveInstitutionMethodDeveloperInput! + ): RemoveInstitutionMethodDeveloperPayload! @cost(weight: "10") + removeInstitutionRepresentative(input: RemoveInstitutionRepresentativeInput!): RemoveInstitutionRepresentativePayload! + @cost(weight: "10") + removeUserMethodDeveloper(input: RemoveUserMethodDeveloperInput!): RemoveUserMethodDeveloperPayload! + @cost(weight: "10") + removeUserRole(input: RemoveUserRoleInput!): RemoveUserRolePayload! + @cost(weight: "10") + requestUserPasswordReset(input: RequestUserPasswordResetInput!): RequestUserPasswordResetPayload! + @cost(weight: "10") + resendUserEmailConfirmation(input: ResendUserEmailConfirmationInput!): ResendUserEmailConfirmationPayload! + @cost(weight: "10") + resendUserEmailVerification: ResendUserEmailVerificationPayload! + @cost(weight: "10") + resetOpenIdConnectApplicationClientSecret( + input: ResetOpenIdConnectApplicationClientSecretInput! + ): ResetOpenIdConnectApplicationClientSecretPayload! @cost(weight: "10") + resetUserPassword(input: ResetUserPasswordInput!): ResetUserPasswordPayload! + @cost(weight: "10") + resetUserTwoFactorAuthenticator: ResetUserTwoFactorAuthenticatorPayload! + @cost(weight: "10") + revokeOpenIdConnectToken(input: RevokeOpenIdConnectTokenInput!): RevokeOpenIdConnectTokenPayload! + @cost(weight: "10") + setComponentExtras(input: SetComponentExtrasInput!): SetComponentExtrasPayload! + @cost(weight: "10") + setInstitutionExtras(input: SetInstitutionExtrasInput!): SetInstitutionExtrasPayload! + @cost(weight: "10") + setUserPassword(input: SetUserPasswordInput!): SetUserPasswordPayload! + @cost(weight: "10") + setUserPhoneNumber(input: SetUserPhoneNumberInput!): SetUserPhoneNumberPayload! + @cost(weight: "10") + switchInstitutionOperatingState(input: SwitchInstitutionOperatingStateInput!): SwitchInstitutionOperatingStatePayload! + @cost(weight: "10") + updateComponent(input: UpdateComponentInput!): UpdateComponentPayload! + @cost(weight: "10") + updateComponentAssembly(input: UpdateComponentAssemblyInput!): UpdateComponentAssemblyPayload! + @cost(weight: "10") + updateDatabase(input: UpdateDatabaseInput!): UpdateDatabasePayload! + @cost(weight: "10") + updateDataFormat(input: UpdateDataFormatInput!): UpdateDataFormatPayload! + @cost(weight: "10") + updateInstitution(input: UpdateInstitutionInput!): UpdateInstitutionPayload! + @cost(weight: "10") + updateMethod(input: UpdateMethodInput!): UpdateMethodPayload! + @cost(weight: "10") + updateOpenIdConnectApplication(input: UpdateOpenIdConnectApplicationInput!): UpdateOpenIdConnectApplicationPayload! + @cost(weight: "10") + verifyDatabase(input: VerifyDatabaseInput!): VerifyDatabasePayload! + @cost(weight: "10") + verifyInstitution(input: VerifyInstitutionInput!): VerifyInstitutionPayload! + @cost(weight: "10") } type AddComponentAssemblyError implements UserError { @@ -172,6 +682,24 @@ type AllowGnuPgKeyFingerprintPayload { query: Query! } +"A connection to a list of items." +type AnyDatabasesConnection { + "A list of edges." + edges: [AnyDatabasesEdge!] + "Information to aid in pagination." + pageInfo: PageInfo! + "Identifies the total count of items in the connection." + totalCount: Int! @cost(weight: "10") +} + +"An edge in a connection." +type AnyDatabasesEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Database! +} + type AppliedMethod { arguments: [NamedMethodArgument!]! method: Method @cost(weight: "10") @@ -179,7 +707,7 @@ type AppliedMethod { sources: [NamedMethodSource!]! } -type CalorimetricData implements Data { +type CalorimetricData implements Node & Data { appliedMethod: AppliedMethod! approvals: [DataApproval!]! component: Component @cost(weight: "10") @@ -204,14 +732,14 @@ type CalorimetricData implements Data { } type CalorimetricDataConnection { - edges: [CalorimetricDataEdge!]! - pageInfo: PageInfo! - totalCount: NonNegativeInt! + edges: [CalorimetricDataEdge!] @cost(weight: "0") + pageInfo: PageInfo! @cost(weight: "0") + totalCount: Int! } type CalorimetricDataEdge { cursor: String! - node: CalorimetricData! + node: CalorimetricData! @cost(weight: "0") } type ChangeInstitutionRepresentativeRoleError implements UserError { @@ -259,103 +787,211 @@ type CielabColor { type Component implements Node { abbreviation: String - assembledOf(where: ComponentAssembledOfFilterInput @cost(weight: "10")): ComponentAssembledOfConnection! @cost(weight: "10") + allCalorimetricData( + after: String + before: String + first: Int + last: Int + locale: Locale + where: CalorimetricDataPropositionInput + ): CalorimetricDataConnection! + @listSize( + assumedSize: 99 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 99 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allGeometricData( + after: String + before: String + first: Int + last: Int + locale: Locale + where: GeometricDataPropositionInput + ): GeometricDataConnection! + @listSize( + assumedSize: 99 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 99 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allHygrothermalData( + after: String + before: String + first: Int + last: Int + locale: Locale + where: HygrothermalDataPropositionInput + ): HygrothermalDataConnection! + @listSize( + assumedSize: 99 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 99 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allLifeCycleData( + after: String + before: String + first: Int + last: Int + locale: Locale + where: LifeCycleDataPropositionInput + ): LifeCycleDataConnection! + @listSize( + assumedSize: 99 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 99 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allOpticalData( + after: String + before: String + first: Int + last: Int + locale: Locale + where: OpticalDataPropositionInput + ): OpticalDataConnection! + @listSize( + assumedSize: 99 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 99 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allPhotovoltaicData( + after: String + before: String + first: Int + last: Int + locale: Locale + where: PhotovoltaicDataPropositionInput + ): PhotovoltaicDataConnection! + @listSize( + assumedSize: 99 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 99 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + assembledOf( + order: [ComponentAssembledOfSortInput!] @cost(weight: "10") + where: ComponentAssembledOfFilterInput @cost(weight: "10") + ): ComponentAssembledOfConnection! @cost(weight: "10") availability: OpenEndedDateTimeRange categories: [ComponentCategory!]! - concretizationOf(where: ComponentConcretizationOfFilterInput @cost(weight: "10")): ComponentConcretizationOfConnection! @cost(weight: "10") + concretizationOf( + order: [ComponentConcretizationOfSortInput!] @cost(weight: "10") + where: ComponentConcretizationOfFilterInput @cost(weight: "10") + ): ComponentConcretizationOfConnection! @cost(weight: "10") + createdAt: DateTime! description: String! extras: Any - generalizationOf(where: ComponentGeneralizationOfFilterInput @cost(weight: "10")): ComponentGeneralizationOfConnection! @cost(weight: "10") + generalizationOf( + order: [ComponentGeneralizationOfSortInput!] @cost(weight: "10") + where: ComponentGeneralizationOfFilterInput @cost(weight: "10") + ): ComponentGeneralizationOfConnection! @cost(weight: "10") id: ID! - isAuthorizedToUpdateNode: Boolean! @cost(weight: "10") + isAuthorizedToUpdateNode: Boolean! @cost(weight: "1") manager: ComponentManagerEdge! @cost(weight: "10") - manufacturers(where: ComponentManufacturerFilterInput @cost(weight: "10")): ComponentManufacturerConnection! @cost(weight: "10") + manufacturers( + order: [ComponentManufacturerSortInput!] @cost(weight: "10") + where: ComponentManufacturerFilterInput @cost(weight: "10") + ): ComponentManufacturerConnection! @cost(weight: "10") name: String! - partOf(where: ComponentPartOfFilterInput @cost(weight: "10")): ComponentPartOfConnection! @cost(weight: "10") - pendingManufacturers(where: ComponentManufacturerFilterInput @cost(weight: "10")): PendingComponentManufacturerConnection @authorize(policy: "WriteScope") @cost(weight: "10") - prime: PrimeSurfaceOrDirection @cost(weight: "10") + partOf( + order: [ComponentPartOfSortInput!] @cost(weight: "10") + where: ComponentPartOfFilterInput @cost(weight: "10") + ): ComponentPartOfConnection! @cost(weight: "10") + pendingManufacturers( + order: [ComponentManufacturerSortInput!] @cost(weight: "10") + where: ComponentManufacturerFilterInput @cost(weight: "10") + ): PendingComponentManufacturerConnection @cost(weight: "10") + prime: PrimeSurfaceOrDirection @cost(weight: "0") switchableLayers: DescriptionOrReference - uuid: Uuid! @cost(weight: "10") - variantOf(where: ComponentVariantOfFilterInput @cost(weight: "10")): ComponentVariantOfConnection! @cost(weight: "10") + updatedAt: DateTime! + uuid: Uuid! + variantOf( + order: [ComponentVariantOfSortInput!] @cost(weight: "10") + where: ComponentVariantOfFilterInput @cost(weight: "10") + ): ComponentVariantOfConnection! @cost(weight: "10") } type ComponentAssembledOfConnection { - edges: [ComponentAssembledOfEdge!]! @cost(weight: "10") - isAuthorizedToAddEdge: Boolean! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + edges: [ComponentAssembledOfEdge!]! @cost(weight: "0") + isAuthorizedToAddEdge: Boolean! @cost(weight: "1") + totalCount: Int! } type ComponentAssembledOfEdge { - index: Byte - isAuthorizedToRemoveEdge: Boolean! @cost(weight: "10") - isAuthorizedToUpdateEdge: Boolean! @cost(weight: "10") - node: Component! @cost(weight: "10") + index: UnsignedByte + isAuthorizedToRemoveEdge: Boolean! @cost(weight: "1") + isAuthorizedToUpdateEdge: Boolean! @cost(weight: "1") + node: Component! @cost(weight: "0") primeSurface: PrimeSurface } type ComponentConcretizationOfConnection { - edges: [ComponentConcretizationOfEdge!]! @cost(weight: "10") - isAuthorizedToAddEdge: Boolean! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + edges: [ComponentConcretizationOfEdge!]! @cost(weight: "0") + isAuthorizedToAddEdge: Boolean! @cost(weight: "1") + totalCount: Int! } type ComponentConcretizationOfEdge { - isAuthorizedToRemoveEdge: Boolean! @cost(weight: "10") - node: Component! @cost(weight: "10") + isAuthorizedToRemoveEdge: Boolean! @cost(weight: "1") + node: Component! @cost(weight: "0") } type ComponentGeneralizationOfConnection { - edges: [ComponentGeneralizationOfEdge!]! @cost(weight: "10") - isAuthorizedToAddEdge: Boolean! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + edges: [ComponentGeneralizationOfEdge!]! @cost(weight: "0") + isAuthorizedToAddEdge: Boolean! @cost(weight: "1") + totalCount: Int! } type ComponentGeneralizationOfEdge { - isAuthorizedToRemoveEdge: Boolean! @cost(weight: "10") - node: Component! @cost(weight: "10") + isAuthorizedToRemoveEdge: Boolean! @cost(weight: "1") + node: Component! @cost(weight: "0") } type ComponentManagerEdge { - node: Institution! @cost(weight: "10") + node: Institution! @cost(weight: "0") } type ComponentManufacturerConnection { - edges: [ComponentManufacturerEdge!]! @cost(weight: "10") - isAuthorizedToAddEdge: Boolean! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + edges: [ComponentManufacturerEdge!]! @cost(weight: "0") + isAuthorizedToAddEdge: Boolean! @cost(weight: "1") + totalCount: Int! } type ComponentManufacturerEdge { - isAuthorizedToConfirmEdge: Boolean! @cost(weight: "10") - isAuthorizedToRemoveEdge: Boolean! @cost(weight: "10") - node: Institution! @cost(weight: "10") + isAuthorizedToConfirmEdge: Boolean! @cost(weight: "1") + isAuthorizedToRemoveEdge: Boolean! @cost(weight: "1") + node: Institution! @cost(weight: "0") } type ComponentPartOfConnection { - edges: [ComponentPartOfEdge!]! @cost(weight: "10") - isAuthorizedToAddEdge: Boolean! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + edges: [ComponentPartOfEdge!]! @cost(weight: "0") + isAuthorizedToAddEdge: Boolean! @cost(weight: "1") + totalCount: Int! } type ComponentPartOfEdge { - index: Byte - isAuthorizedToRemoveEdge: Boolean! @cost(weight: "10") - isAuthorizedToUpdateEdge: Boolean! @cost(weight: "10") - node: Component! @cost(weight: "10") + index: UnsignedByte + isAuthorizedToRemoveEdge: Boolean! @cost(weight: "1") + isAuthorizedToUpdateEdge: Boolean! @cost(weight: "1") + node: Component! @cost(weight: "0") primeSurface: PrimeSurface } -type ComponentVariantOfConnection { - edges: [ComponentVariantOfEdge!]! @cost(weight: "10") - isAuthorizedToAddEdge: Boolean! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") -} - -type ComponentVariantOfEdge { - isAuthorizedToRemoveEdge: Boolean! @cost(weight: "10") - node: Component! @cost(weight: "10") -} - "A connection to a list of items." type ComponentsConnection { "A list of edges." @@ -374,6 +1010,17 @@ type ComponentsEdge { node: Component! } +type ComponentVariantOfConnection { + edges: [ComponentVariantOfEdge!]! @cost(weight: "0") + isAuthorizedToAddEdge: Boolean! @cost(weight: "1") + totalCount: Int! +} + +type ComponentVariantOfEdge { + isAuthorizedToRemoveEdge: Boolean! @cost(weight: "1") + node: Component! @cost(weight: "0") +} + type ConfirmComponentManufacturerError implements UserError { code: ConfirmComponentManufacturerErrorCode! message: String! @@ -451,10 +1098,10 @@ type ConfirmUserMethodDeveloperPayload { } type ContactInformation { - emailAddress: String + emailAddress: EmailAddress isEmailAddressConfirmed: Boolean! isPhoneNumberConfirmed: Boolean! - phoneNumber: String + phoneNumber: PhoneNumber postalAddress: String websiteLocator: Url } @@ -471,27 +1118,27 @@ type CreateComponentPayload { query: Query! } -type CreateDataFormatError implements UserError { - code: CreateDataFormatErrorCode! +type CreateDatabaseError implements UserError { + code: CreateDatabaseErrorCode! message: String! path: [String!]! } -type CreateDataFormatPayload { - dataFormat: DataFormat - errors: [CreateDataFormatError!] +type CreateDatabasePayload { + database: Database + errors: [CreateDatabaseError!] query: Query! } -type CreateDatabaseError implements UserError { - code: CreateDatabaseErrorCode! +type CreateDataFormatError implements UserError { + code: CreateDataFormatErrorCode! message: String! path: [String!]! } -type CreateDatabasePayload { - database: Database - errors: [CreateDatabaseError!] +type CreateDataFormatPayload { + dataFormat: DataFormat + errors: [CreateDataFormatError!] query: Query! } @@ -533,7 +1180,7 @@ type CreateOpenIdConnectApplicationPayload { } type CrossDatabaseDataReference { - database: Institution @cost(weight: "10") + database: Database @cost(weight: "10") databaseId: Uuid! dataId: Uuid! dataKind: DataKind! @@ -545,45 +1192,156 @@ type DataApproval implements Approval { approverId: Uuid! keyFingerprint: String! message: String! - query: String! + query: GraphQlQuery! signature: String! statement: Reference! timestamp: DateTime! variables: Any! } -type DataConnection { - edges: [DataEdge!]! - pageInfo: PageInfo! - totalCount: NonNegativeInt! -} - -type DataEdge { - cursor: String! - node: Data! -} - -type DataFormat implements Node { +type Database implements Node { + allCalorimetricData( + after: String + before: String + first: Int + last: Int + locale: Locale + where: CalorimetricDataPropositionInput + ): CalorimetricDataConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allGeometricData( + after: String + before: String + first: Int + last: Int + locale: Locale + where: GeometricDataPropositionInput + ): GeometricDataConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allHygrothermalData( + after: String + before: String + first: Int + last: Int + locale: Locale + where: HygrothermalDataPropositionInput + ): HygrothermalDataConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allLifeCycleData( + after: String + before: String + first: Int + last: Int + locale: Locale + where: LifeCycleDataPropositionInput + ): LifeCycleDataConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allOpticalData( + after: String + before: String + first: Int + last: Int + locale: Locale + where: OpticalDataPropositionInput + ): OpticalDataConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + allPhotovoltaicData( + after: String + before: String + first: Int + last: Int + locale: Locale + where: PhotovoltaicDataPropositionInput + ): PhotovoltaicDataConnection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 100 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + calorimetricData(id: Uuid!, locale: Locale): CalorimetricData + @cost(weight: "10") + createdAt: DateTime! + data(id: Uuid!, kind: DataKind!, locale: Locale): Data @cost(weight: "10") description: String! - extension: String + geometricData(id: Uuid!, locale: Locale): GeometricData @cost(weight: "10") + hasCalorimetricData(locale: Locale, where: CalorimetricDataPropositionInput): Boolean + @cost(weight: "10") + hasData(kind: DataKind!, locale: Locale, where: DataPropositionInput): Boolean + @cost(weight: "10") + hasGeometricData(locale: Locale, where: GeometricDataPropositionInput): Boolean + @cost(weight: "10") + hasHygrothermalData(locale: Locale, where: HygrothermalDataPropositionInput): Boolean + @cost(weight: "10") + hasLifeCycleData(locale: Locale, where: LifeCycleDataPropositionInput): Boolean + @cost(weight: "10") + hasOpticalData(locale: Locale, where: OpticalDataPropositionInput): Boolean + @cost(weight: "10") + hasPhotovoltaicData(locale: Locale, where: PhotovoltaicDataPropositionInput): Boolean + @cost(weight: "10") + hygrothermalData(id: Uuid!, locale: Locale): HygrothermalData + @cost(weight: "10") id: ID! - isAuthorizedToUpdateNode: Boolean! @cost(weight: "10") - manager: DataFormatManagerEdge! @cost(weight: "10") - mediaType: String! + isAuthorizedToUpdateNode: Boolean! @cost(weight: "1") + isAuthorizedToVerifyNode: Boolean! @cost(weight: "1") + lifeCycleData(id: Uuid!, locale: Locale): LifeCycleData @cost(weight: "10") + locator: Url! name: String! - reference: Reference @cost(weight: "10") - schemaLocator: Url - uuid: Uuid! @cost(weight: "10") + operator: DatabaseOperatorEdge! @cost(weight: "10") + opticalData(id: Uuid!, locale: Locale): OpticalData @cost(weight: "10") + photovoltaicData(id: Uuid!, locale: Locale): PhotovoltaicData + @cost(weight: "10") + updatedAt: DateTime! + uuid: Uuid! + verificationCode: String! + verificationState: DatabaseVerificationState! } -type DataFormatManagerEdge { - node: Institution! @cost(weight: "10") +type DatabaseOperatorEdge { + node: Institution! @cost(weight: "0") } "A connection to a list of items." -type DataFormatsConnection { +type DatabasesConnection { "A list of edges." - edges: [DataFormatsEdge!] + edges: [DatabasesEdge!] "Information to aid in pagination." pageInfo: PageInfo! "Identifies the total count of items in the connection." @@ -591,54 +1349,36 @@ type DataFormatsConnection { } "An edge in a connection." -type DataFormatsEdge { +type DatabasesEdge { "A cursor for use in pagination." cursor: String! "The item at the end of the edge." - node: DataFormat! + node: Database! } -type Database implements Node { - allCalorimetricData(after: String before: String first: NonNegativeInt last: NonNegativeInt locale: Locale where: CalorimetricDataPropositionInput): CalorimetricDataConnection @cost(weight: "10") - allGeometricData(after: String before: String first: NonNegativeInt last: NonNegativeInt locale: Locale where: GeometricDataPropositionInput): GeometricDataConnection @cost(weight: "10") - allHygrothermalData(after: String before: String first: NonNegativeInt last: NonNegativeInt locale: Locale where: HygrothermalDataPropositionInput): HygrothermalDataConnection @cost(weight: "10") - allLifeCycleData(after: String before: String first: NonNegativeInt last: NonNegativeInt locale: Locale where: LifeCycleDataPropositionInput): LifeCycleDataConnection @cost(weight: "10") - allOpticalData(after: String before: String first: NonNegativeInt last: NonNegativeInt locale: Locale where: OpticalDataPropositionInput): OpticalDataConnection @cost(weight: "10") - allPhotovoltaicData(after: String before: String first: NonNegativeInt last: NonNegativeInt locale: Locale where: PhotovoltaicDataPropositionInput): PhotovoltaicDataConnection @cost(weight: "10") - calorimetricData(id: Uuid! locale: Locale): CalorimetricData @cost(weight: "10") - data(id: Uuid! kind: DataKind! locale: Locale): Data @cost(weight: "10") +type DataFormat implements Node { + createdAt: DateTime! description: String! - geometricData(id: Uuid! locale: Locale): GeometricData @cost(weight: "10") - hasCalorimetricData(locale: Locale where: CalorimetricDataPropositionInput): Boolean @cost(weight: "10") - hasData(kind: DataKind! locale: Locale where: DataPropositionInput): Boolean @cost(weight: "10") - hasGeometricData(locale: Locale where: GeometricDataPropositionInput): Boolean @cost(weight: "10") - hasHygrothermalData(locale: Locale where: HygrothermalDataPropositionInput): Boolean @cost(weight: "10") - hasLifeCycleData(locale: Locale where: LifeCycleDataPropositionInput): Boolean @cost(weight: "10") - hasOpticalData(locale: Locale where: OpticalDataPropositionInput): Boolean @cost(weight: "10") - hasPhotovoltaicData(locale: Locale where: PhotovoltaicDataPropositionInput): Boolean @cost(weight: "10") - hygrothermalData(id: Uuid! locale: Locale): HygrothermalData @cost(weight: "10") + extension: String id: ID! - isAuthorizedToUpdateNode: Boolean! @cost(weight: "10") - isAuthorizedToVerifyNode: Boolean! @cost(weight: "10") - lifeCycleData(id: Uuid! locale: Locale): LifeCycleData @cost(weight: "10") - locator: Url! + isAuthorizedToUpdateNode: Boolean! @cost(weight: "1") + manager: DataFormatManagerEdge! @cost(weight: "10") + mediaType: String! name: String! - operator: DatabaseOperatorEdge! @cost(weight: "10") - opticalData(id: Uuid! locale: Locale): OpticalData @cost(weight: "10") - photovoltaicData(id: Uuid! locale: Locale): PhotovoltaicData @cost(weight: "10") - uuid: Uuid! @cost(weight: "10") - verificationCode: String! - verificationState: DatabaseVerificationState! + reference: Reference @cost(weight: "0") + schemaLocator: Url + updatedAt: DateTime! + uuid: Uuid! } -type DatabaseOperatorEdge { - node: Institution! @cost(weight: "10") +type DataFormatManagerEdge { + node: Institution! @cost(weight: "0") } "A connection to a list of items." -type DatabasesConnection { +type DataFormatsConnection { "A list of edges." - edges: [DatabasesEdge!] + edges: [DataFormatsEdge!] "Information to aid in pagination." pageInfo: PageInfo! "Identifies the total count of items in the connection." @@ -646,11 +1386,11 @@ type DatabasesConnection { } "An edge in a connection." -type DatabasesEdge { +type DataFormatsEdge { "A cursor for use in pagination." cursor: String! "The item at the end of the edge." - node: Database! + node: DataFormat! } type DeleteInstitutionError implements UserError { @@ -682,6 +1422,7 @@ type DeleteOpenIdConnectAuthorizationError implements UserError { } type DeleteOpenIdConnectAuthorizationPayload { + application: OpenIdConnectApplication errors: [DeleteOpenIdConnectAuthorizationError!] query: Query! } @@ -712,7 +1453,7 @@ type DeleteUserPayload { type DescriptionOrReference { description: String - reference: Reference @cost(weight: "10") + reference: Reference @cost(weight: "0") } type DisableUserTwoFactorAuthenticationError implements UserError { @@ -799,7 +1540,7 @@ type GenerateUserTwoFactorRecoveryCodesPayload { user: User } -type GeometricData implements Data { +type GeometricData implements Node & Data { appliedMethod: AppliedMethod! approvals: [DataApproval!]! component: Component @cost(weight: "10") @@ -823,14 +1564,14 @@ type GeometricData implements Data { } type GeometricDataConnection { - edges: [GeometricDataEdge!]! - pageInfo: PageInfo! - totalCount: NonNegativeInt! + edges: [GeometricDataEdge!] @cost(weight: "0") + pageInfo: PageInfo! @cost(weight: "0") + totalCount: Int! } type GeometricDataEdge { cursor: String! - node: GeometricData! + node: GeometricData! @cost(weight: "0") } type GetHttpsResource { @@ -849,14 +1590,14 @@ type GetHttpsResourceTree { type GetHttpsResourceTreeNonRootVertex implements GetHttpsResourceTreeVertex { appliedConversionMethod: ToTreeVertexAppliedConversionMethod! - parentId: String! + parentId: ID! value: GetHttpsResource! - vertexId: String! + vertexId: ID! } type GetHttpsResourceTreeRoot implements GetHttpsResourceTreeVertex { value: GetHttpsResource! - vertexId: String! + vertexId: ID! } type GnuPgKeyFingerprint implements Node { @@ -866,18 +1607,15 @@ type GnuPgKeyFingerprint implements Node { forbiddenAt: DateTime id: ID! institution: GnuPgKeyFingerprintInstitutionEdge! @cost(weight: "10") - isAuthorizedToAllowNode: Boolean! @cost(weight: "10") - isAuthorizedToForbidNode: Boolean! @cost(weight: "10") + isAuthorizedToAllowNode: Boolean! @cost(weight: "1") + isAuthorizedToForbidNode: Boolean! @cost(weight: "1") + updatedAt: DateTime! user: GnuPgKeyFingerprintUserEdge! @cost(weight: "10") - uuid: Uuid! @cost(weight: "10") + uuid: Uuid! } type GnuPgKeyFingerprintInstitutionEdge { - node: Institution! @cost(weight: "10") -} - -type GnuPgKeyFingerprintUserEdge { - node: User! @cost(weight: "10") + node: Institution! @cost(weight: "0") } "A connection to a list of items." @@ -898,7 +1636,11 @@ type GnuPgKeyFingerprintsEdge { node: GnuPgKeyFingerprint! } -type HygrothermalData implements Data { +type GnuPgKeyFingerprintUserEdge { + node: User! @cost(weight: "0") +} + +type HygrothermalData implements Node & Data { appliedMethod: AppliedMethod! approvals: [DataApproval!]! component: Component @cost(weight: "10") @@ -921,60 +1663,172 @@ type HygrothermalData implements Data { } type HygrothermalDataConnection { - edges: [HygrothermalDataEdge!]! - pageInfo: PageInfo! - totalCount: NonNegativeInt! + edges: [HygrothermalDataEdge!] @cost(weight: "0") + pageInfo: PageInfo! @cost(weight: "0") + totalCount: Int! } type HygrothermalDataEdge { cursor: String! - node: HygrothermalData! + node: HygrothermalData! @cost(weight: "0") } type Institution implements Node { abbreviation: String contact: ContactInformation + createdAt: DateTime! description: String! - developedMethods(where: InstitutionDevelopedMethodFilterInput @cost(weight: "10")): InstitutionDevelopedMethodConnection! @cost(weight: "10") + developedMethods( + "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: [InstitutionDevelopedMethodSortInput!] @cost(weight: "10") + where: InstitutionDevelopedMethodFilterInput @cost(weight: "10") + ): InstitutionDevelopedMethodConnection! @cost(weight: "10") extras: Any - gnuPgKeyFingerprints(where: InstitutionGnuPgKeyFingerprintFilterInput @cost(weight: "10")): InstitutionGnuPgKeyFingerprintConnection! @cost(weight: "10") - hasGnuPgKeyFingerprint(where: InstitutionGnuPgKeyFingerprintFilterInput @cost(weight: "10")): Boolean! @cost(weight: "10") + gnuPgKeyFingerprints( + order: [InstitutionGnuPgKeyFingerprintSortInput!] @cost(weight: "10") + where: InstitutionGnuPgKeyFingerprintFilterInput @cost(weight: "10") + ): InstitutionGnuPgKeyFingerprintConnection! @cost(weight: "10") + hasGnuPgKeyFingerprint( + order: [InstitutionGnuPgKeyFingerprintSortInput!] @cost(weight: "10") + where: InstitutionGnuPgKeyFingerprintFilterInput @cost(weight: "10") + ): Boolean! @cost(weight: "10") id: ID! - isAuthorizedToDeleteNode: Boolean! @cost(weight: "10") - isAuthorizedToSwitchOperatingStateOfNode: Boolean! @cost(weight: "10") - isAuthorizedToUpdateNode: Boolean! @cost(weight: "10") - isAuthorizedToVerifyNode: Boolean! @cost(weight: "10") - managedComponents(where: InstitutionManagedComponentFilterInput @cost(weight: "10")): InstitutionManagedComponentConnection! @cost(weight: "10") - managedDataFormats(where: InstitutionManagedDataFormatFilterInput @cost(weight: "10")): InstitutionManagedDataFormatConnection! @cost(weight: "10") - managedInstitutions(where: InstitutionManagedInstitutionFilterInput @cost(weight: "10")): InstitutionManagedInstitutionConnection! @cost(weight: "10") - managedMethods(where: InstitutionManagedMethodFilterInput @cost(weight: "10")): InstitutionManagedMethodConnection! @cost(weight: "10") + isAuthorizedToDeleteNode: Boolean! @cost(weight: "1") + isAuthorizedToSwitchOperatingStateOfNode: Boolean! @cost(weight: "1") + isAuthorizedToUpdateNode: Boolean! @cost(weight: "1") + isAuthorizedToVerifyNode: Boolean! @cost(weight: "1") + managedComponents( + "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: [InstitutionManagedComponentSortInput!] @cost(weight: "10") + where: InstitutionManagedComponentFilterInput @cost(weight: "10") + ): InstitutionManagedComponentConnection! @cost(weight: "10") + managedDataFormats( + "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: [InstitutionManagedDataFormatSortInput!] @cost(weight: "10") + where: InstitutionManagedDataFormatFilterInput @cost(weight: "10") + ): InstitutionManagedDataFormatConnection! @cost(weight: "10") + managedInstitutions( + "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: [InstitutionManagedInstitutionSortInput!] @cost(weight: "10") + where: InstitutionManagedInstitutionFilterInput @cost(weight: "10") + ): InstitutionManagedInstitutionConnection! @cost(weight: "10") + managedMethods( + "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: [InstitutionManagedMethodSortInput!] @cost(weight: "10") + where: InstitutionManagedMethodFilterInput @cost(weight: "10") + ): InstitutionManagedMethodConnection! @cost(weight: "10") manager: InstitutionManagerEdge @cost(weight: "10") - manufacturedComponents(where: InstitutionManufacturedComponentFilterInput @cost(weight: "10")): InstitutionManufacturedComponentConnection! @cost(weight: "10") + manufacturedComponents( + "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: [InstitutionManufacturedComponentSortInput!] @cost(weight: "10") + where: InstitutionManufacturedComponentFilterInput @cost(weight: "10") + ): InstitutionManufacturedComponentConnection! @cost(weight: "10") name: String! - openIdConnectApplications(where: InstitutionOwnedOpenIdConnectApplicationFilterInput @cost(weight: "10")): InstitutionOwnedOpenIdConnectApplicationConnection! @cost(weight: "10") - operatedDatabases(where: InstitutionOperatedDatabaseFilterInput @cost(weight: "10")): InstitutionOperatedDatabaseConnection! @cost(weight: "10") + openIdConnectApplications( + order: [InstitutionOwnedOpenIdConnectApplicationSortInput!] @cost(weight: "10") + where: InstitutionOwnedOpenIdConnectApplicationFilterInput @cost(weight: "10") + ): InstitutionOwnedOpenIdConnectApplicationConnection! @cost(weight: "10") + operatedDatabases( + order: [InstitutionOperatedDatabaseSortInput!] @cost(weight: "10") + where: InstitutionOperatedDatabaseFilterInput @cost(weight: "10") + ): InstitutionOperatedDatabaseConnection! @cost(weight: "10") operatingState: InstitutionOperatingState! - pendingDevelopedMethods(where: InstitutionDevelopedMethodFilterInput @cost(weight: "10")): PendingInstitutionDevelopedMethodConnection! @cost(weight: "10") - pendingManufacturedComponents(where: InstitutionManufacturedComponentFilterInput @cost(weight: "10")): PendingInstitutionManufacturedComponentConnection! @cost(weight: "10") - pendingRepresentatives(where: InstitutionRepresentativeFilterInput @cost(weight: "10")): PendingInstitutionRepresentativeConnection @authorize(policy: "ManageInstitutionRepresentativeScope") @cost(weight: "10") - representatives(where: InstitutionRepresentativeFilterInput @cost(weight: "10")): InstitutionRepresentativeConnection! @cost(weight: "10") + pendingDevelopedMethods( + "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: [InstitutionDevelopedMethodSortInput!] @cost(weight: "10") + where: InstitutionDevelopedMethodFilterInput @cost(weight: "10") + ): PendingInstitutionDevelopedMethodConnection! @cost(weight: "10") + pendingManufacturedComponents( + "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: [InstitutionManufacturedComponentSortInput!] @cost(weight: "10") + where: InstitutionManufacturedComponentFilterInput @cost(weight: "10") + ): PendingInstitutionManufacturedComponentConnection! @cost(weight: "10") + pendingRepresentatives( + order: [InstitutionRepresentativeSortInput!] @cost(weight: "10") + where: InstitutionRepresentativeFilterInput @cost(weight: "10") + ): PendingInstitutionRepresentativeConnection @cost(weight: "10") + representatives( + order: [InstitutionRepresentativeSortInput!] @cost(weight: "10") + where: InstitutionRepresentativeFilterInput @cost(weight: "10") + ): InstitutionRepresentativeConnection! @cost(weight: "10") state: InstitutionState! - uuid: Uuid! @cost(weight: "10") + updatedAt: DateTime! + uuid: Uuid! } type InstitutionDevelopedMethodConnection { - edges: [InstitutionDevelopedMethodEdge!]! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + edges: [InstitutionDevelopedMethodEdge!]! @cost(weight: "0") + pageInfo: PageInfo! @cost(weight: "0") + totalCount: Int! } type InstitutionDevelopedMethodEdge { - node: Method! @cost(weight: "10") + cursor: String! + isAuthorizedToConfirmEdge: Boolean! @cost(weight: "1") + isAuthorizedToRemoveEdge: Boolean! @cost(weight: "1") + node: Method! @cost(weight: "0") } type InstitutionGnuPgKeyFingerprintConnection { - edges: [InstitutionGnuPgKeyFingerprintEdge!]! @cost(weight: "10") - isAuthorizedToAddEdge: Boolean! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + edges: [InstitutionGnuPgKeyFingerprintEdge!]! @cost(weight: "0") + isAuthorizedToAddEdge: Boolean! @cost(weight: "1") + totalCount: Int! } type InstitutionGnuPgKeyFingerprintEdge { @@ -982,68 +1836,78 @@ type InstitutionGnuPgKeyFingerprintEdge { } type InstitutionManagedComponentConnection { - edges: [InstitutionManagedComponentEdge!]! @cost(weight: "10") - isAuthorizedToAddEdge: Boolean! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + edges: [InstitutionManagedComponentEdge!]! @cost(weight: "0") + isAuthorizedToAddEdge: Boolean! @cost(weight: "1") + pageInfo: PageInfo! @cost(weight: "0") + totalCount: Int! } type InstitutionManagedComponentEdge { + cursor: String! node: Component! } type InstitutionManagedDataFormatConnection { - edges: [InstitutionManagedDataFormatEdge!]! @cost(weight: "10") - isAuthorizedToAddEdge: Boolean! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + edges: [InstitutionManagedDataFormatEdge!]! @cost(weight: "0") + isAuthorizedToAddEdge: Boolean! @cost(weight: "1") + pageInfo: PageInfo! @cost(weight: "0") + totalCount: Int! } type InstitutionManagedDataFormatEdge { + cursor: String! node: DataFormat! } type InstitutionManagedInstitutionConnection { - edges: [InstitutionManagedInstitutionEdge!]! @cost(weight: "10") - isAuthorizedToAddEdge: Boolean! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + edges: [InstitutionManagedInstitutionEdge!]! @cost(weight: "0") + isAuthorizedToAddEdge: Boolean! @cost(weight: "1") + pageInfo: PageInfo! @cost(weight: "0") + totalCount: Int! } type InstitutionManagedInstitutionEdge { + cursor: String! node: Institution! } type InstitutionManagedMethodConnection { - edges: [InstitutionManagedMethodEdge!]! @cost(weight: "10") - isAuthorizedToAddEdge: Boolean! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + edges: [InstitutionManagedMethodEdge!]! @cost(weight: "0") + isAuthorizedToAddEdge: Boolean! @cost(weight: "1") + pageInfo: PageInfo! @cost(weight: "0") + totalCount: Int! } type InstitutionManagedMethodEdge { + cursor: String! node: Method! } type InstitutionManagerEdge { - node: Institution! @cost(weight: "10") + node: Institution! @cost(weight: "0") } type InstitutionManufacturedComponentConnection { - edges: [InstitutionManufacturedComponentEdge!]! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + edges: [InstitutionManufacturedComponentEdge!]! @cost(weight: "0") + pageInfo: PageInfo! @cost(weight: "0") + totalCount: Int! } type InstitutionManufacturedComponentEdge { - node: Component! @cost(weight: "10") + cursor: String! + node: Component! @cost(weight: "0") } type InstitutionMethodDeveloperEdge { - isAuthorizedToConfirmEdge: Boolean! @cost(weight: "10") - isAuthorizedToRemoveEdge: Boolean! @cost(weight: "10") - node: Institution! @cost(weight: "10") + isAuthorizedToConfirmEdge: Boolean! @cost(weight: "1") + isAuthorizedToRemoveEdge: Boolean! @cost(weight: "1") + node: Institution! @cost(weight: "0") } type InstitutionOperatedDatabaseConnection { - edges: [InstitutionOperatedDatabaseEdge!]! @cost(weight: "10") - isAuthorizedToAddEdge: Boolean! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + edges: [InstitutionOperatedDatabaseEdge!]! @cost(weight: "0") + isAuthorizedToAddEdge: Boolean! @cost(weight: "1") + totalCount: Int! } type InstitutionOperatedDatabaseEdge { @@ -1051,26 +1915,26 @@ type InstitutionOperatedDatabaseEdge { } type InstitutionOwnedOpenIdConnectApplicationConnection { - edges: [InstitutionOwnedOpenIdConnectApplicationEdge!]! @cost(weight: "10") - isAuthorizedToAddEdge: Boolean! @cost(weight: "10") - isAuthorizedToRemoveEdge: Boolean! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + edges: [InstitutionOwnedOpenIdConnectApplicationEdge!]! @cost(weight: "0") + isAuthorizedToAddEdge: Boolean! @cost(weight: "1") + isAuthorizedToRemoveEdge: Boolean! @cost(weight: "1") + totalCount: Int! } type InstitutionOwnedOpenIdConnectApplicationEdge { - isAuthorizedToRemoveEdge: Boolean! @cost(weight: "10") + isAuthorizedToRemoveEdge: Boolean! @cost(weight: "1") node: OpenIdConnectApplication! } type InstitutionRepresentativeConnection { - edges: [InstitutionRepresentativeEdge!]! @cost(weight: "10") - isAuthorizedToAddEdge: Boolean! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + edges: [InstitutionRepresentativeEdge!]! @cost(weight: "0") + isAuthorizedToAddEdge: Boolean! @cost(weight: "1") + totalCount: Int! } type InstitutionRepresentativeEdge { - isAuthorizedToRemoveEdge: Boolean! @cost(weight: "10") - node: User! @cost(weight: "10") + isAuthorizedToRemoveEdge: Boolean! @cost(weight: "1") + node: User! @cost(weight: "0") role: InstitutionRepresentativeRole! } @@ -1092,7 +1956,7 @@ type InstitutionsEdge { node: Institution! } -type LifeCycleData implements Data { +type LifeCycleData implements Node & Data { appliedMethod: AppliedMethod! approvals: [DataApproval!]! component: Component @cost(weight: "10") @@ -1115,14 +1979,14 @@ type LifeCycleData implements Data { } type LifeCycleDataConnection { - edges: [LifeCycleDataEdge!]! - pageInfo: PageInfo! - totalCount: NonNegativeInt! + edges: [LifeCycleDataEdge!] @cost(weight: "0") + pageInfo: PageInfo! @cost(weight: "0") + totalCount: Int! } type LifeCycleDataEdge { cursor: String! - node: LifeCycleData! + node: LifeCycleData! @cost(weight: "0") } type LoginUserError implements UserError { @@ -1178,35 +2042,43 @@ type Method implements Node { availability: OpenEndedDateTimeRange calculationLocator: Url categories: [MethodCategory!]! + createdAt: DateTime! description: String! - developers(where: IMethodDeveloperFilterInput @cost(weight: "10")): MethodDeveloperConnection! @cost(weight: "10") + developers( + order: [MethodDeveloperSortInput!] @cost(weight: "10") + where: MethodDeveloperFilterInput @cost(weight: "10") + ): MethodDeveloperConnection! @cost(weight: "10") id: ID! - isAuthorizedToUpdateNode: Boolean! @cost(weight: "10") + isAuthorizedToUpdateNode: Boolean! @cost(weight: "1") manager: MethodManagerEdge! @cost(weight: "10") name: String! parameters: [MethodParameter!]! - pendingDevelopers(where: IMethodDeveloperFilterInput @cost(weight: "10")): PendingMethodDeveloperConnection @authorize(policy: "WriteScope") @cost(weight: "10") - reference: Reference @cost(weight: "10") + pendingDevelopers( + order: [MethodDeveloperSortInput!] @cost(weight: "10") + where: MethodDeveloperFilterInput @cost(weight: "10") + ): PendingMethodDeveloperConnection @cost(weight: "10") + reference: Reference @cost(weight: "0") sources: [MethodSource!]! - uuid: Uuid! @cost(weight: "10") + updatedAt: DateTime! + uuid: Uuid! validity: OpenEndedDateTimeRange } type MethodDeveloperConnection { edges: [MethodDeveloperEdge!]! @cost(weight: "10") - isAuthorizedToAddInstitutionEdge: Boolean! @cost(weight: "10") - isAuthorizedToAddUserEdge: Boolean! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + isAuthorizedToAddInstitutionEdge: Boolean! @cost(weight: "1") + isAuthorizedToAddUserEdge: Boolean! @cost(weight: "1") + totalCount: Int! @cost(weight: "10") } type MethodDeveloperEdge { - isAuthorizedToConfirmEdge: Boolean! @cost(weight: "10") - isAuthorizedToRemoveEdge: Boolean! @cost(weight: "10") - node: Stakeholder! @cost(weight: "10") + isAuthorizedToConfirmEdge: Boolean! @cost(weight: "1") + isAuthorizedToRemoveEdge: Boolean! @cost(weight: "1") + node: Stakeholder! @cost(weight: "0") } type MethodManagerEdge { - node: Institution! @cost(weight: "10") + node: Institution! @cost(weight: "0") } "A parameter given as a primitive or complex JSON value when this method is applied." @@ -1217,14 +2089,6 @@ type MethodParameter { type: Any! } -"A data source given as a cross-database data reference when this method is applied." -type MethodSource { - "The significance of the source for the method." - description: String! - "The source name." - name: String! -} - "A connection to a list of items." type MethodsConnection { "A list of edges." @@ -1243,77 +2107,12 @@ type MethodsEdge { node: Method! } -type Mutation { - addComponentAssembly(input: AddComponentAssemblyInput!): AddComponentAssemblyPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - addComponentGeneralization(input: AddComponentGeneralizationInput!): AddComponentGeneralizationPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - addComponentManufacturer(input: AddComponentManufacturerInput!): AddComponentManufacturerPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - addComponentVariant(input: AddComponentVariantInput!): AddComponentVariantPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - addGnuPgKeyFingerprint(input: AddGnuPgKeyFingerprintInput!): AddGnuPgKeyFingerprintPayload! @authorize(policy: "ManageGnuPgScope") @cost(weight: "10") - addInstitutionMethodDeveloper(input: AddInstitutionMethodDeveloperInput!): AddInstitutionMethodDeveloperPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - addInstitutionRepresentative(input: AddInstitutionRepresentativeInput!): AddInstitutionRepresentativePayload! @authorize(policy: "ManageInstitutionRepresentativeScope") @cost(weight: "10") - addUserMethodDeveloper(input: AddUserMethodDeveloperInput!): AddUserMethodDeveloperPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - addUserRole(input: AddUserRoleInput!): AddUserRolePayload! @authorize(policy: "ManageUserScope") @cost(weight: "10") - allowGnuPgKeyFingerprint(input: AllowGnuPgKeyFingerprintInput!): AllowGnuPgKeyFingerprintPayload! @authorize(policy: "ManageGnuPgScope") @cost(weight: "10") - changeInstitutionRepresentativeRole(input: ChangeInstitutionRepresentativeRoleInput!): ChangeInstitutionRepresentativeRolePayload! @authorize(policy: "ManageInstitutionRepresentativeScope") @cost(weight: "10") - changeUserEmail(input: ChangeUserEmailInput!): ChangeUserEmailPayload! @authorize(policy: "ManageUserScope") @cost(weight: "10") - changeUserPassword(input: ChangeUserPasswordInput!): ChangeUserPasswordPayload! @authorize(policy: "ManageUserScope") @cost(weight: "10") - confirmComponentManufacturer(input: ConfirmComponentManufacturerInput!): ConfirmComponentManufacturerPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - confirmInstitutionMethodDeveloper(input: ConfirmInstitutionMethodDeveloperInput!): ConfirmInstitutionMethodDeveloperPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - confirmInstitutionRepresentative(input: ConfirmInstitutionRepresentativeInput!): ConfirmInstitutionRepresentativePayload! @authorize(policy: "ManageInstitutionRepresentativeScope") @cost(weight: "10") - confirmUserEmail(input: ConfirmUserEmailInput!): ConfirmUserEmailPayload! @cost(weight: "10") - confirmUserEmailChange(input: ConfirmUserEmailChangeInput!): ConfirmUserEmailChangePayload! @cost(weight: "10") - confirmUserMethodDeveloper(input: ConfirmUserMethodDeveloperInput!): ConfirmUserMethodDeveloperPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - createComponent(input: CreateComponentInput!): CreateComponentPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - createDatabase(input: CreateDatabaseInput!): CreateDatabasePayload! @authorize(policy: "ManageDatabaseScope") @cost(weight: "10") - createDataFormat(input: CreateDataFormatInput!): CreateDataFormatPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - createInstitution(input: CreateInstitutionInput!): CreateInstitutionPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - createMethod(input: CreateMethodInput!): CreateMethodPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - createOpenIdConnectApplication(input: CreateOpenIdConnectApplicationInput!): CreateOpenIdConnectApplicationPayload! @authorize(policy: "ManageOpenIdConnectScope") @cost(weight: "10") - deleteInstitution(input: DeleteInstitutionInput!): DeleteInstitutionPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - deleteOpenIdConnectApplication(input: DeleteOpenIdConnectApplicationInput!): DeleteOpenIdConnectApplicationPayload! @authorize(policy: "ManageOpenIdConnectScope") @cost(weight: "10") - deleteOpenIdConnectAuthorization(input: DeleteOpenIdConnectAuthorizationInput!): DeleteOpenIdConnectAuthorizationPayload! @authorize(policy: "ManageOpenIdConnectScope") @cost(weight: "10") - deletePersonalUserData(input: DeletePersonalUserDataInput!): DeletePersonalUserDataPayload! @authorize(policy: "ManageUserScope") @cost(weight: "10") - deleteUser(input: DeleteUserInput!): DeleteUserPayload! @authorize(policy: "ManageUserScope") @authorize(policy: "AdministrateScope") @cost(weight: "10") - disableUserTwoFactorAuthentication: DisableUserTwoFactorAuthenticationPayload! @authorize(policy: "ManageUserScope") @cost(weight: "10") - enableUserTwoFactorAuthenticator(input: EnableUserTwoFactorAuthenticatorInput!): EnableUserTwoFactorAuthenticatorPayload! @authorize(policy: "ManageUserScope") @cost(weight: "10") - forbidGnuPgKeyFingerprint(input: ForbidGnuPgKeyFingerprintInput!): ForbidGnuPgKeyFingerprintPayload! @authorize(policy: "ManageGnuPgScope") @cost(weight: "10") - forgetUserTwoFactorAuthenticationClient: ForgetUserTwoFactorAuthenticationClientPayload! @authorize(policy: "ManageUserScope") @cost(weight: "10") - generateUserTwoFactorAuthenticatorSharedKeyAndQrCodeUri: GenerateUserTwoFactorAuthenticatorSharedKeyAndQrCodeUriPayload! @authorize(policy: "ManageUserScope") @cost(weight: "10") - generateUserTwoFactorRecoveryCodes: GenerateUserTwoFactorRecoveryCodesPayload! @authorize(policy: "ManageUserScope") @cost(weight: "10") - loginUser(input: LoginUserInput!): LoginUserPayload! @cost(weight: "10") - loginUserWithRecoveryCode(input: LoginUserWithRecoveryCodeInput!): LoginUserWithRecoveryCodePayload! @cost(weight: "10") - loginUserWithTwoFactorCode(input: LoginUserWithTwoFactorCodeInput!): LoginUserWithTwoFactorCodePayload! @cost(weight: "10") - logoutUser: LogoutUserPayload! @authorize(policy: "ManageUserScope") @cost(weight: "10") - registerUser(input: RegisterUserInput!): RegisterUserPayload! @cost(weight: "10") - removeComponentAssembly(input: RemoveComponentAssemblyInput!): RemoveComponentAssemblyPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - removeComponentGeneralization(input: RemoveComponentGeneralizationInput!): RemoveComponentGeneralizationPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - removeComponentManufacturer(input: RemoveComponentManufacturerInput!): RemoveComponentManufacturerPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - removeComponentVariant(input: RemoveComponentVariantInput!): RemoveComponentVariantPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - removeInstitutionMethodDeveloper(input: RemoveInstitutionMethodDeveloperInput!): RemoveInstitutionMethodDeveloperPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - removeInstitutionRepresentative(input: RemoveInstitutionRepresentativeInput!): RemoveInstitutionRepresentativePayload! @authorize(policy: "ManageInstitutionRepresentativeScope") @cost(weight: "10") - removeUserMethodDeveloper(input: RemoveUserMethodDeveloperInput!): RemoveUserMethodDeveloperPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - removeUserRole(input: RemoveUserRoleInput!): RemoveUserRolePayload! @authorize(policy: "ManageUserScope") @cost(weight: "10") - requestUserPasswordReset(input: RequestUserPasswordResetInput!): RequestUserPasswordResetPayload! @cost(weight: "10") - resendUserEmailConfirmation(input: ResendUserEmailConfirmationInput!): ResendUserEmailConfirmationPayload! @cost(weight: "10") - resendUserEmailVerification: ResendUserEmailVerificationPayload! @authorize(policy: "ManageUserScope") @cost(weight: "10") - resetOpenIdConnectApplicationClientSecret(input: ResetOpenIdConnectApplicationClientSecretInput!): ResetOpenIdConnectApplicationClientSecretPayload! @authorize(policy: "ManageOpenIdConnectScope") @cost(weight: "10") - resetUserPassword(input: ResetUserPasswordInput!): ResetUserPasswordPayload! @cost(weight: "10") - resetUserTwoFactorAuthenticator: ResetUserTwoFactorAuthenticatorPayload! @authorize(policy: "ManageUserScope") @cost(weight: "10") - revokeOpenIdConnectToken(input: RevokeOpenIdConnectTokenInput!): RevokeOpenIdConnectTokenPayload! @authorize(policy: "ManageOpenIdConnectScope") @cost(weight: "10") - setComponentExtras(input: SetComponentExtrasInput!): SetComponentExtrasPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - setInstitutionExtras(input: SetInstitutionExtrasInput!): SetInstitutionExtrasPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - setUserPassword(input: SetUserPasswordInput!): SetUserPasswordPayload! @authorize(policy: "ManageUserScope") @cost(weight: "10") - setUserPhoneNumber(input: SetUserPhoneNumberInput!): SetUserPhoneNumberPayload! @authorize(policy: "ManageUserScope") @cost(weight: "10") - switchInstitutionOperatingState(input: SwitchInstitutionOperatingStateInput!): SwitchInstitutionOperatingStatePayload! @authorize(policy: "WriteScope") @cost(weight: "10") - updateComponent(input: UpdateComponentInput!): UpdateComponentPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - updateComponentAssembly(input: UpdateComponentAssemblyInput!): UpdateComponentAssemblyPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - updateDatabase(input: UpdateDatabaseInput!): UpdateDatabasePayload! @authorize(policy: "ManageDatabaseScope") @cost(weight: "10") - updateDataFormat(input: UpdateDataFormatInput!): UpdateDataFormatPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - updateInstitution(input: UpdateInstitutionInput!): UpdateInstitutionPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - updateMethod(input: UpdateMethodInput!): UpdateMethodPayload! @authorize(policy: "WriteScope") @cost(weight: "10") - updateOpenIdConnectApplication(input: UpdateOpenIdConnectApplicationInput!): UpdateOpenIdConnectApplicationPayload! @authorize(policy: "ManageOpenIdConnectScope") @cost(weight: "10") - verifyDatabase(input: VerifyDatabaseInput!): VerifyDatabasePayload! @authorize(policy: "ManageDatabaseScope") @cost(weight: "10") - verifyInstitution(input: VerifyInstitutionInput!): VerifyInstitutionPayload! @authorize(policy: "WriteScope") @authorize(policy: "VerifyScope") @cost(weight: "10") +"A data source given as a cross-database data reference when this method is applied." +type MethodSource { + "The significance of the source for the method." + description: String! + "The source name." + name: String! } type NamedMethodArgument { @@ -1333,90 +2132,134 @@ type Numeration { } type OpenEndedDateTimeRange { - from: DateTime @cost(weight: "10") - to: DateTime @cost(weight: "10") + from: DateTime + to: DateTime } type OpenIdConnectApplication implements Node { applicationType: String - authorizations: OpenIdConnectApplicationAuthorizationConnection! @cost(weight: "10") - clientId: String! @cost(weight: "10") + audiences: [String!]! @cost(weight: "0") + authorizations: OpenIdConnectApplicationGrantedAuthorizationConnection! + @cost(weight: "0") + clientId: String! clientType: String - consentType: OpenIdConnectConsentType! @cost(weight: "10") + consentType: OpenIdConnectConsentType! + createdAt: DateTime! displayName: String - endpoints: [OpenIdConnectEndpoint!]! @cost(weight: "10") - grantTypes: [OpenIdConnectGrantType!]! @cost(weight: "10") + endpoints: [OpenIdConnectEndpoint!]! @cost(weight: "0") + grantTypes: [OpenIdConnectGrantType!]! @cost(weight: "0") id: ID! - isAuthorizedToManageNode: Boolean! @cost(weight: "10") - owner: OpenIdConnectApplicationOwnerEdge! @cost(weight: "10") - postLogoutRedirectUri: Url @cost(weight: "10") - redirectUri: Url @cost(weight: "10") - requirements: [OpenIdConnectRequirement!]! @cost(weight: "10") - responseTypes: [OpenIdConnectResponseType!]! @cost(weight: "10") - scopes: [OpenIdConnectScope!]! @cost(weight: "10") - tokens: OpenIdConnectApplicationTokenConnection! @cost(weight: "10") - uuid: Uuid! @cost(weight: "10") + isAuthorizedToManageNode: Boolean! @cost(weight: "1") + owner: OpenIdConnectApplicationOwnerEdge! @cost(weight: "0") + postLogoutRedirectUri: Url + redirectUri: Url + requirements: [OpenIdConnectRequirement!]! @cost(weight: "0") + resources: [String!]! @cost(weight: "0") + responseTypes: [OpenIdConnectResponseType!]! @cost(weight: "0") + scopes: [OpenIdConnectScope!]! @cost(weight: "0") + tokens: OpenIdConnectApplicationIssuedTokenConnection! @cost(weight: "10") + updatedAt: DateTime! + uuid: Uuid! } -type OpenIdConnectApplicationAuthorizationConnection { - edges: [OpenIdConnectApplicationAuthorizationEdge!]! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") +type OpenIdConnectApplicationGrantedAuthorizationConnection { + edges: [OpenIdConnectApplicationGrantedAuthorizationEdge!]! @cost(weight: "0") + totalCount: Int! } -type OpenIdConnectApplicationAuthorizationEdge { +type OpenIdConnectApplicationGrantedAuthorizationEdge { node: OpenIdConnectAuthorization! } +type OpenIdConnectApplicationIssuedTokenConnection { + edges: [OpenIdConnectApplicationIssuedTokenEdge!]! @cost(weight: "0") + totalCount: Int! +} + +type OpenIdConnectApplicationIssuedTokenEdge { + node: OpenIdConnectToken! +} + type OpenIdConnectApplicationOwnerEdge { - node: Institution! @cost(weight: "10") + node: Institution! @cost(weight: "0") } -type OpenIdConnectApplicationTokenConnection { - edges: [OpenIdConnectApplicationTokenEdge!]! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") +"A connection to a list of items." +type OpenIdConnectApplicationsConnection { + "A list of edges." + edges: [OpenIdConnectApplicationsEdge!] + "Information to aid in pagination." + pageInfo: PageInfo! + "Identifies the total count of items in the connection." + totalCount: Int! @cost(weight: "10") } -type OpenIdConnectApplicationTokenEdge { - node: OpenIdConnectToken! +"An edge in a connection." +type OpenIdConnectApplicationsEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: OpenIdConnectApplication! } type OpenIdConnectAuthorization implements Node { - application: OpenIdConnectAuthorizationApplicationEdge! @cost(weight: "10") - creationDate: DateTime + application: OpenIdConnectAuthorizationApplicationEdge @cost(weight: "10") + createdAt: DateTime! id: ID! - isAuthorizedToDeleteNode: Boolean! @cost(weight: "10") + isAuthorizedToDeleteNode: Boolean! @cost(weight: "1") + scopes: [OpenIdConnectScope!]! @cost(weight: "0") status: String - subject: String - tokens: OpenIdConnectAuthorizationTokenConnection! @cost(weight: "10") + subject: User + tokens: OpenIdConnectAuthorizationIssuedTokenConnection! @cost(weight: "10") type: String - uuid: Uuid! @cost(weight: "10") + updatedAt: DateTime! + uuid: Uuid! } type OpenIdConnectAuthorizationApplicationEdge { node: OpenIdConnectApplication! } -type OpenIdConnectAuthorizationTokenConnection { - edges: [OpenIdConnectAuthorizationTokenEdge!]! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") +type OpenIdConnectAuthorizationIssuedTokenConnection { + edges: [OpenIdConnectAuthorizationIssuedTokenEdge!]! @cost(weight: "0") + totalCount: Int! } -type OpenIdConnectAuthorizationTokenEdge { +type OpenIdConnectAuthorizationIssuedTokenEdge { node: OpenIdConnectToken! } +"A connection to a list of items." +type OpenIdConnectAuthorizationsConnection { + "A list of edges." + edges: [OpenIdConnectAuthorizationsEdge!] + "Information to aid in pagination." + pageInfo: PageInfo! + "Identifies the total count of items in the connection." + totalCount: Int! @cost(weight: "10") +} + +"An edge in a connection." +type OpenIdConnectAuthorizationsEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: OpenIdConnectAuthorization! +} + type OpenIdConnectToken implements Node { - application: OpenIdConnectTokenApplicationEdge! @cost(weight: "10") - authorization: OpenIdConnectTokenAuthorizationEdge! @cost(weight: "10") - creationDate: DateTime - expirationDate: DateTime + application: OpenIdConnectTokenApplicationEdge @cost(weight: "10") + authorization: OpenIdConnectTokenAuthorizationEdge @cost(weight: "10") + createdAt: DateTime! + expiredAt: LocalDateTime id: ID! - isAuthorizedToRevokeNode: Boolean! @cost(weight: "10") - redemptionDate: DateTime + isAuthorizedToRevokeNode: Boolean! @cost(weight: "1") + redeemedAt: LocalDateTime status: String - subject: String + subject: OpenIdConnectSubject type: String - uuid: Uuid! @cost(weight: "10") + updatedAt: DateTime! + uuid: Uuid! } type OpenIdConnectTokenApplicationEdge { @@ -1427,11 +2270,29 @@ type OpenIdConnectTokenAuthorizationEdge { node: OpenIdConnectAuthorization! } -type OpticalData implements Data { +"A connection to a list of items." +type OpenIdConnectTokensConnection { + "A list of edges." + edges: [OpenIdConnectTokensEdge!] + "Information to aid in pagination." + pageInfo: PageInfo! + "Identifies the total count of items in the connection." + totalCount: Int! @cost(weight: "10") +} + +"An edge in a connection." +type OpenIdConnectTokensEdge { + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: OpenIdConnectToken! +} + +type OpticalData implements Node & Data { appliedMethod: AppliedMethod! approvals: [DataApproval!]! cielabColors: [CielabColor!]! - coatedSide: CoatedSide + coatedSide: CoatedSide! colorRenderingIndices: [Float!]! component: Component @cost(weight: "10") componentId: Uuid! @@ -1460,14 +2321,14 @@ type OpticalData implements Data { } type OpticalDataConnection { - edges: [OpticalDataEdge!]! - pageInfo: PageInfo! - totalCount: NonNegativeInt! + edges: [OpticalDataEdge!] @cost(weight: "0") + pageInfo: PageInfo! @cost(weight: "0") + totalCount: Int! } type OpticalDataEdge { cursor: String! - node: OpticalData! + node: OpticalData! @cost(weight: "0") } "Information about pagination in a connection." @@ -1483,9 +2344,9 @@ type PageInfo { } type PendingComponentManufacturerConnection { - edges: [ComponentManufacturerEdge!]! @cost(weight: "10") - isAuthorizedToAddEdge: Boolean! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + edges: [ComponentManufacturerEdge!]! @cost(weight: "0") + isAuthorizedToAddEdge: Boolean! @cost(weight: "1") + totalCount: Int! } "A connection to a list of items." @@ -1507,21 +2368,23 @@ type PendingDatabasesEdge { } type PendingInstitutionDevelopedMethodConnection { - edges: [InstitutionDevelopedMethodEdge!]! @cost(weight: "10") - isAuthorizedToConfirmEdges: Boolean! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + edges: [InstitutionDevelopedMethodEdge!]! @cost(weight: "0") + isAuthorizedToConfirmEdges: Boolean! @cost(weight: "1") + pageInfo: PageInfo! @cost(weight: "0") + totalCount: Int! } type PendingInstitutionManufacturedComponentConnection { - edges: [InstitutionManufacturedComponentEdge!]! @cost(weight: "10") - isAuthorizedToConfirmEdges: Boolean! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + edges: [InstitutionManufacturedComponentEdge!]! @cost(weight: "0") + isAuthorizedToConfirmEdges: Boolean! @cost(weight: "1") + pageInfo: PageInfo! @cost(weight: "0") + totalCount: Int! } type PendingInstitutionRepresentativeConnection { - edges: [InstitutionRepresentativeEdge!]! @cost(weight: "10") - isAuthorizedToAddEdge: Boolean! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + edges: [InstitutionRepresentativeEdge!]! @cost(weight: "0") + isAuthorizedToAddEdge: Boolean! @cost(weight: "1") + totalCount: Int! } "A connection to a list of items." @@ -1544,24 +2407,25 @@ type PendingInstitutionsEdge { type PendingMethodDeveloperConnection { edges: [MethodDeveloperEdge!]! @cost(weight: "10") - isAuthorizedToAddInstitutionEdge: Boolean! @cost(weight: "10") - isAuthorizedToAddUserEdge: Boolean! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + isAuthorizedToAddInstitutionEdge: Boolean! @cost(weight: "1") + isAuthorizedToAddUserEdge: Boolean! @cost(weight: "1") + totalCount: Int! @cost(weight: "10") } type PendingUserDevelopedMethodConnection { - edges: [UserDevelopedMethodEdge!]! @cost(weight: "10") - isAuthorizedToConfirmEdges: Boolean! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + edges: [UserDevelopedMethodEdge!]! @cost(weight: "0") + isAuthorizedToConfirmEdges: Boolean! @cost(weight: "1") + pageInfo: PageInfo! @cost(weight: "0") + totalCount: Int! } type PendingUserRepresentedInstitutionConnection { - edges: [UserRepresentedInstitutionEdge!]! @cost(weight: "10") - isAuthorizedToConfirmEdges: Boolean! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + edges: [UserRepresentedInstitutionEdge!]! @cost(weight: "0") + isAuthorizedToConfirmEdges: Boolean! @cost(weight: "1") + totalCount: Int! } -type PhotovoltaicData implements Data { +type PhotovoltaicData implements Node & Data { appliedMethod: AppliedMethod! approvals: [DataApproval!]! component: Component @cost(weight: "10") @@ -1584,14 +2448,14 @@ type PhotovoltaicData implements Data { } type PhotovoltaicDataConnection { - edges: [PhotovoltaicDataEdge!]! - pageInfo: PageInfo! - totalCount: NonNegativeInt! + edges: [PhotovoltaicDataEdge!] @cost(weight: "0") + pageInfo: PageInfo! @cost(weight: "0") + totalCount: Int! } type PhotovoltaicDataEdge { cursor: String! - node: PhotovoltaicData! + node: PhotovoltaicData! @cost(weight: "0") } type PrimeSurfaceOrDirection { @@ -1602,51 +2466,19 @@ type PrimeSurfaceOrDirection { type Publication { abstract: String "The website arXiv.org is a free and open-access archive for publications. The arXiv identifier can be used to define a publication." - arXiv: String + arXiv: ArXiv authors: [String!] "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: String + doi: Doi "Referenced section" section: String title: String - "A Uniform Resource Name (URN) can be used to define a publication. TODO: Improve the regex pattern to further restrict the string." - urn: String - "If a persistent identifier 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." + "A Uniform Resource Name (URN) can be used to define a publication." + urn: Url + "If a persistent identifier like DOI is defined above, this web address 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: Url } -type Query { - component(id: Uuid!): Component @cost(weight: "10") - components("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: [ComponentSortInput!] @cost(weight: "10") where: ComponentFilterInput @cost(weight: "10")): ComponentsConnection @listSize(assumedSize: 2147483646, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - currentInstitution: Institution @authorize(policy: "Authenticated") @cost(weight: "10") - currentOpenIdConnectApplication: OpenIdConnectApplication @authorize(policy: "ManageOpenIdConnectScope") @cost(weight: "10") - currentUser: User @authorize(policy: "Authenticated") @cost(weight: "10") - database(id: Uuid!): Database @cost(weight: "10") - databases("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: [DatabaseSortInput!] @cost(weight: "10") where: DatabaseFilterInput @cost(weight: "10")): DatabasesConnection @listSize(assumedSize: 2147483646, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - dataFormat(id: Uuid!): DataFormat @cost(weight: "10") - dataFormats("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: [DataFormatSortInput!] @cost(weight: "10") where: DataFormatFilterInput @cost(weight: "10")): DataFormatsConnection @listSize(assumedSize: 2147483646, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - gnuPgKeyFingerprint(fingerprint: String!): GnuPgKeyFingerprint @authorize(policy: "ManageGnuPgScope") @cost(weight: "10") - gnuPgKeyFingerprints("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: [GnuPgKeyFingerprintSortInput!] @cost(weight: "10") where: GnuPgKeyFingerprintFilterInput @cost(weight: "10")): GnuPgKeyFingerprintsConnection @authorize(policy: "ManageGnuPgScope") @listSize(assumedSize: 2147483646, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - institution(id: Uuid!): Institution @cost(weight: "10") - institutions("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: [InstitutionSortInput!] @cost(weight: "10") where: InstitutionFilterInput @cost(weight: "10")): InstitutionsConnection @listSize(assumedSize: 2147483646, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - method(id: Uuid!): Method @cost(weight: "10") - methods("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: [MethodSortInput!] @cost(weight: "10") where: MethodFilterInput @cost(weight: "10")): MethodsConnection @listSize(assumedSize: 2147483646, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @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") - openIdConnectApplication(id: Uuid!): OpenIdConnectApplication @authorize(policy: "ManageOpenIdConnectScope") @cost(weight: "10") - openIdConnectApplications: [OpenIdConnectApplication!]! @authorize(policy: "ManageOpenIdConnectScope") @cost(weight: "10") - openIdConnectAuthorization(id: Uuid!): OpenIdConnectAuthorization @authorize(policy: "ManageOpenIdConnectScope") @cost(weight: "10") - openIdConnectAuthorizations: [OpenIdConnectAuthorization!]! @authorize(policy: "ManageOpenIdConnectScope") @cost(weight: "10") - openIdConnectToken(id: Uuid!): OpenIdConnectToken @authorize(policy: "ManageOpenIdConnectScope") @cost(weight: "10") - openIdConnectTokens: [OpenIdConnectToken!]! @authorize(policy: "ManageOpenIdConnectScope") @cost(weight: "10") - pendingDatabases("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: [DatabaseSortInput!] @cost(weight: "10") where: DatabaseFilterInput @cost(weight: "10")): PendingDatabasesConnection @authorize(policy: "ManageDatabaseScope") @listSize(assumedSize: 2147483646, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - pendingInstitutions("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: [InstitutionSortInput!] @cost(weight: "10") where: InstitutionFilterInput @cost(weight: "10")): PendingInstitutionsConnection @authorize(policy: "WriteScope") @authorize(policy: "VerifyScope") @listSize(assumedSize: 2147483646, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") - user(id: Uuid!): User @cost(weight: "10") - users("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: [UserSortInput!] @cost(weight: "10") where: UserFilterInput @cost(weight: "10")): UsersConnection @listSize(assumedSize: 2147483646, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @cost(weight: "10") -} - type RegisterUserError implements UserError { code: RegisterUserErrorCode! message: String! @@ -1718,9 +2550,9 @@ type RemoveInstitutionMethodDeveloperError implements UserError { } type RemoveInstitutionMethodDeveloperPayload { - developedMethodEdge: InstitutionDevelopedMethodEdge errors: [RemoveInstitutionMethodDeveloperError!] - methodDeveloperEdge: InstitutionMethodDeveloperEdge + institution: Institution @cost(weight: "10") + method: Method @cost(weight: "10") query: Query! } @@ -1744,10 +2576,10 @@ type RemoveUserMethodDeveloperError implements UserError { } type RemoveUserMethodDeveloperPayload { - developedMethodEdge: UserDevelopedMethodEdge errors: [RemoveUserMethodDeveloperError!] - methodDeveloperEdge: UserMethodDeveloperEdge + method: Method @cost(weight: "10") query: Query! + user: User @cost(weight: "10") } type RemoveUserRoleError implements UserError { @@ -1956,27 +2788,27 @@ type UpdateComponentPayload { query: Query! } -type UpdateDataFormatError implements UserError { - code: UpdateDataFormatErrorCode! +type UpdateDatabaseError implements UserError { + code: UpdateDatabaseErrorCode! message: String! path: [String!]! } -type UpdateDataFormatPayload { - dataFormat: DataFormat - errors: [UpdateDataFormatError!] +type UpdateDatabasePayload { + database: Database + errors: [UpdateDatabaseError!] query: Query! } -type UpdateDatabaseError implements UserError { - code: UpdateDatabaseErrorCode! +type UpdateDataFormatError implements UserError { + code: UpdateDataFormatErrorCode! message: String! path: [String!]! } -type UpdateDatabasePayload { - database: Database - errors: [UpdateDatabaseError!] +type UpdateDataFormatPayload { + dataFormat: DataFormat + errors: [UpdateDataFormatError!] query: Query! } @@ -2017,39 +2849,82 @@ type UpdateOpenIdConnectApplicationPayload { } type User implements Node { - contact: ContactInformation! @cost(weight: "10") - developedMethods(where: UserDevelopedMethodFilterInput @cost(weight: "10")): UserDevelopedMethodConnection! @cost(weight: "10") - gnuPgKeyFingerprints(where: UserGnuPgKeyFingerprintFilterInput @cost(weight: "10")): UserGnuPgKeyFingerprintConnection! @cost(weight: "10") - hasGnuPgKeyFingerprint(where: UserGnuPgKeyFingerprintFilterInput @cost(weight: "10")): Boolean! @cost(weight: "10") - hasPassword: Boolean @cost(weight: "10") + authorizedOpenIdConnectConsentTypes: [OpenIdConnectConsentType!]! + authorizedOpenIdConnectEndpoints: [OpenIdConnectEndpoint!]! + authorizedOpenIdConnectGrantTypes: [OpenIdConnectGrantType!]! + authorizedOpenIdConnectResponseTypes: [OpenIdConnectResponseType!]! + authorizedOpenIdConnectScopes: [OpenIdConnectScope!]! + contact: ContactInformation @cost(weight: "0") + developedMethods( + "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: [UserDevelopedMethodSortInput!] @cost(weight: "10") + where: UserDevelopedMethodFilterInput @cost(weight: "10") + ): UserDevelopedMethodConnection! @cost(weight: "10") + gnuPgKeyFingerprints( + order: [UserGnuPgKeyFingerprintSortInput!] @cost(weight: "10") + where: UserGnuPgKeyFingerprintFilterInput @cost(weight: "10") + ): UserGnuPgKeyFingerprintConnection! @cost(weight: "10") + hasGnuPgKeyFingerprint( + order: [UserGnuPgKeyFingerprintSortInput!] @cost(weight: "10") + where: UserGnuPgKeyFingerprintFilterInput @cost(weight: "10") + ): Boolean! @cost(weight: "10") + hasPassword: Boolean id: ID! - isAuthorizedToAddApprovals: Boolean! @cost(weight: "10") - isAuthorizedToDeleteUser: Boolean! @cost(weight: "10") - isAuthorizedToManageOpenIdConnect: Boolean! @cost(weight: "10") + isAuthorizedToAddApprovals: Boolean! @cost(weight: "1") + isAuthorizedToDeleteUser: Boolean! @cost(weight: "1") + isAuthorizedToManageOpenIdConnect: Boolean! @cost(weight: "1") "Full name" - name: String! @cost(weight: "10") - pendingDevelopedMethods(where: UserDevelopedMethodFilterInput @cost(weight: "10")): PendingUserDevelopedMethodConnection @authorize(policy: "WriteScope") @cost(weight: "10") - pendingRepresentedInstitutions(where: UserRepresentedInstitutionFilterInput @cost(weight: "10")): PendingUserRepresentedInstitutionConnection @authorize(policy: "WriteScope") @cost(weight: "10") - representedInstitutions(where: UserRepresentedInstitutionFilterInput @cost(weight: "10")): UserRepresentedInstitutionConnection! @cost(weight: "10") - roles: [UserRole!] @cost(weight: "10") - rolesCurrentUserCanAdd: [UserRole!]! @cost(weight: "10") - rolesCurrentUserCanRemove: [UserRole!]! @cost(weight: "10") - twoFactorAuthentication: TwoFactorAuthentication @cost(weight: "10") - uuid: Uuid! @cost(weight: "10") + name: String! + pendingDevelopedMethods( + "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: [UserDevelopedMethodSortInput!] @cost(weight: "10") + where: UserDevelopedMethodFilterInput @cost(weight: "10") + ): PendingUserDevelopedMethodConnection @cost(weight: "10") + pendingRepresentedInstitutions( + order: [UserRepresentedInstitutionSortInput!] @cost(weight: "10") + where: UserRepresentedInstitutionFilterInput @cost(weight: "10") + ): PendingUserRepresentedInstitutionConnection @cost(weight: "10") + representedInstitutions( + order: [UserRepresentedInstitutionSortInput!] @cost(weight: "10") + where: UserRepresentedInstitutionFilterInput @cost(weight: "10") + ): UserRepresentedInstitutionConnection! @cost(weight: "10") + roles: [UserRole!] @cost(weight: "0") + rolesCurrentUserCanAdd: [UserRole!]! + rolesCurrentUserCanRemove: [UserRole!]! + twoFactorAuthentication: TwoFactorAuthentication @cost(weight: "0") + uuid: Uuid! } type UserDevelopedMethodConnection { - edges: [UserDevelopedMethodEdge!]! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + edges: [UserDevelopedMethodEdge!]! @cost(weight: "0") + pageInfo: PageInfo! @cost(weight: "0") + totalCount: Int! } type UserDevelopedMethodEdge { - node: Method! @cost(weight: "10") + cursor: String! + isAuthorizedToConfirmEdge: Boolean! @cost(weight: "1") + isAuthorizedToRemoveEdge: Boolean! @cost(weight: "1") + node: Method! @cost(weight: "0") } type UserGnuPgKeyFingerprintConnection { - edges: [UserGnuPgKeyFingerprintEdge!]! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + edges: [UserGnuPgKeyFingerprintEdge!]! @cost(weight: "0") + totalCount: Int! } type UserGnuPgKeyFingerprintEdge { @@ -2057,18 +2932,19 @@ type UserGnuPgKeyFingerprintEdge { } type UserMethodDeveloperEdge { - isAuthorizedToConfirmEdge: Boolean! @cost(weight: "10") - isAuthorizedToRemoveEdge: Boolean! @cost(weight: "10") - node: User! @cost(weight: "10") + isAuthorizedToConfirmEdge: Boolean! @cost(weight: "1") + isAuthorizedToRemoveEdge: Boolean! @cost(weight: "1") + node: User! @cost(weight: "0") } type UserRepresentedInstitutionConnection { - edges: [UserRepresentedInstitutionEdge!]! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + edges: [UserRepresentedInstitutionEdge!]! @cost(weight: "0") + totalCount: Int! } type UserRepresentedInstitutionEdge { - node: Institution! @cost(weight: "10") + isAuthorizedToRemoveEdge: Boolean! @cost(weight: "1") + node: Institution! @cost(weight: "0") role: InstitutionRepresentativeRole! } @@ -2114,13 +2990,62 @@ type VerifyInstitutionPayload { query: Query! } +interface Approval { + approverId: Uuid! + keyFingerprint: String! + message: String! + query: GraphQlQuery! + signature: String! + timestamp: DateTime! + variables: Any! +} + +interface Data { + appliedMethod: AppliedMethod! + approvals: [DataApproval!]! + component: Component + componentId: Uuid! + createdAt: DateTime! + creator: Institution + creatorId: Uuid! + database: Database + databaseId: Uuid! + description: String + id: ID! + kind: DataKind! + locale: Locale! + name: String + resources: [GetHttpsResource!]! + resourceTree: GetHttpsResourceTree! + timestamp: DateTime! + 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 OpenIdConnectSubject = OpenIdConnectApplication | User + union Reference = Publication | Standard union Stakeholder = Institution | User input AddComponentAssemblyInput { assembledComponentId: Uuid! - index: Byte + index: UnsignedByte partComponentId: Uuid! primeSurface: PrimeSurface } @@ -2177,29 +3102,19 @@ input BooleanFilterInput { input ByteFilterInput { 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 CalendarSystemFilterInput { - and: [CalendarSystemFilterInput!] - or: [CalendarSystemFilterInput!] - id: StringFilterInput - name: StringFilterInput - minYear: IntFilterInput - maxYear: IntFilterInput - eras: FilterInputTypeOfErasFilterInput -} - input CalorimetricDataPropositionInput { and: [CalorimetricDataPropositionInput!] componentId: UuidPropositionInput @@ -2217,7 +3132,7 @@ input ChangeInstitutionRepresentativeRoleInput { } input ChangeUserEmailInput { - newEmail: String! + newEmail: EmailAddress! } input ChangeUserPasswordInput { @@ -2257,96 +3172,155 @@ input CoatedSidePropositionInput { input ComponentAssembledOfFilterInput { and: [ComponentAssembledOfFilterInput!] + createdAt: DateTimeFilterInput + index: ByteFilterInput or: [ComponentAssembledOfFilterInput!] partComponent: ComponentFilterInput - index: ByteFilterInput - primeSurface: NullableOfPrimeSurfaceFilterInput + primeSurface: PrimeSurfaceFilterInput + updatedAt: DateTimeFilterInput } -input ComponentAssemblySortInput { - assembledComponent: ComponentSortInput @cost(weight: "10") - partComponent: ComponentSortInput @cost(weight: "10") +input ComponentAssembledOfSortInput { + createdAt: SortEnumType @cost(weight: "10") index: SortEnumType @cost(weight: "10") primeSurface: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") +} + +input ComponentAssembliesFilterInput { + all: ComponentAssembledOfFilterInput @cost(weight: "10") + any: Boolean @cost(weight: "10") + none: ComponentAssembledOfFilterInput @cost(weight: "10") + some: ComponentAssembledOfFilterInput @cost(weight: "10") } input ComponentCategoryFilterInput { equalTo: ComponentCategory @cost(weight: "10") - notEqualTo: ComponentCategory @cost(weight: "10") in: [ComponentCategory!] @cost(weight: "10") + notEqualTo: ComponentCategory @cost(weight: "10") notIn: [ComponentCategory!] @cost(weight: "10") } input ComponentCategorysFilterInput { all: ComponentCategoryFilterInput @cost(weight: "10") + any: Boolean @cost(weight: "10") none: ComponentCategoryFilterInput @cost(weight: "10") some: ComponentCategoryFilterInput @cost(weight: "10") - any: Boolean @cost(weight: "10") } input ComponentConcretizationOfFilterInput { and: [ComponentConcretizationOfFilterInput!] - or: [ComponentConcretizationOfFilterInput!] + createdAt: DateTimeFilterInput generalComponent: ComponentFilterInput + or: [ComponentConcretizationOfFilterInput!] + updatedAt: DateTimeFilterInput +} + +input ComponentConcretizationOfSortInput { + createdAt: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input ComponentFilterInput { - and: [ComponentFilterInput!] - or: [ComponentFilterInput!] - id: UuidFilterInput - name: StringFilterInput abbreviation: StringFilterInput - description: StringFilterInput + and: [ComponentFilterInput!] categories: ComponentCategorysFilterInput + concretizations: ComponentsFilterInput + createdAt: DateTimeFilterInput + description: StringFilterInput extras: JsonElementFilterInput - partOf: FilterInputTypeOfComponentsFilterInput - parts: FilterInputTypeOfComponentsFilterInput - partOfEdges: FilterInputTypeOfComponentAssemblysFilterInput - partEdges: FilterInputTypeOfComponentAssemblysFilterInput - concretizations: FilterInputTypeOfComponentsFilterInput - generalizations: FilterInputTypeOfComponentsFilterInput - variants: FilterInputTypeOfComponentsFilterInput + generalizations: ComponentsFilterInput + id: UuidFilterInput manager: InstitutionFilterInput - manufacturers: FilterInputTypeOfInstitutionsFilterInput - manufacturerEdges: FilterInputTypeOfComponentManufacturersFilterInput + manufacturerEdges: ComponentManufacturersFilterInput + manufacturers: InstitutionsFilterInput + name: StringFilterInput + or: [ComponentFilterInput!] + partEdges: ComponentAssembliesFilterInput + partOf: ComponentsFilterInput + partOfEdges: ComponentAssembliesFilterInput + parts: ComponentsFilterInput + updatedAt: DateTimeFilterInput + variants: ComponentsFilterInput } input ComponentGeneralizationOfFilterInput { and: [ComponentGeneralizationOfFilterInput!] - or: [ComponentGeneralizationOfFilterInput!] concreteComponent: ComponentFilterInput + createdAt: DateTimeFilterInput + or: [ComponentGeneralizationOfFilterInput!] + updatedAt: DateTimeFilterInput +} + +input ComponentGeneralizationOfSortInput { + createdAt: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input ComponentManufacturerFilterInput { and: [ComponentManufacturerFilterInput!] - or: [ComponentManufacturerFilterInput!] + createdAt: DateTimeFilterInput institution: InstitutionFilterInput + or: [ComponentManufacturerFilterInput!] + updatedAt: DateTimeFilterInput +} + +input ComponentManufacturersFilterInput { + all: ComponentManufacturerFilterInput @cost(weight: "10") + any: Boolean @cost(weight: "10") + none: ComponentManufacturerFilterInput @cost(weight: "10") + some: ComponentManufacturerFilterInput @cost(weight: "10") } input ComponentManufacturerSortInput { - component: ComponentSortInput @cost(weight: "10") - institution: InstitutionSortInput @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input ComponentPartOfFilterInput { and: [ComponentPartOfFilterInput!] - or: [ComponentPartOfFilterInput!] assembledComponent: ComponentFilterInput + createdAt: DateTimeFilterInput index: ByteFilterInput - primeSurface: NullableOfPrimeSurfaceFilterInput + or: [ComponentPartOfFilterInput!] + primeSurface: PrimeSurfaceFilterInput + updatedAt: DateTimeFilterInput +} + +input ComponentPartOfSortInput { + createdAt: SortEnumType @cost(weight: "10") + index: SortEnumType @cost(weight: "10") + primeSurface: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") +} + +input ComponentsFilterInput { + all: ComponentFilterInput @cost(weight: "10") + any: Boolean @cost(weight: "10") + none: ComponentFilterInput @cost(weight: "10") + some: ComponentFilterInput @cost(weight: "10") } input ComponentSortInput { - id: SortEnumType @cost(weight: "10") - name: SortEnumType @cost(weight: "10") abbreviation: SortEnumType @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") description: SortEnumType @cost(weight: "10") + id: SortEnumType @cost(weight: "10") + name: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input ComponentVariantOfFilterInput { and: [ComponentVariantOfFilterInput!] - or: [ComponentVariantOfFilterInput!] + createdAt: DateTimeFilterInput ofComponent: ComponentFilterInput + or: [ComponentVariantOfFilterInput!] + updatedAt: DateTimeFilterInput +} + +input ComponentVariantOfSortInput { + createdAt: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input ConfirmComponentManufacturerInput { @@ -2366,13 +3340,13 @@ input ConfirmInstitutionRepresentativeInput { input ConfirmUserEmailChangeInput { confirmationCode: String! - currentEmail: String! - newEmail: String! + currentEmail: EmailAddress! + newEmail: EmailAddress! } input ConfirmUserEmailInput { confirmationCode: String! - email: String! + email: EmailAddress! } input ConfirmUserMethodDeveloperInput { @@ -2382,28 +3356,26 @@ input ConfirmUserMethodDeveloperInput { input ContactInformationFilterInput { and: [ContactInformationFilterInput!] + emailAddress: StringFilterInput + isEmailAddressConfirmed: BooleanFilterInput + isPhoneNumberConfirmed: BooleanFilterInput or: [ContactInformationFilterInput!] phoneNumber: StringFilterInput - isPhoneNumberConfirmed: BooleanFilterInput postalAddress: StringFilterInput - emailAddress: StringFilterInput - isEmailAddressConfirmed: BooleanFilterInput websiteLocator: UrlFilterInput } input ContactInformationInput { - emailAddress: String - phoneNumber: String + emailAddress: EmailAddress + phoneNumber: PhoneNumber postalAddress: String websiteLocator: Url } input ContactInformationSortInput { + emailAddress: SortEnumType @cost(weight: "10") phoneNumber: SortEnumType @cost(weight: "10") - isPhoneNumberConfirmed: SortEnumType @cost(weight: "10") postalAddress: SortEnumType @cost(weight: "10") - emailAddress: SortEnumType @cost(weight: "10") - isEmailAddressConfirmed: SortEnumType @cost(weight: "10") websiteLocator: UriSortInput @cost(weight: "10") } @@ -2422,6 +3394,13 @@ input CreateComponentInput { switchableLayers: DescriptionOrReferenceInput } +input CreateDatabaseInput { + description: String! + locator: Url! + name: String! + operatorId: Uuid! +} + input CreateDataFormatInput { description: String! extension: String @@ -2432,13 +3411,6 @@ input CreateDataFormatInput { schemaLocator: Url } -input CreateDatabaseInput { - description: String! - locator: Url! - name: String! - operatorId: Uuid! -} - input CreateInstitutionInput { abbreviation: String contact: ContactInformationInput @@ -2478,26 +3450,64 @@ input CreateOpenIdConnectApplicationInput { scopes: [OpenIdConnectScope!]! } -input DataFormatFilterInput { - and: [DataFormatFilterInput!] - or: [DataFormatFilterInput!] +input DatabaseFilterInput { + and: [DatabaseFilterInput!] + createdAt: DateTimeFilterInput + description: StringFilterInput id: UuidFilterInput + locator: UrlFilterInput name: StringFilterInput - extension: StringFilterInput + operator: InstitutionFilterInput + or: [DatabaseFilterInput!] + updatedAt: DateTimeFilterInput +} + +input DatabasesFilterInput { + all: DatabaseFilterInput @cost(weight: "10") + any: Boolean @cost(weight: "10") + none: DatabaseFilterInput @cost(weight: "10") + some: DatabaseFilterInput @cost(weight: "10") +} + +input DatabaseSortInput { + createdAt: SortEnumType @cost(weight: "10") + description: SortEnumType @cost(weight: "10") + id: SortEnumType @cost(weight: "10") + locator: UriSortInput @cost(weight: "10") + name: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") +} + +input DataFormatFilterInput { + and: [DataFormatFilterInput!] + createdAt: DateTimeFilterInput description: StringFilterInput + extension: StringFilterInput + id: UuidFilterInput + manager: InstitutionFilterInput mediaType: StringFilterInput + name: StringFilterInput + or: [DataFormatFilterInput!] schemaLocator: UrlFilterInput - manager: InstitutionFilterInput + updatedAt: DateTimeFilterInput +} + +input DataFormatsFilterInput { + all: DataFormatFilterInput @cost(weight: "10") + any: Boolean @cost(weight: "10") + none: DataFormatFilterInput @cost(weight: "10") + some: DataFormatFilterInput @cost(weight: "10") } input DataFormatSortInput { - id: SortEnumType @cost(weight: "10") - name: SortEnumType @cost(weight: "10") - extension: SortEnumType @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") description: SortEnumType @cost(weight: "10") + extension: SortEnumType @cost(weight: "10") + id: SortEnumType @cost(weight: "10") mediaType: SortEnumType @cost(weight: "10") + name: SortEnumType @cost(weight: "10") schemaLocator: UriSortInput @cost(weight: "10") - manager: InstitutionSortInput @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input DataPropositionInput { @@ -2508,51 +3518,33 @@ input DataPropositionInput { resources: GetHttpsResourcesPropositionInput } -input DatabaseFilterInput { - and: [DatabaseFilterInput!] - or: [DatabaseFilterInput!] - id: UuidFilterInput - name: StringFilterInput - description: StringFilterInput - locator: UrlFilterInput - operator: InstitutionFilterInput -} - -input DatabaseSortInput { - id: SortEnumType @cost(weight: "10") - name: SortEnumType @cost(weight: "10") - description: SortEnumType @cost(weight: "10") - locator: UriSortInput @cost(weight: "10") - operator: InstitutionSortInput @cost(weight: "10") -} - input DateTimeFilterInput { 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 DecimalFilterInput { 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") } @@ -2578,8 +3570,8 @@ input DeleteUserInput { input DescriptionOrReferenceFilterInput { and: [DescriptionOrReferenceFilterInput!] - or: [DescriptionOrReferenceFilterInput!] description: StringFilterInput + or: [DescriptionOrReferenceFilterInput!] } input DescriptionOrReferenceInput { @@ -2591,14 +3583,23 @@ input DescriptionOrReferenceSortInput { description: SortEnumType @cost(weight: "10") } -input EnableUserTwoFactorAuthenticatorInput { - verificationCode: String! +input DurationFilterInput { + 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 EraFilterInput { - and: [EraFilterInput!] - or: [EraFilterInput!] - name: StringFilterInput +input EnableUserTwoFactorAuthenticatorInput { + verificationCode: String! } input FileMetaInformationPropositionInput { @@ -2611,118 +3612,20 @@ input FilesMetaInformationPropositionInput { some: FileMetaInformationPropositionInput } -input FilterInputTypeOfComponentAssemblysFilterInput { - all: ComponentAssembledOfFilterInput @cost(weight: "10") - none: ComponentAssembledOfFilterInput @cost(weight: "10") - some: ComponentAssembledOfFilterInput @cost(weight: "10") - any: Boolean @cost(weight: "10") -} - -input FilterInputTypeOfComponentManufacturersFilterInput { - all: ComponentManufacturerFilterInput @cost(weight: "10") - none: ComponentManufacturerFilterInput @cost(weight: "10") - some: ComponentManufacturerFilterInput @cost(weight: "10") - any: Boolean @cost(weight: "10") -} - -input FilterInputTypeOfComponentsFilterInput { - all: ComponentFilterInput @cost(weight: "10") - none: ComponentFilterInput @cost(weight: "10") - some: ComponentFilterInput @cost(weight: "10") - any: Boolean @cost(weight: "10") -} - -input FilterInputTypeOfDataFormatsFilterInput { - all: DataFormatFilterInput @cost(weight: "10") - none: DataFormatFilterInput @cost(weight: "10") - some: DataFormatFilterInput @cost(weight: "10") - any: Boolean @cost(weight: "10") -} - -input FilterInputTypeOfDatabasesFilterInput { - all: DatabaseFilterInput @cost(weight: "10") - none: DatabaseFilterInput @cost(weight: "10") - some: DatabaseFilterInput @cost(weight: "10") - any: Boolean @cost(weight: "10") -} - -input FilterInputTypeOfErasFilterInput { - all: EraFilterInput @cost(weight: "10") - none: EraFilterInput @cost(weight: "10") - some: EraFilterInput @cost(weight: "10") - any: Boolean @cost(weight: "10") -} - -input FilterInputTypeOfGnuPgKeyFingerprintsFilterInput { - all: GnuPgKeyFingerprintFilterInput @cost(weight: "10") - none: GnuPgKeyFingerprintFilterInput @cost(weight: "10") - some: GnuPgKeyFingerprintFilterInput @cost(weight: "10") - any: Boolean @cost(weight: "10") -} - -input FilterInputTypeOfInstitutionMethodDevelopersFilterInput { - all: InstitutionDevelopedMethodFilterInput @cost(weight: "10") - none: InstitutionDevelopedMethodFilterInput @cost(weight: "10") - some: InstitutionDevelopedMethodFilterInput @cost(weight: "10") - any: Boolean @cost(weight: "10") -} - -input FilterInputTypeOfInstitutionRepresentativesFilterInput { - all: InstitutionRepresentativeFilterInput @cost(weight: "10") - none: InstitutionRepresentativeFilterInput @cost(weight: "10") - some: InstitutionRepresentativeFilterInput @cost(weight: "10") - any: Boolean @cost(weight: "10") -} - -input FilterInputTypeOfInstitutionsFilterInput { - all: InstitutionFilterInput @cost(weight: "10") - none: InstitutionFilterInput @cost(weight: "10") - some: InstitutionFilterInput @cost(weight: "10") - any: Boolean @cost(weight: "10") -} - -input FilterInputTypeOfMethodsFilterInput { - all: InstitutionManagedMethodFilterInput @cost(weight: "10") - none: InstitutionManagedMethodFilterInput @cost(weight: "10") - some: InstitutionManagedMethodFilterInput @cost(weight: "10") - any: Boolean @cost(weight: "10") -} - -input FilterInputTypeOfOpenIdConnectTokensFilterInput { - all: OpenIdConnectTokenFilterInput @cost(weight: "10") - none: OpenIdConnectTokenFilterInput @cost(weight: "10") - some: OpenIdConnectTokenFilterInput @cost(weight: "10") - any: Boolean @cost(weight: "10") -} - -input FilterInputTypeOfUserMethodDevelopersFilterInput { - all: UserDevelopedMethodFilterInput @cost(weight: "10") - none: UserDevelopedMethodFilterInput @cost(weight: "10") - some: UserDevelopedMethodFilterInput @cost(weight: "10") - any: Boolean @cost(weight: "10") -} - -input FilterInputTypeOfUsersFilterInput { - all: UserFilterInput @cost(weight: "10") - none: UserFilterInput @cost(weight: "10") - some: UserFilterInput @cost(weight: "10") - any: Boolean @cost(weight: "10") -} - input FloatFilterInput { 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: ClosedIntervalInputOfDoubleInput @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: ClosedIntervalInputOfDoubleInput @cost(weight: "10") } input FloatPropositionInput { @@ -2763,25 +3666,32 @@ input GetHttpsResourcesPropositionInput { } input GnuPgKeyFingerprintFilterInput { + allowedAt: DateTimeFilterInput and: [GnuPgKeyFingerprintFilterInput!] - or: [GnuPgKeyFingerprintFilterInput!] - id: UuidFilterInput + createdAt: DateTimeFilterInput fingerprint: StringFilterInput - createdAt: OffsetDateTimeFilterInput - allowedAt: OffsetDateTimeFilterInput - forbiddenAt: OffsetDateTimeFilterInput - user: UserFilterInput + forbiddenAt: DateTimeFilterInput + id: UuidFilterInput institution: InstitutionFilterInput + or: [GnuPgKeyFingerprintFilterInput!] + updatedAt: DateTimeFilterInput + user: UserFilterInput +} + +input GnuPgKeyFingerprintsFilterInput { + all: GnuPgKeyFingerprintFilterInput @cost(weight: "10") + any: Boolean @cost(weight: "10") + none: GnuPgKeyFingerprintFilterInput @cost(weight: "10") + some: GnuPgKeyFingerprintFilterInput @cost(weight: "10") } input GnuPgKeyFingerprintSortInput { - id: SortEnumType @cost(weight: "10") - fingerprint: SortEnumType @cost(weight: "10") - createdAt: SortEnumType @cost(weight: "10") allowedAt: SortEnumType @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") + fingerprint: SortEnumType @cost(weight: "10") forbiddenAt: SortEnumType @cost(weight: "10") - user: UserSortInput @cost(weight: "10") - institution: InstitutionSortInput @cost(weight: "10") + id: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input HygrothermalDataPropositionInput { @@ -2792,209 +3702,313 @@ input HygrothermalDataPropositionInput { resources: GetHttpsResourcesPropositionInput } -input IMethodDeveloperFilterInput { - and: [IMethodDeveloperFilterInput!] - or: [IMethodDeveloperFilterInput!] -} - input InstitutionDevelopedMethodFilterInput { and: [InstitutionDevelopedMethodFilterInput!] - or: [InstitutionDevelopedMethodFilterInput!] + createdAt: DateTimeFilterInput method: InstitutionManagedMethodFilterInput + or: [InstitutionDevelopedMethodFilterInput!] + updatedAt: DateTimeFilterInput +} + +input InstitutionDevelopedMethodSortInput { + createdAt: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input InstitutionFilterInput { - and: [InstitutionFilterInput!] - or: [InstitutionFilterInput!] - id: UuidFilterInput - name: StringFilterInput abbreviation: StringFilterInput - description: StringFilterInput + and: [InstitutionFilterInput!] contact: ContactInformationFilterInput - state: InstitutionStateFilterInput + createdAt: DateTimeFilterInput + description: StringFilterInput + developedMethodEdges: InstitutionMethodDevelopersFilterInput + developedMethods: MethodsFilterInput extras: JsonElementFilterInput - developedMethods: FilterInputTypeOfMethodsFilterInput - developedMethodEdges: FilterInputTypeOfInstitutionMethodDevelopersFilterInput - managedMethods: FilterInputTypeOfMethodsFilterInput - managedDataFormats: FilterInputTypeOfDataFormatsFilterInput - manufacturedComponents: FilterInputTypeOfComponentsFilterInput - manufacturedComponentEdges: FilterInputTypeOfComponentManufacturersFilterInput - operatedDatabases: FilterInputTypeOfDatabasesFilterInput + gnuPgKeyFingerprints: GnuPgKeyFingerprintsFilterInput + id: UuidFilterInput + managedDataFormats: DataFormatsFilterInput + managedInstitutions: InstitutionsFilterInput + managedMethods: MethodsFilterInput manager: InstitutionFilterInput - managedInstitutions: FilterInputTypeOfInstitutionsFilterInput - representatives: FilterInputTypeOfUsersFilterInput - representativeEdges: FilterInputTypeOfInstitutionRepresentativesFilterInput - gnuPgKeyFingerprints: FilterInputTypeOfGnuPgKeyFingerprintsFilterInput + manufacturedComponentEdges: ComponentManufacturersFilterInput + manufacturedComponents: ComponentsFilterInput + name: StringFilterInput + operatedDatabases: DatabasesFilterInput + or: [InstitutionFilterInput!] + representativeEdges: InstitutionRepresentativesFilterInput + representatives: UsersFilterInput + state: InstitutionStateFilterInput + updatedAt: DateTimeFilterInput } input InstitutionGnuPgKeyFingerprintFilterInput { + allowedAt: DateTimeFilterInput and: [InstitutionGnuPgKeyFingerprintFilterInput!] - or: [InstitutionGnuPgKeyFingerprintFilterInput!] - id: UuidFilterInput + createdAt: DateTimeFilterInput fingerprint: StringFilterInput - createdAt: OffsetDateTimeFilterInput - allowedAt: OffsetDateTimeFilterInput - forbiddenAt: OffsetDateTimeFilterInput + forbiddenAt: DateTimeFilterInput + id: UuidFilterInput + or: [InstitutionGnuPgKeyFingerprintFilterInput!] + updatedAt: DateTimeFilterInput user: UserFilterInput } +input InstitutionGnuPgKeyFingerprintSortInput { + allowedAt: SortEnumType @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") + fingerprint: SortEnumType @cost(weight: "10") + forbiddenAt: SortEnumType @cost(weight: "10") + id: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") +} + input InstitutionManagedComponentFilterInput { - and: [InstitutionManagedComponentFilterInput!] - or: [InstitutionManagedComponentFilterInput!] - id: UuidFilterInput - name: StringFilterInput abbreviation: StringFilterInput - description: StringFilterInput + and: [InstitutionManagedComponentFilterInput!] categories: ComponentCategorysFilterInput + concretizations: ComponentsFilterInput + createdAt: DateTimeFilterInput + description: StringFilterInput extras: JsonElementFilterInput - partOf: FilterInputTypeOfComponentsFilterInput - parts: FilterInputTypeOfComponentsFilterInput - partOfEdges: FilterInputTypeOfComponentAssemblysFilterInput - partEdges: FilterInputTypeOfComponentAssemblysFilterInput - concretizations: FilterInputTypeOfComponentsFilterInput - generalizations: FilterInputTypeOfComponentsFilterInput - variants: FilterInputTypeOfComponentsFilterInput - manufacturers: FilterInputTypeOfInstitutionsFilterInput - manufacturerEdges: FilterInputTypeOfComponentManufacturersFilterInput + generalizations: ComponentsFilterInput + id: UuidFilterInput + manufacturerEdges: ComponentManufacturersFilterInput + manufacturers: InstitutionsFilterInput + name: StringFilterInput + or: [InstitutionManagedComponentFilterInput!] + partEdges: ComponentAssembliesFilterInput + partOf: ComponentsFilterInput + partOfEdges: ComponentAssembliesFilterInput + parts: ComponentsFilterInput + updatedAt: DateTimeFilterInput + variants: ComponentsFilterInput +} + +input InstitutionManagedComponentSortInput { + abbreviation: SortEnumType @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") + description: SortEnumType @cost(weight: "10") + id: SortEnumType @cost(weight: "10") + name: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input InstitutionManagedDataFormatFilterInput { and: [InstitutionManagedDataFormatFilterInput!] - or: [InstitutionManagedDataFormatFilterInput!] - id: UuidFilterInput - name: StringFilterInput - extension: StringFilterInput + createdAt: DateTimeFilterInput description: StringFilterInput + extension: StringFilterInput + id: UuidFilterInput mediaType: StringFilterInput + name: StringFilterInput + or: [InstitutionManagedDataFormatFilterInput!] schemaLocator: UrlFilterInput + updatedAt: DateTimeFilterInput +} + +input InstitutionManagedDataFormatSortInput { + createdAt: SortEnumType @cost(weight: "10") + description: SortEnumType @cost(weight: "10") + extension: SortEnumType @cost(weight: "10") + id: SortEnumType @cost(weight: "10") + mediaType: SortEnumType @cost(weight: "10") + name: SortEnumType @cost(weight: "10") + schemaLocator: UriSortInput @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input InstitutionManagedInstitutionFilterInput { + abbreviation: StringFilterInput and: [InstitutionManagedInstitutionFilterInput!] - or: [InstitutionManagedInstitutionFilterInput!] + contact: ContactInformationFilterInput + createdAt: DateTimeFilterInput + description: StringFilterInput + developedMethodEdges: InstitutionMethodDevelopersFilterInput + developedMethods: MethodsFilterInput + extras: JsonElementFilterInput + gnuPgKeyFingerprints: GnuPgKeyFingerprintsFilterInput id: UuidFilterInput + managedDataFormats: DataFormatsFilterInput + managedInstitutions: InstitutionsFilterInput + managedMethods: MethodsFilterInput + manufacturedComponentEdges: ComponentManufacturersFilterInput + manufacturedComponents: ComponentsFilterInput name: StringFilterInput - abbreviation: StringFilterInput - description: StringFilterInput - contact: ContactInformationFilterInput + operatedDatabases: DatabasesFilterInput + or: [InstitutionManagedInstitutionFilterInput!] + representativeEdges: InstitutionRepresentativesFilterInput + representatives: UsersFilterInput state: InstitutionStateFilterInput - extras: JsonElementFilterInput - developedMethods: FilterInputTypeOfMethodsFilterInput - developedMethodEdges: FilterInputTypeOfInstitutionMethodDevelopersFilterInput - managedMethods: FilterInputTypeOfMethodsFilterInput - managedDataFormats: FilterInputTypeOfDataFormatsFilterInput - manufacturedComponents: FilterInputTypeOfComponentsFilterInput - manufacturedComponentEdges: FilterInputTypeOfComponentManufacturersFilterInput - operatedDatabases: FilterInputTypeOfDatabasesFilterInput - managedInstitutions: FilterInputTypeOfInstitutionsFilterInput - representatives: FilterInputTypeOfUsersFilterInput - representativeEdges: FilterInputTypeOfInstitutionRepresentativesFilterInput - gnuPgKeyFingerprints: FilterInputTypeOfGnuPgKeyFingerprintsFilterInput + updatedAt: DateTimeFilterInput +} + +input InstitutionManagedInstitutionSortInput { + abbreviation: SortEnumType @cost(weight: "10") + contact: ContactInformationSortInput @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") + description: SortEnumType @cost(weight: "10") + id: SortEnumType @cost(weight: "10") + name: SortEnumType @cost(weight: "10") + state: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input InstitutionManagedMethodFilterInput { and: [InstitutionManagedMethodFilterInput!] - or: [InstitutionManagedMethodFilterInput!] - id: UuidFilterInput - name: StringFilterInput - description: StringFilterInput calculationLocator: UrlFilterInput categories: MethodCategorysFilterInput - institutionDevelopers: FilterInputTypeOfInstitutionsFilterInput - institutionDeveloperEdges: FilterInputTypeOfInstitutionMethodDevelopersFilterInput - userDevelopers: FilterInputTypeOfUsersFilterInput - userDeveloperEdges: FilterInputTypeOfUserMethodDevelopersFilterInput + createdAt: DateTimeFilterInput + description: StringFilterInput + id: UuidFilterInput + institutionDeveloperEdges: InstitutionMethodDevelopersFilterInput + institutionDevelopers: InstitutionsFilterInput + name: StringFilterInput + or: [InstitutionManagedMethodFilterInput!] + updatedAt: DateTimeFilterInput + userDeveloperEdges: UserMethodDevelopersFilterInput + userDevelopers: UsersFilterInput +} + +input InstitutionManagedMethodSortInput { + calculationLocator: UriSortInput @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") + description: SortEnumType @cost(weight: "10") + id: SortEnumType @cost(weight: "10") + name: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input InstitutionManufacturedComponentFilterInput { and: [InstitutionManufacturedComponentFilterInput!] - or: [InstitutionManufacturedComponentFilterInput!] component: ComponentFilterInput + createdAt: DateTimeFilterInput + or: [InstitutionManufacturedComponentFilterInput!] + updatedAt: DateTimeFilterInput } -input InstitutionMethodDeveloperSortInput { - method: MethodSortInput @cost(weight: "10") - institution: InstitutionSortInput @cost(weight: "10") +input InstitutionManufacturedComponentSortInput { + createdAt: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") +} + +input InstitutionMethodDevelopersFilterInput { + all: InstitutionDevelopedMethodFilterInput @cost(weight: "10") + any: Boolean @cost(weight: "10") + none: InstitutionDevelopedMethodFilterInput @cost(weight: "10") + some: InstitutionDevelopedMethodFilterInput @cost(weight: "10") } input InstitutionOperatedDatabaseFilterInput { and: [InstitutionOperatedDatabaseFilterInput!] - or: [InstitutionOperatedDatabaseFilterInput!] - id: UuidFilterInput - name: StringFilterInput + createdAt: DateTimeFilterInput description: StringFilterInput + id: UuidFilterInput locator: UrlFilterInput + name: StringFilterInput + or: [InstitutionOperatedDatabaseFilterInput!] + updatedAt: DateTimeFilterInput +} + +input InstitutionOperatedDatabaseSortInput { + createdAt: SortEnumType @cost(weight: "10") + description: SortEnumType @cost(weight: "10") + id: SortEnumType @cost(weight: "10") + locator: UriSortInput @cost(weight: "10") + name: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input InstitutionOwnedOpenIdConnectApplicationFilterInput { and: [InstitutionOwnedOpenIdConnectApplicationFilterInput!] - or: [InstitutionOwnedOpenIdConnectApplicationFilterInput!] - id: UuidFilterInput applicationType: StringFilterInput clientId: StringFilterInput consentType: StringFilterInput + createdAt: DateTimeFilterInput displayName: StringFilterInput + id: UuidFilterInput + or: [InstitutionOwnedOpenIdConnectApplicationFilterInput!] + updatedAt: DateTimeFilterInput +} + +input InstitutionOwnedOpenIdConnectApplicationSortInput { + applicationType: SortEnumType @cost(weight: "10") + clientId: SortEnumType @cost(weight: "10") + consentType: SortEnumType @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") + displayName: SortEnumType @cost(weight: "10") + id: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input InstitutionRepresentativeFilterInput { and: [InstitutionRepresentativeFilterInput!] + createdAt: DateTimeFilterInput or: [InstitutionRepresentativeFilterInput!] - user: UserFilterInput role: InstitutionRepresentativeRoleFilterInput + updatedAt: DateTimeFilterInput + user: UserFilterInput } input InstitutionRepresentativeRoleFilterInput { equalTo: InstitutionRepresentativeRole @cost(weight: "10") - notEqualTo: InstitutionRepresentativeRole @cost(weight: "10") in: [InstitutionRepresentativeRole!] @cost(weight: "10") + notEqualTo: InstitutionRepresentativeRole @cost(weight: "10") notIn: [InstitutionRepresentativeRole!] @cost(weight: "10") } +input InstitutionRepresentativesFilterInput { + all: InstitutionRepresentativeFilterInput @cost(weight: "10") + any: Boolean @cost(weight: "10") + none: InstitutionRepresentativeFilterInput @cost(weight: "10") + some: InstitutionRepresentativeFilterInput @cost(weight: "10") +} + input InstitutionRepresentativeSortInput { - institution: InstitutionSortInput @cost(weight: "10") - user: UserSortInput @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") role: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") +} + +input InstitutionsFilterInput { + all: InstitutionFilterInput @cost(weight: "10") + any: Boolean @cost(weight: "10") + none: InstitutionFilterInput @cost(weight: "10") + some: InstitutionFilterInput @cost(weight: "10") } input InstitutionSortInput { - id: SortEnumType @cost(weight: "10") - name: SortEnumType @cost(weight: "10") abbreviation: SortEnumType @cost(weight: "10") - description: SortEnumType @cost(weight: "10") contact: ContactInformationSortInput @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") + description: SortEnumType @cost(weight: "10") + id: SortEnumType @cost(weight: "10") + name: SortEnumType @cost(weight: "10") state: SortEnumType @cost(weight: "10") - manager: InstitutionSortInput @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input InstitutionStateFilterInput { equalTo: InstitutionState @cost(weight: "10") - notEqualTo: InstitutionState @cost(weight: "10") in: [InstitutionState!] @cost(weight: "10") + notEqualTo: InstitutionState @cost(weight: "10") notIn: [InstitutionState!] @cost(weight: "10") } input IntFilterInput { 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 IsoDayOfWeekFilterInput { - equalTo: IsoDayOfWeek @cost(weight: "10") - notEqualTo: IsoDayOfWeek @cost(weight: "10") - in: [IsoDayOfWeek!] @cost(weight: "10") - notIn: [IsoDayOfWeek!] @cost(weight: "10") -} - input JsonElementFilterInput { and: [JsonElementFilterInput!] or: [JsonElementFilterInput!] @@ -3003,8 +4017,8 @@ input JsonElementFilterInput { input JsonValueKindFilterInput { 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") } @@ -3017,58 +4031,52 @@ input LifeCycleDataPropositionInput { } input LocalDateFilterInput { - and: [LocalDateFilterInput!] - or: [LocalDateFilterInput!] - calendar: CalendarSystemFilterInput - year: IntFilterInput - month: IntFilterInput - day: IntFilterInput - dayOfWeek: IsoDayOfWeekFilterInput - yearOfEra: IntFilterInput - era: EraFilterInput - dayOfYear: IntFilterInput + 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 LocalDateTimeFilterInput { - and: [LocalDateTimeFilterInput!] - or: [LocalDateTimeFilterInput!] - calendar: CalendarSystemFilterInput - year: IntFilterInput - yearOfEra: IntFilterInput - era: EraFilterInput - month: IntFilterInput - dayOfYear: IntFilterInput - day: IntFilterInput - dayOfWeek: IsoDayOfWeekFilterInput - hour: IntFilterInput - clockHourOfHalfDay: IntFilterInput - minute: IntFilterInput - second: IntFilterInput - millisecond: IntFilterInput - tickOfSecond: IntFilterInput - tickOfDay: LongFilterInput - nanosecondOfSecond: IntFilterInput - nanosecondOfDay: LongFilterInput - timeOfDay: LocalTimeFilterInput - date: LocalDateFilterInput + 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 LocalTimeFilterInput { - and: [LocalTimeFilterInput!] - or: [LocalTimeFilterInput!] - hour: IntFilterInput - clockHourOfHalfDay: IntFilterInput - minute: IntFilterInput - second: IntFilterInput - millisecond: IntFilterInput - tickOfSecond: IntFilterInput - tickOfDay: LongFilterInput - nanosecondOfSecond: IntFilterInput - nanosecondOfDay: LongFilterInput + 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 LoginUserInput { - email: String! + email: EmailAddress! password: String! } @@ -3083,46 +4091,60 @@ input LoginUserWithTwoFactorCodeInput { input LongFilterInput { 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") } input MethodCategoryFilterInput { equalTo: MethodCategory @cost(weight: "10") - notEqualTo: MethodCategory @cost(weight: "10") in: [MethodCategory!] @cost(weight: "10") + notEqualTo: MethodCategory @cost(weight: "10") notIn: [MethodCategory!] @cost(weight: "10") } input MethodCategorysFilterInput { all: MethodCategoryFilterInput @cost(weight: "10") + any: Boolean @cost(weight: "10") none: MethodCategoryFilterInput @cost(weight: "10") some: MethodCategoryFilterInput @cost(weight: "10") - any: Boolean @cost(weight: "10") +} + +input MethodDeveloperFilterInput { + and: [MethodDeveloperFilterInput!] + createdAt: DateTimeFilterInput + or: [MethodDeveloperFilterInput!] + updatedAt: DateTimeFilterInput +} + +input MethodDeveloperSortInput { + createdAt: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input MethodFilterInput { and: [MethodFilterInput!] - or: [MethodFilterInput!] - id: UuidFilterInput - name: StringFilterInput - description: StringFilterInput calculationLocator: UrlFilterInput categories: MethodCategorysFilterInput - institutionDevelopers: FilterInputTypeOfInstitutionsFilterInput - institutionDeveloperEdges: FilterInputTypeOfInstitutionMethodDevelopersFilterInput - userDevelopers: FilterInputTypeOfUsersFilterInput - userDeveloperEdges: FilterInputTypeOfUserMethodDevelopersFilterInput + createdAt: DateTimeFilterInput + description: StringFilterInput + id: UuidFilterInput + institutionDeveloperEdges: InstitutionMethodDevelopersFilterInput + institutionDevelopers: InstitutionsFilterInput manager: InstitutionFilterInput + name: StringFilterInput + or: [MethodFilterInput!] + updatedAt: DateTimeFilterInput + userDeveloperEdges: UserMethodDevelopersFilterInput + userDevelopers: UsersFilterInput } input MethodParameterInput { @@ -3130,12 +4152,20 @@ input MethodParameterInput { type: Any! } +input MethodsFilterInput { + all: InstitutionManagedMethodFilterInput @cost(weight: "10") + any: Boolean @cost(weight: "10") + none: InstitutionManagedMethodFilterInput @cost(weight: "10") + some: InstitutionManagedMethodFilterInput @cost(weight: "10") +} + input MethodSortInput { + calculationLocator: UriSortInput @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") + description: SortEnumType @cost(weight: "10") id: SortEnumType @cost(weight: "10") name: SortEnumType @cost(weight: "10") - description: SortEnumType @cost(weight: "10") - calculationLocator: UriSortInput @cost(weight: "10") - manager: InstitutionSortInput @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } "A data source given as a cross-database data reference when this method is applied." @@ -3144,11 +4174,19 @@ input MethodSourceInput { name: String! } -input NullableOfPrimeSurfaceFilterInput { - equalTo: PrimeSurface @cost(weight: "10") - notEqualTo: PrimeSurface @cost(weight: "10") - in: [PrimeSurface] @cost(weight: "10") - notIn: [PrimeSurface] @cost(weight: "10") +input NonNegativeIntFilterInput { + equalTo: NonNegativeInt @cost(weight: "10") + greaterThan: NonNegativeInt @cost(weight: "10") + greaterThanOrEqualTo: NonNegativeInt @cost(weight: "10") + in: [NonNegativeInt] @cost(weight: "10") + lessThan: NonNegativeInt @cost(weight: "10") + lessThanOrEqualTo: NonNegativeInt @cost(weight: "10") + notEqualTo: NonNegativeInt @cost(weight: "10") + notGreaterThan: NonNegativeInt @cost(weight: "10") + notGreaterThanOrEqualTo: NonNegativeInt @cost(weight: "10") + notIn: [NonNegativeInt] @cost(weight: "10") + notLessThan: NonNegativeInt @cost(weight: "10") + notLessThanOrEqualTo: NonNegativeInt @cost(weight: "10") } input NumerationInput { @@ -3157,41 +4195,6 @@ input NumerationInput { suffix: String } -input OffsetDateTimeFilterInput { - and: [OffsetDateTimeFilterInput!] - or: [OffsetDateTimeFilterInput!] - calendar: CalendarSystemFilterInput - year: IntFilterInput - month: IntFilterInput - day: IntFilterInput - dayOfWeek: IsoDayOfWeekFilterInput - yearOfEra: IntFilterInput - era: EraFilterInput - dayOfYear: IntFilterInput - hour: IntFilterInput - clockHourOfHalfDay: IntFilterInput - minute: IntFilterInput - second: IntFilterInput - millisecond: IntFilterInput - tickOfSecond: IntFilterInput - tickOfDay: LongFilterInput - nanosecondOfSecond: IntFilterInput - nanosecondOfDay: LongFilterInput - localDateTime: LocalDateTimeFilterInput - date: LocalDateFilterInput - timeOfDay: LocalTimeFilterInput - offset: OffsetFilterInput -} - -input OffsetFilterInput { - and: [OffsetFilterInput!] - or: [OffsetFilterInput!] - seconds: IntFilterInput - milliseconds: IntFilterInput - ticks: LongFilterInput - nanoseconds: LongFilterInput -} - input OpenEndedDateTimeRangeInput { from: DateTime to: DateTime @@ -3199,49 +4202,94 @@ input OpenEndedDateTimeRangeInput { input OpenIdConnectApplicationFilterInput { and: [OpenIdConnectApplicationFilterInput!] - or: [OpenIdConnectApplicationFilterInput!] - id: UuidFilterInput applicationType: StringFilterInput clientId: StringFilterInput consentType: StringFilterInput + createdAt: DateTimeFilterInput displayName: StringFilterInput + id: UuidFilterInput + or: [OpenIdConnectApplicationFilterInput!] + owner: InstitutionFilterInput + updatedAt: DateTimeFilterInput +} + +input OpenIdConnectApplicationSortInput { + applicationType: SortEnumType @cost(weight: "10") + clientId: SortEnumType @cost(weight: "10") + consentType: SortEnumType @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") + displayName: SortEnumType @cost(weight: "10") + id: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input OpenIdConnectAuthorizationFilterInput { and: [OpenIdConnectAuthorizationFilterInput!] - or: [OpenIdConnectAuthorizationFilterInput!] + application: InstitutionOwnedOpenIdConnectApplicationFilterInput + createdAt: DateTimeFilterInput id: UuidFilterInput - creationDate: DateTimeFilterInput + or: [OpenIdConnectAuthorizationFilterInput!] status: StringFilterInput subject: StringFilterInput - tokens: FilterInputTypeOfOpenIdConnectTokensFilterInput + tokens: OpenIdConnectTokensFilterInput type: StringFilterInput + updatedAt: DateTimeFilterInput +} + +input OpenIdConnectAuthorizationSortInput { + createdAt: SortEnumType @cost(weight: "10") + id: SortEnumType @cost(weight: "10") + status: SortEnumType @cost(weight: "10") + subject: SortEnumType @cost(weight: "10") + type: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input OpenIdConnectTokenFilterInput { and: [OpenIdConnectTokenFilterInput!] - or: [OpenIdConnectTokenFilterInput!] + application: InstitutionOwnedOpenIdConnectApplicationFilterInput + authorization: OpenIdConnectAuthorizationFilterInput + createdAt: DateTimeFilterInput + expiredAt: DateTimeFilterInput id: UuidFilterInput - creationDate: DateTimeFilterInput - expirationDate: DateTimeFilterInput - redemptionDate: DateTimeFilterInput + or: [OpenIdConnectTokenFilterInput!] + redeemedAt: DateTimeFilterInput status: StringFilterInput subject: StringFilterInput type: StringFilterInput + updatedAt: DateTimeFilterInput +} + +input OpenIdConnectTokensFilterInput { + all: OpenIdConnectTokenFilterInput @cost(weight: "10") + any: Boolean @cost(weight: "10") + none: OpenIdConnectTokenFilterInput @cost(weight: "10") + some: OpenIdConnectTokenFilterInput @cost(weight: "10") +} + +input OpenIdConnectTokenSortInput { + createdAt: SortEnumType @cost(weight: "10") + expiredAt: SortEnumType @cost(weight: "10") + id: SortEnumType @cost(weight: "10") + redeemedAt: SortEnumType @cost(weight: "10") + status: SortEnumType @cost(weight: "10") + subject: SortEnumType @cost(weight: "10") + type: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input OpticalComponentSubtypePropositionInput { equalTo: OpticalComponentSubtype - in: [OpticalComponentSubtype!] + in: [OpticalComponentSubtype] notEqualTo: OpticalComponentSubtype - notIn: [OpticalComponentSubtype!] + notIn: [OpticalComponentSubtype] } input OpticalComponentTypePropositionInput { equalTo: OpticalComponentType - in: [OpticalComponentType!] + in: [OpticalComponentType] notEqualTo: OpticalComponentType - notIn: [OpticalComponentType!] + notIn: [OpticalComponentType] } input OpticalDataPropositionInput { @@ -3270,24 +4318,31 @@ input PhotovoltaicDataPropositionInput { resources: GetHttpsResourcesPropositionInput } +input PrimeSurfaceFilterInput { + equalTo: PrimeSurface @cost(weight: "10") + in: [PrimeSurface] @cost(weight: "10") + notEqualTo: PrimeSurface @cost(weight: "10") + notIn: [PrimeSurface] @cost(weight: "10") +} + input PublicationInput { abstract: String - arXiv: String + arXiv: ArXiv authors: [String!] - doi: String + doi: Doi section: String title: String - urn: String + urn: Url webAddress: Url } -input ReferenceInput { +input ReferenceInput @oneOf { publication: PublicationInput standard: StandardInput } input RegisterUserInput { - email: String! + email: EmailAddress! name: String! password: String! passwordConfirmation: String! @@ -3335,12 +4390,12 @@ input RemoveUserRoleInput { } input RequestUserPasswordResetInput { - email: String! + email: EmailAddress! returnTo: Url } input ResendUserEmailConfirmationInput { - email: String! + email: EmailAddress! } input ResetOpenIdConnectApplicationClientSecretInput { @@ -3348,7 +4403,7 @@ input ResetOpenIdConnectApplicationClientSecretInput { } input ResetUserPasswordInput { - email: String! + email: EmailAddress! password: String! passwordConfirmation: String! resetCode: String! @@ -3374,21 +4429,21 @@ input SetUserPasswordInput { } input SetUserPhoneNumberInput { - phoneNumber: String! + phoneNumber: PhoneNumber! } input ShortFilterInput { 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") } @@ -3404,41 +4459,26 @@ input StandardInput { input StringFilterInput { and: [StringFilterInput!] - or: [StringFilterInput!] - 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: [StringFilterInput!] startsWith: String @cost(weight: "20") - doesNotStartWith: String @cost(weight: "20") - endsWith: String @cost(weight: "20") - doesNotEndWith: String @cost(weight: "20") } input SwitchInstitutionOperatingStateInput { institutionId: Uuid! } -input TimeSpanFilterInput { - 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 UpdateComponentAssemblyInput { assembledComponentId: Uuid! - index: Byte + index: UnsignedByte partComponentId: Uuid! primeSurface: PrimeSurface } @@ -3457,6 +4497,13 @@ input UpdateComponentInput { switchableLayers: DescriptionOrReferenceInput } +input UpdateDatabaseInput { + databaseId: Uuid! + description: String! + locator: Url! + name: String! +} + input UpdateDataFormatInput { dataFormatId: Uuid! description: String! @@ -3467,13 +4514,6 @@ input UpdateDataFormatInput { schemaLocator: Url } -input UpdateDatabaseInput { - databaseId: Uuid! - description: String! - locator: Url! - name: String! -} - input UpdateInstitutionInput { abbreviation: String contact: ContactInformationInput @@ -3512,97 +4552,139 @@ input UpdateOpenIdConnectApplicationInput { 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 UrlFilterInput { 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 UserDevelopedMethodFilterInput { and: [UserDevelopedMethodFilterInput!] - or: [UserDevelopedMethodFilterInput!] + createdAt: DateTimeFilterInput method: InstitutionManagedMethodFilterInput + or: [UserDevelopedMethodFilterInput!] + updatedAt: DateTimeFilterInput +} + +input UserDevelopedMethodSortInput { + createdAt: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input UserFilterInput { and: [UserFilterInput!] - or: [UserFilterInput!] + createdAt: DateTimeFilterInput + developedMethodEdges: UserMethodDevelopersFilterInput + developedMethods: MethodsFilterInput id: UuidFilterInput - developedMethods: FilterInputTypeOfMethodsFilterInput - developedMethodEdges: FilterInputTypeOfUserMethodDevelopersFilterInput - representedInstitutions: FilterInputTypeOfInstitutionsFilterInput - representedInstitutionEdges: FilterInputTypeOfInstitutionRepresentativesFilterInput + "Full name" + name: StringFilterInput + or: [UserFilterInput!] + representedInstitutionEdges: InstitutionRepresentativesFilterInput + representedInstitutions: InstitutionsFilterInput + updatedAt: DateTimeFilterInput } input UserGnuPgKeyFingerprintFilterInput { + allowedAt: DateTimeFilterInput and: [UserGnuPgKeyFingerprintFilterInput!] - or: [UserGnuPgKeyFingerprintFilterInput!] - id: UuidFilterInput + createdAt: DateTimeFilterInput fingerprint: StringFilterInput - createdAt: OffsetDateTimeFilterInput - allowedAt: OffsetDateTimeFilterInput - forbiddenAt: OffsetDateTimeFilterInput + forbiddenAt: DateTimeFilterInput + id: UuidFilterInput institution: InstitutionFilterInput + or: [UserGnuPgKeyFingerprintFilterInput!] + updatedAt: DateTimeFilterInput +} + +input UserGnuPgKeyFingerprintSortInput { + allowedAt: SortEnumType @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") + fingerprint: SortEnumType @cost(weight: "10") + forbiddenAt: SortEnumType @cost(weight: "10") + id: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } -input UserMethodDeveloperSortInput { - method: MethodSortInput @cost(weight: "10") - user: UserSortInput @cost(weight: "10") +input UserMethodDevelopersFilterInput { + all: UserDevelopedMethodFilterInput @cost(weight: "10") + any: Boolean @cost(weight: "10") + none: UserDevelopedMethodFilterInput @cost(weight: "10") + some: UserDevelopedMethodFilterInput @cost(weight: "10") } input UserRepresentedInstitutionFilterInput { and: [UserRepresentedInstitutionFilterInput!] - or: [UserRepresentedInstitutionFilterInput!] + createdAt: DateTimeFilterInput institution: InstitutionFilterInput + or: [UserRepresentedInstitutionFilterInput!] role: InstitutionRepresentativeRoleFilterInput + updatedAt: DateTimeFilterInput +} + +input UserRepresentedInstitutionSortInput { + createdAt: SortEnumType @cost(weight: "10") + role: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") +} + +input UsersFilterInput { + all: UserFilterInput @cost(weight: "10") + any: Boolean @cost(weight: "10") + none: UserFilterInput @cost(weight: "10") + some: UserFilterInput @cost(weight: "10") } input UserSortInput { + createdAt: SortEnumType @cost(weight: "10") id: SortEnumType @cost(weight: "10") + "Full name" + name: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @cost(weight: "10") } input UuidFilterInput { 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") } @@ -3801,17 +4883,17 @@ enum CreateComponentErrorCode { DUPLICATE_COMPONENT_ID } -enum CreateDataFormatErrorCode { +enum CreateDatabaseErrorCode { UNKNOWN - TWO_REFERENCES - UNKNOWN_MANAGER UNAUTHORIZED + UNKNOWN_OPERATOR } -enum CreateDatabaseErrorCode { +enum CreateDataFormatErrorCode { UNKNOWN + TWO_REFERENCES + UNKNOWN_MANAGER UNAUTHORIZED - UNKNOWN_OPERATOR } enum CreateInstitutionErrorCode { @@ -3837,6 +4919,16 @@ enum CreateOpenIdConnectApplicationErrorCode { UNAUTHORIZED UNKNOWN_INSTITUTION DUPLICATE_CLIENT_ID + ILLEGAL_CONSENT_TYPE + ILLEGAL_SCOPE + ILLEGAL_GRANT_TYPE + ILLEGAL_ENDPOINT + ILLEGAL_RESPONSE_TYPE +} + +enum DatabaseVerificationState { + PENDING + VERIFIED } enum DataKind { @@ -3848,11 +4940,6 @@ enum DataKind { PHOTOVOLTAIC_DATA } -enum DatabaseVerificationState { - PENDING - VERIFIED -} - enum DeleteInstitutionErrorCode { UNKNOWN UNKNOWN_INSTITUTION @@ -3938,17 +5025,6 @@ enum InstitutionState { VERIFIED } -enum IsoDayOfWeek { - NONE - MONDAY - TUESDAY - WEDNESDAY - THURSDAY - FRIDAY - SATURDAY - SUNDAY -} - enum JsonValueKind { UNDEFINED OBJECT @@ -3994,6 +5070,9 @@ enum MethodCategory { enum OpenIdConnectConsentType { EXPLICIT + EXTERNAL + IMPLICIT + SYSTEMATIC } enum OpenIdConnectEndpoint { @@ -4028,11 +5107,14 @@ enum OpenIdConnectScope { EMAIL PHONE PROFILE + OFFLINE_ACCESS + OPEN_ID ROLES READ_API WRITE_API ADMINISTRATE_API VERIFY_API + SUPPORT_API MANAGE_DATABASE_API MANAGE_GNU_PG_API MANAGE_INSTITUTION_REPRESENTATIVE_API @@ -4041,28 +5123,30 @@ enum OpenIdConnectScope { } enum OpticalComponentSubtype { - MONOLITHIC - LAMINATE - INTERLAYER - EMBEDDED_COATING + ACID_ETCHED_GLASS + APPLIED_FILM + CELLULAR_SHADE + CHROMOGENIC COATED COATING - APPLIED_FILM - FILM - VENETIAN_BLIND DIFFUSING_SHADE - ROLLER_SHADE - WOVEN_SHADE - VERTICAL_LOUVER + EMBEDDED_COATING + FILM + FRITTED_GLASS + INTERLAYER + LAMINATE + MONOLITHIC PERFORATED_SCREEN - CELLULAR_SHADE PLEATED_SHADE + ROLLER_SHADE ROMAN_SHADE - SHADE_MATERIAL - FRITTED_GLASS - ACID_ETCHED_GLASS SANDBLASTED_GLASS - CHROMOGENIC + SHADE_MATERIAL + SOLAR_SCREEN + UNKNOWN + VENETIAN_BLIND + VERTICAL_LOUVER + WOVEN_SHADE } enum OpticalComponentType { @@ -4149,6 +5233,7 @@ enum RemoveUserMethodDeveloperErrorCode { enum RemoveUserRoleErrorCode { UNKNOWN UNKNOWN_USER + LAST_USER UNAUTHORIZED } @@ -4281,17 +5366,17 @@ enum UpdateComponentErrorCode { AMBIGUOUS_REFERENCE } -enum UpdateDataFormatErrorCode { +enum UpdateDatabaseErrorCode { UNKNOWN - TWO_REFERENCES UNAUTHORIZED - UNKNOWN_DATA_FORMAT + UNKNOWN_DATABASE } -enum UpdateDatabaseErrorCode { +enum UpdateDataFormatErrorCode { UNKNOWN + TWO_REFERENCES UNAUTHORIZED - UNKNOWN_DATABASE + UNKNOWN_DATA_FORMAT } enum UpdateInstitutionErrorCode { @@ -4311,11 +5396,17 @@ enum UpdateOpenIdConnectApplicationErrorCode { UNKNOWN UNAUTHORIZED UNKNOWN_APPLICATION + ILLEGAL_CONSENT_TYPE + ILLEGAL_GRANT_TYPE + ILLEGAL_SCOPE + ILLEGAL_RESPONSE_TYPE + ILLEGAL_ENDPOINT } enum UserRole { ADMINISTRATOR VERIFIER + SUPPORTER } enum VerifyDatabaseErrorCode { @@ -4333,63 +5424,138 @@ enum VerifyInstitutionErrorCode { UNAUTHORIZED } -"The authorize directive." -directive @authorize("Defines when when the authorize directive shall be applied.By default the authorize directives are applied during the validation phase." apply: ApplyPolicy! = BEFORE_RESOLVER "The name of the authorization policy that determines access to the annotated resource." policy: String "Roles that are allowed to access the annotated resource." roles: [String!]) repeatable on OBJECT | FIELD_DEFINITION +scalar Any @specifiedBy(url: "https://scalars.graphql.org/chillicream/any.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 - -"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 +""" +arXiv identifier, that is, a string of the format +[Understanding the arXiv identifier](https://info.arxiv.org/help/arxiv_identifier.html). +""" +scalar ArXiv -"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") +"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") -""" -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. +"The `Decimal` scalar type represents a decimal floating-point number with high precision." +scalar Decimal + @specifiedBy(url: "https://scalars.graphql.org/chillicream/decimal.html") -Example: `Europe/Zurich` """ -scalar DateTimeZone +Digital Object Identifier (DOI) name, that is, a string compliant with [ISO +26324:2025](https://www.iso.org/standard/88862.html). + +See also +[What is a DOI?](https://www.doi.org/the-identifier/what-is-a-doi/). In short, +a DOI name is a digital identifier of an object, any object — physical, +digital, or abstract. DOIs solve a common problem: keeping track of things. +Things can be matter, material, content, or activities. Designed to be used by +humans as well as machines, DOIs identify objects persistently. They allow +things to be uniquely identified and accessed reliably. You know what you have, +where it is, and others can track it too. +""" +scalar Doi -"The `Decimal` scalar type represents a decimal floating-point number." -scalar Decimal +"The `Duration` scalar type represents a duration of time." +scalar Duration + @specifiedBy(url: "https://scalars.graphql.org/chillicream/duration.html") +"The EmailAddress scalar type constitutes a valid email address, represented as a UTF-8 character sequence. The scalar follows the specification defined by the HTML Spec https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address." +scalar EmailAddress + +""" +[September 2025 GraphQL](https://spec.graphql.org/September2025/) +compliant +[query](https://spec.graphql.org/September2025/#sec-Language.Operations) +string without +[variables](http://spec.graphql.org/September2025/#sec-Language.Variables) +and without unnecessary white-space for the present schema. """ -Represents a fixed (and calendar-independent) length of time. +scalar GraphQlQuery -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" +""" +[BCP 47](https://tools.ietf.org/html/bcp47) +compliant +[Language Tag](https://tools.ietf.org/html/bcp47#section-2) +string like `"de-AT"`, `"sr-Latn-RS"`, `"en-US"`, or `"en-GB"`, where the language +part is essentially an +[ISO 639 Language Code](https://www.iso.org/iso-639-language-codes.html), +the script an +[ISO 15924:2004 Script Code](https://www.iso.org/standard/29546.html), +and the region an +[ISO 3166-1 Country Code](https://www.iso.org/iso-3166-country-codes.html). +Note that the +[Internet Assigned Numbers Authority (IANA)](https://www.iana.org) +maintains the +[Language Subtag Registry](https://www.iana.org/assignments/lang-subtags-templates/lang-subtags-templates.xhtml). +""" 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 PhoneNumber scalar type represents a value that conforms to the standard E.164 format." +scalar PhoneNumber + +"The `Short` scalar type represents a signed 16-bit integer." scalar Short + @specifiedBy(url: "https://scalars.graphql.org/chillicream/short.html") -"The `TimeSpan` scalar represents an ISO-8601 compliant duration type." -scalar TimeSpan +"The `UnsignedByte` scalar type represents an unsigned 8-bit integer." +scalar UnsignedByte + @specifiedBy( + url: "https://scalars.graphql.org/chillicream/unsigned-byte.html" + ) -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/backend/test/Integration/IntegrationTests.cs b/backend/test/Integration/IntegrationTests.cs index e076c1047..f9219d2cf 100644 --- a/backend/test/Integration/IntegrationTests.cs +++ b/backend/test/Integration/IntegrationTests.cs @@ -56,7 +56,6 @@ public void Dispose() { // Dispose of unmanaged resources. Dispose(true); - // Suppress finalization. GC.SuppressFinalize(this); } @@ -253,6 +252,17 @@ Func> task ); } + protected Task AsAdministrator( + Func> task + ) + { + return AsUser( + emailAddress: DbSeeder.AdministratorUser.EmailAddress, + password: AppSettings.BootstrapUserPassword, + task: task + ); + } + protected Task AsVerifier( Func> task ) @@ -264,12 +274,12 @@ Func> task ); } - protected Task AsAdministrator( + protected Task AsSupporter( Func> task ) { return AsUser( - emailAddress: DbSeeder.AdministratorUser.EmailAddress, + emailAddress: DbSeeder.SupporterUser.EmailAddress, password: AppSettings.BootstrapUserPassword, task: task ); @@ -465,6 +475,19 @@ await message.Content.ReadAsStringAsync() } } + protected static async Task AssertHttpBadRequest(HttpResponseMessage message) + { + if (message.StatusCode != HttpStatusCode.BadRequest) + { + // We wrap this check in an if-condition such that the message + // content is only read when the status code is not 200. + message.StatusCode.Should().Be( + HttpStatusCode.BadRequest, + await message.Content.ReadAsStringAsync() + ); + } + } + protected static async Task AssertHttpFailure(HttpResponseMessage message) { if (message.StatusCode == HttpStatusCode.OK) diff --git a/backend/test/Integration/MigrationTests.cs b/backend/test/Integration/MigrationTests.cs index f576aa4e9..f41d02a2b 100644 --- a/backend/test/Integration/MigrationTests.cs +++ b/backend/test/Integration/MigrationTests.cs @@ -14,7 +14,14 @@ namespace Metabase.Tests.Integration; public sealed class MigrationTests : IntegrationTests { - // Inspired by https://www.meziantou.net/detect-missing-migrations-in-entity-framework-core.htm + /// + /// Compare the database snapshot `ApplicationDbContextModelSnapshot.cs` + /// with the database schema wanted by `ApplicationDbContext`. If they are + /// out-of-sync, then `make migration NAME=...` creates a migration and + /// updates the snapshot. + /// + /// Inspired by https://www.meziantou.net/detect-missing-migrations-in-entity-framework-core.htm + /// [Test] public Task EnsureMigrationsAreUpToDate() { @@ -44,7 +51,10 @@ public Task EnsureMigrationsAreUpToDate() source: snapshotModel?.GetRelationalModel(), target: model.GetRelationalModel()); // The differences should be empty if the migrations are up-to-date - modelDifferences.Should().BeEquivalentTo(Enumerable.Empty()); // .BeEmpty(); + modelDifferences.Should().BeEquivalentTo( + Enumerable.Empty(), + because: "you forgot to add a migration with `make migration NAME='...' in the shell `./docker.mk shell SERVICE=backend`." + ); }); } } \ No newline at end of file diff --git a/backend/test/Metabase.Tests.csproj b/backend/test/Metabase.Tests.csproj index 6f8bdd3dd..04587fd72 100644 --- a/backend/test/Metabase.Tests.csproj +++ b/backend/test/Metabase.Tests.csproj @@ -9,21 +9,28 @@
- + + - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/database.mk b/database.mk index f1b5f4d9a..3cdc00759 100755 --- a/database.mk +++ b/database.mk @@ -13,7 +13,7 @@ dump_archive_name = postgresql_dumpall.gz # Taken from https://www.client9.com/self-documenting-makefiles/ help : ## Print this help - @awk -F ':|##' '/^[^\t].+?:.*?##/ {\ + @awk -F ':.*?## ' '/^[^\t].+?:.*?##/ {\ printf "\033[36m%-30s\033[0m %s\n", $$1, $$NF \ }' $(MAKEFILE_LIST) .PHONY : help @@ -22,7 +22,6 @@ help : ## Print this help psql : ## Enter PostgreSQL interactive terminal in the `database` container docker compose up \ --no-build \ - --no-deps \ --no-recreate \ --wait \ database @@ -41,7 +40,6 @@ remove-volume : ## Remove data volume create : ## Create database with name `${POSTGRES_DATABASE_NAME}` docker compose up \ --no-build \ - --no-deps \ --no-recreate \ --wait \ database @@ -56,7 +54,6 @@ create : ## Create database with name `${POSTGRES_DATABASE_NAME}` drop : ## Drop database with name `${POSTGRES_DATABASE_NAME}` docker compose up \ --no-build \ - --no-deps \ --no-recreate \ --wait \ database @@ -71,7 +68,6 @@ drop : ## Drop database with name `${POSTGRES_DATABASE_NAME}` sql : ## Run the SQL script in the file `${SCRIPT}` in the database service, for example, `make sql SCRIPT=./my.sql ` (note that after database schema changes it is necessary to restart the backend service for the object-relational mapper Npgsql to work seamlessly, for example, by restarting the backend service with `./docker.mk restart SERVICE=backend`) docker compose up \ --no-build \ - --no-deps \ --no-recreate \ --wait \ database @@ -89,7 +85,7 @@ sql : ## Run the SQL script in the file `${SCRIPT}` in the database service, for .PHONY : sql migrate : SCRIPT = ./backend/src/Migrations/migrate.sql -migrate : ## Migrate database by running the idempotent SQL script ./backend/src/Migrations/migrate.sql +migrate : ## Migrate database by running the idempotent SQL script ./backend/src/Migrations/migrate.sql $(MAKE) --file="${SELF}" sql SCRIPT="${SCRIPT}" docker compose restart \ --no-deps \ @@ -103,7 +99,6 @@ backup : ## Backup database and related data to directory with absolute path `${ mkdir --parents "${DIR}" docker compose up \ --no-build \ - --no-deps \ --no-recreate \ --wait \ database @@ -125,7 +120,6 @@ restore : ## Restore database and related data from directory with absolute path backend docker compose up \ --no-build \ - --no-deps \ --no-recreate \ --wait \ database diff --git a/deploy.mk b/deploy.mk index 2d7c1bba4..116571931 100755 --- a/deploy.mk +++ b/deploy.mk @@ -16,18 +16,18 @@ dotenv_linter = \ --volume "$(shell pwd):/mnt:ro" \ --pull "always" \ --quiet \ - dotenvlinter/dotenv-linter:latest + dotenvlinter/dotenv-linter:4.0.0 # Taken from https://www.client9.com/self-documenting-makefiles/ help : ## Print this help - @awk -F ':|##' '/^[^\t].+?:.*?##/ {\ + @awk -F ':.*?## ' '/^[^\t].+?:.*?##/ {\ printf "\033[36m%-30s\033[0m %s\n", $$1, $$NF \ }' $(MAKEFILE_LIST) .PHONY : help .DEFAULT_GOAL := help do : ON_ERROR = pause -do : symlink ## Deploy tag, branch, or commit `${TARGET}`, for example, `./deploy.mk do TARGET=v1.0.0 ON_ERROR=pause` +do : symlink ## Deploy tag or commit `${TARGET}`, for example, `./deploy.mk do TARGET=v1.0.0 ON_ERROR=pause` ./deploy.sh target "${TARGET}" --on-error "${ON_ERROR}" .PHONY : do @@ -96,7 +96,6 @@ dotenv : ## Assert that all variables in ./.env.${ENVIRONMENT}.sample are availa services : ## Recreate services docker compose up \ --no-build \ - --no-deps \ --force-recreate \ --renew-anon-volumes \ --remove-orphans \ @@ -117,7 +116,7 @@ restart : ## Restart service `${SERVICE}` and await its health symlink : ## Confirm that ./Makefile links to ./docker.mk and that ./docker-compose.yaml links to the correct ./docker-compose.*.yaml if [[ ! -L "./Makefile" ]] || [[ ! "./Makefile" -ef "./docker.mk" ]]; then \ - echo "./docker-compose.yaml does not link to $${file}" >&2 ; \ + echo "./Makefile does not link to ./docker.mk" >&2 ; \ exit 1 ; \ fi if [[ "${ENVIRONMENT}" == "staging" ]]; then \ diff --git a/docker-compose.development.yaml b/docker-compose.development.yaml index a0459aeb0..90f7bfd60 100644 --- a/docker-compose.development.yaml +++ b/docker-compose.development.yaml @@ -2,11 +2,30 @@ name: "${NAME:?required}_${ENVIRONMENT:?required}" services: + # Upgrade PostgreSQL major version if necessary + # https://hub.docker.com/r/pgautoupgrade/pgautoupgrade#one-shot-mode + upgrade_postgres: + # Keep major version in sync with the one in the service `database` + image: "pgautoupgrade/pgautoupgrade:18-trixie" + volumes: + - data:/var/lib/postgresql + environment: + PGAUTO_ONESHOT: "yes" + POSTGRES_USER: "${POSTGRES_USER:?required}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:?required}" + network_mode: "none" + restart: "no" + pull_policy: "missing" + database: - # Keep version in sync with the one in `Startup.cs` - image: "postgres:13.23-trixie" + # Keep version in sync with the one in in the service `upgrade_postgres` + # and with the one in `Startup.cs` + image: "postgres:18.4-trixie" + depends_on: + upgrade_postgres: + condition: "service_completed_successfully" volumes: - - data:/var/lib/postgresql/data + - data:/var/lib/postgresql - ./.psqlrc:/etc/postgresql-common/psqlrc:ro environment: POSTGRES_USER: "${POSTGRES_USER:?required}" @@ -38,7 +57,7 @@ services: start_interval: "5s" labels: autoheal: "true" - pull_policy: "always" + pull_policy: "missing" email: image: "rnwood/smtp4dev:v3.10" @@ -75,7 +94,7 @@ services: start_interval: "5s" labels: autoheal: "true" - pull_policy: "always" + pull_policy: "missing" backend: build: @@ -117,6 +136,7 @@ services: XBASE_Subdomain: "${SUBDOMAIN:?required}" XBASE_BootstrapUserPassword: "${BOOTSTRAP_USER_PASSWORD:?required}" XBASE_OpenIdConnectClientSecret: "${OPEN_ID_CONNECT_CLIENT_SECRET:?required}" + XBASE_EmailAddress: "${EMAIL_ADDRESS:?required}" XBASE_Database__Host: "database" XBASE_Database__Port: 5432 XBASE_Database__Name: "${POSTGRES_DATABASE_NAME:?required}" @@ -129,6 +149,9 @@ services: XBASE_TestlabSolarFacades__OpenIdConnectClientSecret: "${TESTLAB_SOLAR_FACADES_OPEN_ID_CONNECT_CLIENT_SECRET:?required}" XBASE_Igsdb__ApiToken: "${IGSDB_API_TOKEN:?required}" XBASE_Igsdb__OpenIdConnectClientSecret: "${IGSDB_OPEN_ID_CONNECT_CLIENT_SECRET:?required}" + XBASE_Epea__UserId: "${EPEA_USER_ID:?required}" + XBASE_Epea__ApiKey: "${EPEA_API_KEY:?required}" + XBASE_Epea__OpenIdConnectClientSecret: "${EPEA_OPEN_ID_CONNECT_CLIENT_SECRET:?required}" XBASE_OpenTelemetry__Host: "${OPEN_TELEMETRY_HOST:?required}" XBASE_OpenTelemetry__GrpcPort: "${OPEN_TELEMETRY_GRPC_PORT:?required}" # https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-watch#environment-variables @@ -225,7 +248,7 @@ services: pull_policy: "never" reverse_proxy: - image: "nginx:1.29-trixie-otel" + image: "nginx:1.31-trixie-otel" depends_on: backend: condition: "service_started" @@ -256,7 +279,7 @@ services: start_interval: "5s" labels: autoheal: "true" - pull_policy: "always" + pull_policy: "missing" volumes: data: diff --git a/docker-compose.production.yaml b/docker-compose.production.yaml index cddd9c04f..11bce5417 100644 --- a/docker-compose.production.yaml +++ b/docker-compose.production.yaml @@ -2,11 +2,30 @@ name: "${NAME:?required}_${ENVIRONMENT:?required}" services: + # Upgrade PostgreSQL major version if necessary + # https://hub.docker.com/r/pgautoupgrade/pgautoupgrade#one-shot-mode + upgrade_postgres: + # Keep major version in sync with the one in the service `database` + image: "pgautoupgrade/pgautoupgrade:18-trixie" + volumes: + - data:/var/lib/postgresql + environment: + PGAUTO_ONESHOT: "yes" + POSTGRES_USER: "${POSTGRES_USER:?required}" + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:?required}" + network_mode: "none" + restart: "no" + pull_policy: "always" + database: - # Keep version in sync with the one in `Startup.cs` - image: "postgres:13.23-trixie" + # Keep version in sync with the one in in the service `upgrade_postgres` + # and with the one in `Startup.cs` + image: "postgres:18.4-trixie" + depends_on: + upgrade_postgres: + condition: "service_completed_successfully" volumes: - - data:/var/lib/postgresql/data + - data:/var/lib/postgresql - ./.psqlrc:/etc/postgresql-common/psqlrc:ro environment: POSTGRES_USER: "${POSTGRES_USER:?required}" @@ -102,6 +121,7 @@ services: XBASE_Subdomain: "${SUBDOMAIN:?required}" XBASE_BootstrapUserPassword: "${BOOTSTRAP_USER_PASSWORD:?required}" XBASE_OpenIdConnectClientSecret: "${OPEN_ID_CONNECT_CLIENT_SECRET:?required}" + XBASE_EmailAddress: "${EMAIL_ADDRESS:?required}" XBASE_Database__Host: "database" XBASE_Database__Port: 5432 XBASE_Database__Name: "${POSTGRES_DATABASE_NAME:?required}" @@ -114,6 +134,9 @@ services: XBASE_TestlabSolarFacades__OpenIdConnectClientSecret: "${TESTLAB_SOLAR_FACADES_OPEN_ID_CONNECT_CLIENT_SECRET:?required}" XBASE_Igsdb__ApiToken: "${IGSDB_API_TOKEN:?required}" XBASE_Igsdb__OpenIdConnectClientSecret: "${IGSDB_OPEN_ID_CONNECT_CLIENT_SECRET:?required}" + XBASE_Epea__UserId: "${EPEA_USER_ID:?required}" + XBASE_Epea__ApiKey: "${EPEA_API_KEY:?required}" + XBASE_Epea__OpenIdConnectClientSecret: "${EPEA_OPEN_ID_CONNECT_CLIENT_SECRET:?required}" XBASE_OpenTelemetry__Host: "${OPEN_TELEMETRY_HOST:?required}" XBASE_OpenTelemetry__GrpcPort: "${OPEN_TELEMETRY_GRPC_PORT:?required}" # The value `/dev/pts/0` is the output of running `tty` within `shellb` @@ -180,7 +203,7 @@ services: pull_policy: "never" reverse_proxy: - image: "nginx:1.29-trixie-otel" + image: "nginx:1.31-trixie-otel" depends_on: backend: condition: "service_started" diff --git a/docker.mk b/docker.mk index 94643d4ba..ccdf1ee89 100755 --- a/docker.mk +++ b/docker.mk @@ -20,7 +20,7 @@ dotenv_linter = \ # Taken from https://www.client9.com/self-documenting-makefiles/ help : ## Print this help - @awk -F ':|##' '/^[^\t].+?:.*?##/ {\ + @awk -F ':.*?## ' '/^[^\t].+?:.*?##/ {\ printf "\033[36m%-30s\033[0m %s\n", $$1, $$NF \ }' $(MAKEFILE_LIST) .PHONY : help @@ -32,7 +32,7 @@ environment : ## Print value of variable `ENVIRONMENT` symlink : ## Confirm that ./Makefile links to ./docker.mk and that ./docker-compose.yaml links to the correct ./docker-compose.*.yaml if [[ ! -L "./Makefile" ]] || [[ ! "./Makefile" -ef "./docker.mk" ]]; then \ - echo "./docker-compose.yaml does not link to $${file}" >&2 ; \ + echo "./Makefile does not link to ./docker.mk" >&2 ; \ exit 1 ; \ fi if [[ "${ENVIRONMENT}" == "staging" ]]; then \ @@ -89,7 +89,6 @@ remove : ## Remove stopped services up : symlink dotenv ## (Re)create and start services docker compose up \ --no-build \ - --no-deps \ --no-recreate \ --remove-orphans \ --wait ${SERVICE} @@ -133,7 +132,6 @@ logs : ## Follow logs exec : ## Execute the one-time command `${COMMAND}` against the `${SERVICE}` service docker compose up \ --no-build \ - --no-deps \ --no-recreate \ --wait \ ${SERVICE} diff --git a/forge.mk b/forge.mk index f68bf0bc3..d11bb0128 100755 --- a/forge.mk +++ b/forge.mk @@ -10,7 +10,7 @@ TARGET = $(shell git rev-parse --verify HEAD) # Taken from https://www.client9.com/self-documenting-makefiles/ help : ## Print this help - @awk -F ':|##' '/^[^\t].+?:.*?##/ {\ + @awk -F ':.*?## ' '/^[^\t].+?:.*?##/ {\ printf "\033[36m%-30s\033[0m %s\n", $$1, $$NF \ }' $(MAKEFILE_LIST) .PHONY : help diff --git a/frontend/Dockerfile.development b/frontend/Dockerfile.development index 96ee81553..be4450434 100644 --- a/frontend/Dockerfile.development +++ b/frontend/Dockerfile.development @@ -1,11 +1,11 @@ -# syntax=docker/dockerfile:1.21 +# syntax=docker/dockerfile:1.23 # check=error=true # Available versions are listed on https://hub.docker.com/r/docker/dockerfile # Inspired by https://mherman.org/blog/dockerizing-a-react-app/ # https://hub.docker.com/_/node -FROM node:24.11-trixie-slim +FROM node:24.16-trixie-slim SHELL ["/bin/bash", "-o", "errexit", "-o", "nounset", "-o", "pipefail", "-c"] ############# @@ -16,21 +16,21 @@ SHELL ["/bin/bash", "-o", "errexit", "-o", "nounset", "-o", "pipefail", "-c"] # https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run RUN apt-get update \ && apt-get install \ - --assume-yes \ - --no-install-recommends \ - make \ - tini \ - wget \ + --assume-yes \ + --no-install-recommends \ + make \ + tini \ + wget \ && rm \ - --recursive \ - --force \ - /var/lib/apt/lists/* + --recursive \ + --force \ + /var/lib/apt/lists/* ENV HOME=/home/node RUN mkdir --parents ${HOME}/app \ && chown \ - node:node \ - ${HOME}/app \ + node:node \ + ${HOME}/app \ && ln --symbolic ${HOME}/app /app ############# diff --git a/frontend/Dockerfile.production b/frontend/Dockerfile.production index 056def371..791074429 100644 --- a/frontend/Dockerfile.production +++ b/frontend/Dockerfile.production @@ -1,11 +1,11 @@ -# syntax=docker/dockerfile:1.21 +# syntax=docker/dockerfile:1.23 # check=error=true # Available versions are listed on https://hub.docker.com/r/docker/dockerfile # Inspired by https://github.com/vercel/next.js/blob/canary/examples/with-docker-multi-env/docker/production/Dockerfile # linked to in https://nextjs.org/docs/deployment -FROM node:24.11-trixie-slim AS base +FROM node:24.16-trixie-slim AS base FROM base AS dependencies SHELL ["/bin/bash", "-o", "errexit", "-o", "nounset", "-o", "pipefail", "-c"] @@ -29,13 +29,13 @@ RUN addgroup --system --gid 1001 nodejs \ RUN apt-get update \ && apt-get upgrade --assume-yes \ && apt-get install \ - --assume-yes \ - --no-install-recommends \ - wget \ + --assume-yes \ + --no-install-recommends \ + wget \ && rm \ - --recursive \ - --force \ - /var/lib/apt/lists/* + --recursive \ + --force \ + /var/lib/apt/lists/* COPY --from=build --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=build --chown=nextjs:nodejs /app/public ./public COPY --from=build --chown=nextjs:nodejs /app/.next/static ./.next/static diff --git a/frontend/Makefile b/frontend/Makefile index dc30d3ac6..9cb9286db 100644 --- a/frontend/Makefile +++ b/frontend/Makefile @@ -7,7 +7,7 @@ MAKEFLAGS += --warn-undefined-variables # Taken from https://www.client9.com/self-documenting-makefiles/ help : ## Print this help - @awk -F ':|##' '/^[^\t].+?:.*?##/ {\ + @awk -F ':.*?## ' '/^[^\t].+?:.*?##/ {\ printf "\033[36m%-30s\033[0m %s\n", $$1, $$NF \ }' $(MAKEFILE_LIST) .PHONY : help @@ -41,10 +41,6 @@ outdated : ## Show outdated packages yarn outdated .PHONY : outdated -lint : ## Lint code - next lint -.PHONY : lint - format : ## Format the code yarn run \ prettier --write . diff --git a/frontend/codegen.ts b/frontend/codegen.ts index 9e23c42de..3271a109a 100644 --- a/frontend/codegen.ts +++ b/frontend/codegen.ts @@ -1,5 +1,53 @@ import type { CodegenConfig } from "@graphql-codegen/cli"; +const scalars = { + ID: { + input: "string", + output: "string", + }, + Any: "object", + ArXiv: "string", + Byte: "number", + DateTime: "string", + DateTimeZone: "string", + Decimal: "string", + Doi: "string", + Duration: "string", + EmailAddress: "string", + GraphQlQuery: "string", + LocalDate: "string", + LocalDateTime: "string", + LocalTime: "string", + Locale: "string", + Long: "number", + NonNegativeInt: "number", + PhoneNumber: "string", + Short: "number", + TimeSpan: "string", + UnsignedByte: "number", + Upload: "unknown", + Url: "string", + Uuid: "string", +}; + +// 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", + // ... + strictScalars: true, + scalars: scalars, + useTypeImports: true, + // immutableTypes: true, + // nullability: true, +}; + const config: CodegenConfig = { overwrite: true, schema: "./type-defs.graphqls", @@ -10,9 +58,11 @@ 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 @@ -20,44 +70,12 @@ const config: CodegenConfig = { }, 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", + ...graphQlConfig, // 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, }, }, "./__generated__/fragmentMatcherTypes.ts": { @@ -69,6 +87,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 000000000..239f5ff1b --- /dev/null +++ b/frontend/components/ActiveFilterAndSortBar.tsx @@ -0,0 +1,131 @@ +import { Tag, Space, Typography, Flex } from "antd"; +import { + ObjectFilterState, + getFilterOperatorLabel, + createFilterStateReducer, + FilterDefinition, + AnyFilterDefinition, +} from "../lib/filter"; +import { formatSortDirection, SortState, SortDefinition } from "../lib/sort"; +import DeleteButton from "./DeleteButton"; +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[], +): string => + createFilterStateReducer( + (scalar) => + `${getFilterOperatorLabel(scalar.operator)}|${JSON.stringify(scalar.value)}`, + (list, _, stringifyValue) => + `${getFilterOperatorLabel(list.operator)}|${stringifyValue()}`, + (object, context, stringifyValue) => + `${String(context.items[object.index].field)}|${stringifyValue()}`, + )(filter, definitions as AnyFilterDefinition[]); + +const renderFilter = ( + value: ObjectFilterState, + definitions: readonly FilterDefinition[], +) => + createFilterStateReducer( + (scalar) => ( + <> + {getFilterOperatorLabel(scalar.operator)}{" "} + {Array.isArray(scalar.value) + ? `{${scalar.value.join(", ")}}` + : scalar.value} + + ), + (list, _, renderValue) => ( + <> + {getFilterOperatorLabel(list.operator)} {renderValue()} + + ), + (object, context, renderValue) => ( + <> + {getLabel(context.items[object.index], "none-upper")} {renderValue()} + + ), + )(value, definitions as AnyFilterDefinition[]); + +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 000000000..8dc5fbc40 --- /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 000000000..614fed5b8 --- /dev/null +++ b/frontend/components/CodeView.tsx @@ -0,0 +1,35 @@ +import { CSSProperties } from "react"; +import Copyable from "./Copyable"; +import CopyableBlock from "./CopyableBlock"; + +export default function CodeView({ + code, + inline = false, + style, +}: { + code: string; + inline?: boolean; + style?: CSSProperties; +}) { + if (inline) { + return ( + + {code} + + ); + } else { + return ( + +
+          
+            {code}
+          
+        
+
+ ); + } +} diff --git a/frontend/components/ConfirmButton.tsx b/frontend/components/ConfirmButton.tsx new file mode 100644 index 000000000..8244964ea --- /dev/null +++ b/frontend/components/ConfirmButton.tsx @@ -0,0 +1,34 @@ +import { CheckOutlined } from "@ant-design/icons"; +import { Button, ButtonProps, Tooltip } from "antd"; + +interface ConfirmButtonProps extends Omit< + ButtonProps, + "type" | "icon" | "shape" | "children" +> { + type?: "primary" | "default" | "icon"; +} + +export default function ConfirmButton({ + type = "default", + ...rest +}: ConfirmButtonProps) { + switch (type) { + case "icon": + return ( + + + ); + } +} diff --git a/frontend/components/ContactInformation.tsx b/frontend/components/ContactInformation.tsx new file mode 100644 index 000000000..fc7c87854 --- /dev/null +++ b/frontend/components/ContactInformation.tsx @@ -0,0 +1,95 @@ +import { Space, Typography } from "antd"; +import { + MailOutlined, + PhoneOutlined, + GlobalOutlined, + EnvironmentOutlined, +} from "@ant-design/icons"; +import { ContactInformationPartialFragment } from "../queries/common.generated"; + +export const phoneNumberFormInput = { + placeholder: "[+][Country Code][Local Area Code][Local Phone Number]", + extra: ( + + in the format{" "} + [+][Country Code][Local Area Code][Local Phone Number] as + standardized in{" "} + + E.164 + + . For example, (510) 486-4000 in the standard local format of + the United States becomes +15104864000 in E.164 format,{" "} + 02 313 99 44 in Belgium becomes +3223139944, and{" "} + 0761 4588-0 in Germany becomes +4976145880. See + also{" "} + + Country Codes + {" "} + and{" "} + + Formatting International Phone Numbers + + . + + ), +}; + +export default function ContactInformation({ + contact, +}: { + contact: ContactInformationPartialFragment | null; +}) { + const hasContact = + contact?.emailAddress || + contact?.phoneNumber || + contact?.websiteLocator || + contact?.postalAddress; + + if (!hasContact) { + return <>; + // return ; + } + + return ( + + {contact?.emailAddress && ( +
+ + + {contact.emailAddress} + +
+ )} + {contact?.phoneNumber && ( +
+ + + {contact.phoneNumber} + +
+ )} + {contact?.websiteLocator && ( +
+ + + {contact.websiteLocator} + +
+ )} + {contact?.postalAddress && ( +
+ + {contact.postalAddress} +
+ )} +
+ ); +} diff --git a/frontend/components/CopyButton.tsx b/frontend/components/CopyButton.tsx new file mode 100644 index 000000000..952d734e8 --- /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 000000000..b97f82dc7 --- /dev/null +++ b/frontend/components/Copyable.tsx @@ -0,0 +1,30 @@ +import { Space } from "antd"; +import { CSSProperties, ReactNode } from "react"; +import CopyButton from "./CopyButton"; + +export default function Copyable({ + text, + onlyIcon, + color, + style, + children, +}: { + text: string; + onlyIcon?: boolean; + color?: "white"; + style?: CSSProperties; + 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 000000000..c1dd917ff --- /dev/null +++ b/frontend/components/CopyableBlock.tsx @@ -0,0 +1,56 @@ +import { Button } from "antd"; +import { CheckOutlined, CopyOutlined } from "@ant-design/icons"; +import { CSSProperties, ReactNode, useState } from "react"; + +export default function CopyableBlock({ + text, + color, + style, + children, +}: { + text: string; + color?: "white"; + style?: CSSProperties; + 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 000000000..5e3a7af9d --- /dev/null +++ b/frontend/components/DateTimeX.tsx @@ -0,0 +1,38 @@ +import { Calendar, Popover } from "antd"; +import dayjs from "dayjs"; +import { Scalars } from "../__generated__/graphql"; + +type DateTimeProps = + | { + value: Scalars["DateTime"]["output"]; + } + | { + parsedValue: dayjs.Dayjs; + }; + +export default function DateTimeX(props: DateTimeProps) { + const parsedValue = "value" in props ? dayjs(props.value) : props.parsedValue; + + 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 000000000..d38c06d0e --- /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/DescriptionOrReference.tsx b/frontend/components/DescriptionOrReference.tsx new file mode 100644 index 000000000..b49d2ce05 --- /dev/null +++ b/frontend/components/DescriptionOrReference.tsx @@ -0,0 +1,28 @@ +import { Typography } from "antd"; +import { DescriptionOrReferencePartialFragment } from "../queries/common.generated"; +import Reference from "./Reference"; + +interface ReferenceProps { + title: string; + data: DescriptionOrReferencePartialFragment; +} + +export default function DescriptionOrReference({ + title, + data, +}: ReferenceProps) { + if (!data.description && !data.reference) { + return null; + } + return ( +
+ {title} + {data.description && ( + + {data.description} + + )} + {data.reference && } +
+ ); +} diff --git a/frontend/components/EditButton.tsx b/frontend/components/EditButton.tsx new file mode 100644 index 000000000..21a085b86 --- /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 000000000..6c6476c77 --- /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 000000000..5cb078379 --- /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/ErrorAlert.tsx b/frontend/components/ErrorAlert.tsx index 23537461e..d0c05dcad 100644 --- a/frontend/components/ErrorAlert.tsx +++ b/frontend/components/ErrorAlert.tsx @@ -16,6 +16,7 @@ export default function ErrorAlert({ messages }: { messages: string[] }) { ) } + style={{ marginBottom: "1em" }} /> ) : null; } diff --git a/frontend/components/Float.tsx b/frontend/components/Float.tsx new file mode 100644 index 000000000..5b11ac080 --- /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/FloatPropositionFormList.tsx b/frontend/components/FloatPropositionFormList.tsx deleted file mode 100644 index 480da3e38..000000000 --- a/frontend/components/FloatPropositionFormList.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { Form, Select, InputNumber, Button, Space } from "antd"; -import { PlusOutlined } from "@ant-design/icons"; - -const tailLayout = { - wrapperCol: { offset: 8, span: 16 }, -}; - -enum Negator { - Is = "is", - IsNot = "isNot", -} - -export enum FloatPropositionComparator { - EqualTo = "equalTo", - LessThanOrEqualTo = "lessThanOrEqualTo", - GreaterThanOrEqualTo = "greaterThanOrEqualTo", - // InClosedInterval = "inClosedInterval" -} - -interface FloatPropositionFormListProps { - name: string; - label: string; - minimum: number; - maximum: number; -}; - -export function FloatPropositionFormList({ - name, - label, - minimum, - maximum, -}: FloatPropositionFormListProps) { - return ( - - {(fields, { add, remove }, { errors }) => ( - <> - {fields.map(({ key, name, ...restField }, index) => ( - - - - - - - - - - - - ))} - - - - - - )} - - ); -} diff --git a/frontend/components/Footer.tsx b/frontend/components/Footer.tsx index ab4ce83a4..8505147a3 100644 --- a/frontend/components/Footer.tsx +++ b/frontend/components/Footer.tsx @@ -1,14 +1,13 @@ -import { Typography } from "antd"; +import { Space, Typography } from "antd"; import paths from "../paths"; 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 000000000..485431af8 --- /dev/null +++ b/frontend/components/GnuPgKeyLink.tsx @@ -0,0 +1,26 @@ +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 b80fe130e..000000000 --- a/frontend/components/Highlight.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { forwardRef } from "react"; -import Highlighter from "react-highlight-words"; - -interface 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 000000000..11e394fc6 --- /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 000000000..452ac3ff8 --- /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 000000000..526d8a8b8 --- /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 000000000..e6542433b --- /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 000000000..eed97db8d --- /dev/null +++ b/frontend/components/JsonView.tsx @@ -0,0 +1,39 @@ +import { Tooltip } from "antd"; +import CopyableBlock from "./CopyableBlock"; +import { CSSProperties } from "react"; + +export default function JsonView({ + data, + inline = false, + color, + style, +}: { + data: object; + inline?: boolean; + color?: "white"; + style?: CSSProperties; +}) { + 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 000000000..0e7d2fc94 --- /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 jump = (id: string | undefined) => { + 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 aedbfe195..b14427283 100644 --- a/frontend/components/Layout.tsx +++ b/frontend/components/Layout.tsx @@ -1,12 +1,12 @@ import Head from "next/head"; import { ReactNode, useEffect } from "react"; import Footer from "./Footer"; -import NavBar from "./NavBar"; -import { Layout as AntLayout, App, Typography } from "antd"; +import NavBar, { NavItemProps } from "./NavBar"; +import { Layout as AntLayout, App, Divider, Flex, Typography } from "antd"; import paths from "../paths"; import { useCookies } from "react-cookie"; -const navItems = [ +const navItems: NavItemProps[] = [ { path: paths.home, label: "Home", @@ -16,27 +16,27 @@ const navItems = [ label: "Data", subitems: [ { - 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", }, ], @@ -74,13 +74,19 @@ const navItems = [ ]; interface LayoutProps { + pageTitles?: string[]; + items?: NavItemProps[]; children?: ReactNode; -}; +} const cookieConsentName = "consent"; const cookieConsentValue = "yes"; -export default function Layout({ children }: LayoutProps) { +export default function Layout({ + pageTitles = [], + items = navItems, + children, +}: LayoutProps) { const appTitle = "Building Envelope Data"; const [cookies, setCookie] = useCookies([cookieConsentName]); @@ -93,7 +99,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. @@ -110,18 +116,36 @@ export default function Layout({ children }: LayoutProps) { return ( - {appTitle} + {[...pageTitles, appTitle].join(" • ")} - + + + - - {children} + + +
{children}
+
-