From 2f4b718ddc1bdf220cd6f6d70bbf806741580c5f Mon Sep 17 00:00:00 2001 From: Simon Wacker Date: Sat, 28 Mar 2026 00:15:59 +0100 Subject: [PATCH 001/145] Clean-up institution page --- ...titutionManufacturedComponentConnection.cs | 1 - frontend/components/ContactInformation.tsx | 62 ++ frontend/components/CopyableText.tsx | 29 + frontend/components/JsonViewer.tsx | 13 + frontend/components/PageHeader.tsx | 80 +++ frontend/components/components/Component.tsx | 5 +- .../components/institutions/Institution.tsx | 575 ++++++++++-------- .../institutions/InstitutionTable.tsx | 55 ++ .../RemoveInstitutionRepresentative.tsx | 24 +- frontend/components/methods/MethodTable.tsx | 7 +- frontend/lib/array.ts | 6 + frontend/package.json | 1 - frontend/pages/institutions/index.tsx | 45 +- frontend/queries/institutions.graphql | 125 +--- frontend/yarn.lock | 176 +----- 15 files changed, 615 insertions(+), 589 deletions(-) create mode 100644 frontend/components/ContactInformation.tsx create mode 100644 frontend/components/CopyableText.tsx create mode 100644 frontend/components/JsonViewer.tsx create mode 100644 frontend/components/PageHeader.tsx create mode 100644 frontend/components/institutions/InstitutionTable.tsx diff --git a/backend/src/GraphQl/Institutions/InstitutionManufacturedComponentConnection.cs b/backend/src/GraphQl/Institutions/InstitutionManufacturedComponentConnection.cs index e1e60a1bf..0585366d8 100644 --- a/backend/src/GraphQl/Institutions/InstitutionManufacturedComponentConnection.cs +++ b/backend/src/GraphQl/Institutions/InstitutionManufacturedComponentConnection.cs @@ -5,7 +5,6 @@ using Metabase.Authorization; using Metabase.Data; using Metabase.GraphQl.Users; -using Microsoft.AspNetCore.Identity; namespace Metabase.GraphQl.Institutions; diff --git a/frontend/components/ContactInformation.tsx b/frontend/components/ContactInformation.tsx new file mode 100644 index 000000000..cb6d0fd32 --- /dev/null +++ b/frontend/components/ContactInformation.tsx @@ -0,0 +1,62 @@ +import { Space, Typography } from "antd"; +import { + MailOutlined, + PhoneOutlined, + GlobalOutlined, + EnvironmentOutlined, +} from "@ant-design/icons"; +import { ContactInformationPartialFragment } from "../queries/institutions.generated"; + +export default function ContactCard({ + contact, +}: { + contact: ContactInformationPartialFragment | null; +}) { + const hasContact = + contact?.phoneNumber || + contact?.emailAddress || + contact?.postalAddress || + contact?.websiteLocator; + + 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/CopyableText.tsx b/frontend/components/CopyableText.tsx new file mode 100644 index 000000000..17045ba44 --- /dev/null +++ b/frontend/components/CopyableText.tsx @@ -0,0 +1,29 @@ +import { Button, Space } from "antd"; +import { CopyOutlined } from "@ant-design/icons"; +import { useState } from "react"; + +export default function CopyableText({ text }: { text: string }) { + const [copied, setCopied] = useState(false); + + return ( + + {text} + + + ); +} diff --git a/frontend/components/JsonViewer.tsx b/frontend/components/JsonViewer.tsx new file mode 100644 index 000000000..56680d637 --- /dev/null +++ b/frontend/components/JsonViewer.tsx @@ -0,0 +1,13 @@ +export default function JsonViewer({ jsonData }: { jsonData: any }) { + return ( +
+      
+        {JSON.stringify(jsonData, null, 2)}
+      
+    
+ ); +} diff --git a/frontend/components/PageHeader.tsx b/frontend/components/PageHeader.tsx new file mode 100644 index 000000000..9fea739bf --- /dev/null +++ b/frontend/components/PageHeader.tsx @@ -0,0 +1,80 @@ +import { Breadcrumb, Button, Space, Typography } from "antd"; +import { ArrowLeftOutlined } from "@ant-design/icons"; +import { Scalars } from "../__generated__/graphql"; +import CopyableText from "./CopyableText"; + +const { Title, Text } = Typography; + +interface Props { + id?: Scalars["Uuid"]["output"]; + title: string; + subTitle?: string; + tags?: React.ReactNode[]; + onBack?: () => void; + extra?: React.ReactNode; + breadcrumb?: { title: string; href?: string }[]; + children?: React.ReactNode; +} + +export default function PageHeader({ + id, + title, + subTitle, + tags, + onBack, + extra, + breadcrumb, + children, +}: Props) { + return ( + <> + {breadcrumb && ( + ({ + title: item.title, + href: item.href, + }))} + style={{ marginBottom: 12 }} + /> + )} + +
+ {onBack && ( +
+ + {children &&
{children}
} + + ); +} diff --git a/frontend/components/components/Component.tsx b/frontend/components/components/Component.tsx index 0032f256c..a4c481125 100644 --- a/frontend/components/components/Component.tsx +++ b/frontend/components/components/Component.tsx @@ -29,10 +29,11 @@ import { RemoveComponentAssembly } from "./RemoveComponentAssembly"; import { RemoveComponentGeneralization } from "./RemoveComponentGeneralization"; import { RemoveComponentVariant } from "./RemoveComponentVariant"; import { useQueryHandler } from "../../lib/hooks/useQueryHandler"; +import JsonViewer from "../JsonViewer"; interface ComponentProps { componentId: Scalars["Uuid"]["input"]; -}; +} export default function Component({ componentId }: ComponentProps) { const { loading, error, data } = useQuery(ComponentDocument, { @@ -104,7 +105,7 @@ export default function Component({ componentId }: ComponentProps) { )} {component.extras != undefined && ( - {JSON.stringify(component.extras, null, "\t")} + )} diff --git a/frontend/components/institutions/Institution.tsx b/frontend/components/institutions/Institution.tsx index ca13fc2a4..9d351299f 100644 --- a/frontend/components/institutions/Institution.tsx +++ b/frontend/components/institutions/Institution.tsx @@ -5,10 +5,11 @@ import { Typography, Skeleton, Result, - Descriptions, Tag, + Space, + Tabs, + TabsProps, } from "antd"; -import { PageHeader } from "@ant-design/pro-layout"; import { InstitutionDocument } from "../../queries/institutions.generated"; import { Scalars } from "../../__generated__/graphql"; import CreateComponent from "../components/CreateComponent"; @@ -19,7 +20,6 @@ import CreateDatabase from "../databases/CreateDatabase"; import AddInstitutionRepresentative from "./AddInstitutionRepresentative"; import Link from "next/link"; import paths from "../../paths"; -import { ReactNode } from "react"; import { DataFormatTable } from "../dataFormats/DataFormatTable"; import { ComponentTable } from "../components/ComponentTable"; import DatabaseTable from "../databases/DatabaseTable"; @@ -37,12 +37,17 @@ import RemoveInstitutionRepresentative from "./RemoveInstitutionRepresentative"; import { useQueryHandler } from "../../lib/hooks/useQueryHandler"; import ConfirmInstitutionMethodDeveloper from "../methods/ConfirmInstitutionMethodDeveloper"; import { ConfirmComponentManufacturer } from "../components/ConfirmComponentManufacturer"; +import ContactInformation from "../ContactInformation"; +import JsonViewer from "../JsonViewer"; +import PageHeader from "../PageHeader"; +import { isTruthy } from "../../lib/array"; +import InstitutionTable from "./InstitutionTable"; -interface InstitutionProps { +interface Props { institutionId: Scalars["Uuid"]["input"]; -}; +} -export default function Institution({ institutionId }: InstitutionProps) { +export default function Institution({ institutionId }: Props) { const { loading, error, data } = useQuery(InstitutionDocument, { variables: { uuid: institutionId, @@ -65,9 +70,260 @@ export default function Institution({ institutionId }: InstitutionProps) { ); } + const mainTabs: TabsProps["items"] = [ + (institution.manufacturedComponents.edges.length >= 1 || + institution.managedComponents.isAuthorizedToAddEdge) && { + key: "components", + label: "Manufactured Components", + children: ( + x.node, + )} + /> + ), + }, + (institution.developedMethods.edges.length >= 1 || + institution.managedMethods.isAuthorizedToAddEdge) && { + key: "methods", + label: "Developed Methods", + children: ( + x.node)} + /> + ), + }, + (institution.operatedDatabases.edges.length >= 1 || + institution.operatedDatabases.isAuthorizedToAddEdge) && { + key: "databases", + label: "Operated Databases", + children: ( + x.node)} + /> + ), + }, + (institution.gnuPgKeyFingerprints.edges.length >= 1 || + institution.gnuPgKeyFingerprints.isAuthorizedToAddEdge) && { + key: "gnuPgKeyFingerprints", + label: "GnuPG Key Fingerprints", + children: ( + e.node, + ) as GnuPgKeyFingerprintsPartialFragment[] + } + institutionId={institution.uuid} + /> + ), + }, + ].filter(isTruthy); + + const managedTabs: TabsProps["items"] = [ + (institution.managedComponents.edges.length >= 1 || + institution.managedComponents.isAuthorizedToAddEdge) && { + key: "components", + label: "Components", + children: ( + x.node)} + /> + ), + }, + (institution.managedMethods.edges.length >= 1 || + institution.managedMethods.isAuthorizedToAddEdge) && { + key: "methods", + label: "Methods", + children: ( + x.node)} + /> + ), + }, + (institution.managedDataFormats.edges.length >= 1 || + institution.managedDataFormats.isAuthorizedToAddEdge) && { + key: "dataFormats", + label: "Data Formats", + children: ( + x.node)} + /> + ), + }, + (institution.managedInstitutions.edges.length >= 1 || + institution.managedInstitutions.isAuthorizedToAddEdge) && { + key: "institutions", + label: "Institutions", + children: ( + x.node, + )} + /> + ), + }, + institution.openIdConnectApplications.isAuthorizedToAddEdge && { + key: "openIdConnectApplications", + label: "OpenId Connect Applications", + children: ( + e.node, + ) as OpenIdConnectApplicationsPartialFragment[] + } + /> + ), + }, + ].filter(isTruthy); + + const createTabs: TabsProps["items"] = [ + institution.managedComponents.isAuthorizedToAddEdge && { + key: "components", + label: "Components", + children: ( + + ), + }, + institution.managedMethods.isAuthorizedToAddEdge && { + key: "methods", + label: "Methods", + children: , + }, + institution.managedDataFormats.isAuthorizedToAddEdge && { + key: "dataFormats", + label: "Data Formats", + children: , + }, + institution.managedInstitutions.isAuthorizedToAddEdge && { + key: "institutions", + label: "Institutions", + children: , + }, + institution.operatedDatabases.isAuthorizedToAddEdge && { + key: "databases", + label: "Databases", + children: , + }, + institution.gnuPgKeyFingerprints.isAuthorizedToAddEdge && { + key: "gnuPgKeyFingerprints", + label: "GnuPG Key Fingerprints", + children: , + }, + institution.openIdConnectApplications.isAuthorizedToAddEdge && { + key: "openIdConnectApplications", + label: "OpenId Connect Applications", + children: ( + + ), + }, + institution.representatives.isAuthorizedToAddEdge && { + key: "representatives", + label: "Representatives", + children: ( + <> + + + ), + }, + ].filter(isTruthy); + + const pendingTabs: TabsProps["items"] = [ + institution.pendingManufacturedComponents.isAuthorizedToConfirmEdges && + institution.pendingManufacturedComponents.edges.length >= 1 && { + key: "components", + label: "Components", + children: ( + <> + ( + + + {item.node.name} + + + + )} + /> + + ), + }, + institution.pendingDevelopedMethods.isAuthorizedToConfirmEdges && + institution.pendingDevelopedMethods.edges.length >= 1 && { + key: "methods", + label: "Methods", + children: ( + <> + ( + + + {item.node.name} + + + + )} + /> + + ), + }, + institution.representatives.isAuthorizedToAddEdge && + institution.pendingRepresentatives != null && + institution.pendingRepresentatives.edges.length >= 1 && { + key: "representatives", + label: "Representatives", + children: ( + <> + ( + + + {`${item.node.name} (${item.node.uuid})`} + + {item.role} + {item.isAuthorizedToRemoveEdge && ( + + )} + + )} + /> + + ), + }, + ].filter(isTruthy); + return ( <> , ]} - extra={([] as ReactNode[]) - .concat( - institution.isAuthorizedToUpdateNode - ? [ - , - ] - : [], - ) - .concat( - institution.isAuthorizedToDeleteNode - ? [ - , - ] - : [], - ) - .concat( - institution.isAuthorizedToSwitchOperatingStateOfNode - ? [ - , - ] - : [], - )} - backIcon={false} + extra={[ + institution.isAuthorizedToUpdateNode && ( + + ), + institution.isAuthorizedToSwitchOperatingStateOfNode && ( + + ), + institution.isAuthorizedToDeleteNode && ( + + ), + ].filter(Boolean)} > - - {institution.uuid} - {institution.contact?.phoneNumber && ( - - {institution.contact.phoneNumber} - - )} - {institution.contact?.postalAddress && ( - - {institution.contact.postalAddress} - + + {institution.extras != null && ( + )} - {institution.contact?.emailAddress && ( - - {institution.contact.emailAddress} - + + {institution.representatives.edges.length >= 1 && ( +
+ <> + Represented by{" "} + {institution.representatives.edges.map((edge, index) => ( + + + {`${edge.node.name} (${edge.node.uuid})`} + {" "} + as {edge.role} + {edge.isAuthorizedToRemoveEdge && ( + + )} + {index < institution.representatives.edges.length - 2 && + ", "} + {index < institution.representatives.edges.length - 1 && + " and "} + + ))} + +
)} - {institution.contact?.websiteLocator && ( - - - {institution.contact.websiteLocator} - - - )} - {institution.extras != undefined && ( - - {JSON.stringify(institution.extras, null, "\t")} - + {institution.manager?.node && ( +
+ <> + Managed by{" "} + + {institution.manager?.node?.name} + + +
)} -
+
- Manufactured Components - x.node)} - /> - {institution.pendingManufacturedComponents.isAuthorizedToConfirmEdges && - institution.pendingManufacturedComponents.edges.length >= 1 && ( - ( - - - {item.node.name} - - - - )} - /> - )} - - Managed Components - x.node)} - /> - {institution.managedComponents.isAuthorizedToAddEdge && ( - - )} - - Operated Databases - x.node)} - /> - {institution.operatedDatabases.isAuthorizedToAddEdge && ( - - )} - - Managed Data Formats - x.node)} - /> - {institution.managedDataFormats.isAuthorizedToAddEdge && ( - - )} - - Managed Methods - x.node)} - /> - {institution.managedMethods.isAuthorizedToAddEdge && ( - - )} - - Developed Methods - ( - - {item.node.name} - - )} - /> - {institution.pendingDevelopedMethods.isAuthorizedToConfirmEdges && - institution.pendingDevelopedMethods.edges.length >= 1 && ( - ( - - - {item.node.name} - - - - )} - /> - )} - - GnuPG Key Fingerprints - e.node, - ) as GnuPgKeyFingerprintsPartialFragment[] - } - institutionId={institution.uuid} - /> - {institution.gnuPgKeyFingerprints.isAuthorizedToAddEdge && ( - - )} - {institution.openIdConnectApplications.isAuthorizedToAddEdge && ( + {mainTabs.length >= 1 && } + {managedTabs.length >= 1 && ( <> - OpenId Connect Applications + Managed & Owned Entities - e.node, - ) as OpenIdConnectApplicationsPartialFragment[] - } - /> + )} - {institution.openIdConnectApplications.isAuthorizedToAddEdge && ( - - )} - - Managed Institutions - x.node)} - renderItem={(item) => ( - - {item.name} - - )} - /> - {institution.managedInstitutions.isAuthorizedToAddEdge && ( - - )} - - Representatives - ( - - - {`${item.node.name} (${item.node.uuid})`} - - {item.role} - {item.isAuthorizedToRemoveEdge && ( - - )} - - )} - /> - {institution.representatives.isAuthorizedToAddEdge && - institution.pendingRepresentatives != null && - institution.pendingRepresentatives.edges.length >= 1 && ( - ( - - - {`${item.node.name} (${item.node.uuid})`} - - {item.role} - {item.isAuthorizedToRemoveEdge && ( - - )} - - )} - /> - )} - {institution.representatives.isAuthorizedToAddEdge && ( - + {createTabs.length >= 1 && ( + <> + + + Create & Add Entities + + + )} - {institution.manager?.node && ( + {pendingTabs.length >= 1 && ( <> - Managing Institution - - {institution.manager?.node?.name} - + Pending Entities + )} diff --git a/frontend/components/institutions/InstitutionTable.tsx b/frontend/components/institutions/InstitutionTable.tsx new file mode 100644 index 000000000..b4da664d4 --- /dev/null +++ b/frontend/components/institutions/InstitutionTable.tsx @@ -0,0 +1,55 @@ +import { Table } from "antd"; +import { InstitutionsPartialFragment } from "../../queries/institutions.generated"; +import { + getNameColumnProps, + getAbbreviationColumnProps, + getDescriptionColumnProps, + getUuidColumnProps, +} from "../../lib/table"; +import paths from "../../paths"; +import { useState } from "react"; +import { setMapValue } from "../../lib/freeTextFilter"; + +export default function InstitutionTable({ + loading, + institutions, +}: { + loading: boolean; + institutions: InstitutionsPartialFragment[]; +}) { + const [filterText, setFilterText] = useState(() => new Map()); + const onFilterTextChange = setMapValue(filterText, setFilterText); + + return ( + ( + onFilterTextChange, + (x) => filterText.get(x), + paths.institution, + ), + }, + { + ...getNameColumnProps<(typeof nodes)[0]>(onFilterTextChange, (x) => + filterText.get(x), + ), + }, + { + ...getAbbreviationColumnProps<(typeof nodes)[0]>( + onFilterTextChange, + (x) => filterText.get(x), + ), + }, + { + ...getDescriptionColumnProps<(typeof nodes)[0]>( + onFilterTextChange, + (x) => filterText.get(x), + ), + }, + ]} + dataSource={institutions} + /> + ); +} diff --git a/frontend/components/institutions/RemoveInstitutionRepresentative.tsx b/frontend/components/institutions/RemoveInstitutionRepresentative.tsx index 71acf596e..0cd345f56 100644 --- a/frontend/components/institutions/RemoveInstitutionRepresentative.tsx +++ b/frontend/components/institutions/RemoveInstitutionRepresentative.tsx @@ -1,5 +1,5 @@ import { useMutation } from "@apollo/client/react"; -import { Button } from "antd"; +import { Button, Popconfirm, Tooltip } from "antd"; import { InstitutionDocument } from "../../queries/institutions.generated"; import { Scalars } from "../../__generated__/graphql"; import { UserDocument } from "../../queries/users.generated"; @@ -8,6 +8,7 @@ import { RemoveInstitutionRepresentativeMutation, } from "../../queries/institutionRepresentatives.generated"; import { useMutationHandler } from "../../lib/hooks/useMutationHandler"; +import { DeleteOutlined } from "@ant-design/icons"; interface Props { institutionId: Scalars["Uuid"]["input"]; @@ -63,8 +64,23 @@ export default function RemoveInstitutionRepresentative({ }; return ( - + + +
( - onFilterTextChange, - (x) => filterText.get(x), - paths.institution, - ), - }, - { - ...getNameColumnProps<(typeof nodes)[0]>(onFilterTextChange, (x) => - filterText.get(x), - ), - }, - { - ...getAbbreviationColumnProps<(typeof nodes)[0]>( - onFilterTextChange, - (x) => filterText.get(x), - ), - }, - { - ...getDescriptionColumnProps<(typeof nodes)[0]>( - onFilterTextChange, - (x) => filterText.get(x), - ), - }, - ]} - dataSource={nodes} - /> + The GraphQL endpoint{" "} provides all information about institutions. diff --git a/frontend/queries/institutions.graphql b/frontend/queries/institutions.graphql index 8234cfbad..a9b8c128f 100644 --- a/frontend/queries/institutions.graphql +++ b/frontend/queries/institutions.graphql @@ -1,5 +1,9 @@ -#import "./openIdConnect.graphql" +#import "./components.graphql" +#import "./dataFormats.graphql" +#import "./databases.graphql" #import "./gnuPgKeyFingerprints.graphql" +#import "./methods.graphql" +#import "./openIdConnect.graphql" fragment ContactInformationPartial on ContactInformation { phoneNumber @@ -39,32 +43,14 @@ fragment InstitutionPartial on Institution { manufacturedComponents { edges { node { - id - uuid - name - abbreviation - description - categories - availability { - from - to - } + ...ComponentsPartial } } } managedComponents { edges { node { - id - uuid - name - abbreviation - description - categories - availability { - from - to - } + ...ComponentsPartial } } isAuthorizedToAddEdge @@ -82,18 +68,7 @@ fragment InstitutionPartial on Institution { operatedDatabases { edges { node { - id - uuid - name - description - locator - operator { - node { - id - uuid - name - } - } + ...DatabasesPartial } } isAuthorizedToAddEdge @@ -131,45 +106,7 @@ fragment InstitutionPartial on Institution { managedDataFormats { edges { node { - id - uuid - name - extension - description - mediaType - schemaLocator - reference { - ... on Standard { - abstract - section - title - locator - numeration { - mainNumber - prefix - suffix - } - standardizers - year - } - ... on Publication { - abstract - section - title - arXiv - authors - doi - urn - webAddress - } - } - manager { - node { - id - uuid - name - } - } + ...DataFormatsPartial } } isAuthorizedToAddEdge @@ -177,45 +114,7 @@ fragment InstitutionPartial on Institution { managedMethods { edges { node { - id - uuid - name - description - validity { - from - to - } - availability { - from - to - } - reference { - ... on Standard { - abstract - section - title - locator - numeration { - mainNumber - prefix - suffix - } - standardizers - year - } - ... on Publication { - abstract - section - title - arXiv - authors - doi - urn - webAddress - } - } - calculationLocator - categories + ...MethodsPartial } } isAuthorizedToAddEdge @@ -223,9 +122,7 @@ fragment InstitutionPartial on Institution { developedMethods { edges { node { - id - uuid - name + ...MethodsPartial } } } diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 6555bcfc9..5493eb546 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -2,13 +2,6 @@ # yarn lockfile v1 -"@ant-design/colors@^7.0.0": - version "7.2.1" - resolved "https://registry.yarnpkg.com/@ant-design/colors/-/colors-7.2.1.tgz#3bbc1c6c18550020d1622a0067ff03492318df98" - integrity sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ== - dependencies: - "@ant-design/fast-color" "^2.0.6" - "@ant-design/colors@^8.0.0", "@ant-design/colors@^8.0.1": version "8.0.1" resolved "https://registry.yarnpkg.com/@ant-design/colors/-/colors-8.0.1.tgz#6b5444f2ab4061c7b1aa4bc776adb023b0253161" @@ -25,19 +18,6 @@ "@babel/runtime" "^7.23.2" "@rc-component/util" "^1.4.0" -"@ant-design/cssinjs@^1.21.1": - version "1.24.0" - resolved "https://registry.yarnpkg.com/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz#7db091f03f189abc77a13cbd27a2293802cd7285" - integrity sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg== - dependencies: - "@babel/runtime" "^7.11.1" - "@emotion/hash" "^0.8.0" - "@emotion/unitless" "^0.7.5" - classnames "^2.3.1" - csstype "^3.1.3" - rc-util "^5.35.0" - stylis "^4.3.4" - "@ant-design/cssinjs@^2.1.2": version "2.1.2" resolved "https://registry.yarnpkg.com/@ant-design/cssinjs/-/cssinjs-2.1.2.tgz#0219e37afdd957248b10da366febae1e4001c952" @@ -51,13 +31,6 @@ csstype "^3.1.3" stylis "^4.3.4" -"@ant-design/fast-color@^2.0.6": - version "2.0.6" - resolved "https://registry.yarnpkg.com/@ant-design/fast-color/-/fast-color-2.0.6.tgz#ab4d4455c1542c9017d367c2fa8ca3e4215d0ba2" - integrity sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA== - dependencies: - "@babel/runtime" "^7.24.7" - "@ant-design/fast-color@^3.0.0", "@ant-design/fast-color@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@ant-design/fast-color/-/fast-color-3.0.1.tgz#fee56b95427c0b55b216c93d9a7f3473f31615b5" @@ -68,17 +41,6 @@ resolved "https://registry.yarnpkg.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz#ed2be7fb4d82ac7e1d45a54a5b06d6cecf8be6f6" integrity sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA== -"@ant-design/icons@^5.0.0": - version "5.6.1" - resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-5.6.1.tgz#7290fcdc3d96ff3fca793ed399053cd29ad5dbd3" - integrity sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg== - dependencies: - "@ant-design/colors" "^7.0.0" - "@ant-design/icons-svg" "^4.4.0" - "@babel/runtime" "^7.24.8" - classnames "^2.2.6" - rc-util "^5.31.1" - "@ant-design/icons@^6.1.0": version "6.1.0" resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-6.1.0.tgz#97cc14a3c0528b8e2b37f41f232b019f2ca38c2c" @@ -89,55 +51,6 @@ "@rc-component/util" "^1.3.0" clsx "^2.1.1" -"@ant-design/pro-layout@^7.10.3": - version "7.22.7" - resolved "https://registry.yarnpkg.com/@ant-design/pro-layout/-/pro-layout-7.22.7.tgz#7002b1838e1cdf879bbec813677b478c26fbbb77" - integrity sha512-fvmtNA1r9SaasVIQIQt611VSlNxtVxDbQ3e+1GhYQza3tVJi/3gCZuDyfMfTnbLmf3PaW/YvLkn7MqDbzAzoLA== - dependencies: - "@ant-design/cssinjs" "^1.21.1" - "@ant-design/icons" "^5.0.0" - "@ant-design/pro-provider" "2.16.2" - "@ant-design/pro-utils" "2.18.0" - "@babel/runtime" "^7.18.0" - "@umijs/route-utils" "^4.0.0" - "@umijs/use-params" "^1.0.9" - classnames "^2.3.2" - lodash "^4.17.21" - lodash-es "^4.17.21" - path-to-regexp "8.2.0" - rc-resize-observer "^1.1.0" - rc-util "^5.0.6" - swr "^2.0.0" - warning "^4.0.3" - -"@ant-design/pro-provider@2.16.2": - version "2.16.2" - resolved "https://registry.yarnpkg.com/@ant-design/pro-provider/-/pro-provider-2.16.2.tgz#3ccec06ea9a69a4d48adc593a268413c813dab2f" - integrity sha512-0KmCH1EaOND787Jz6VRMYtLNZmqfT0JPjdUfxhyOxFfnBRfrjyfZgIa6CQoAJLEUMWv57PccWS8wRHVUUk2Yiw== - dependencies: - "@ant-design/cssinjs" "^1.21.1" - "@babel/runtime" "^7.18.0" - "@ctrl/tinycolor" "^3.4.0" - dayjs "^1.11.10" - rc-util "^5.0.1" - swr "^2.0.0" - -"@ant-design/pro-utils@2.18.0": - version "2.18.0" - resolved "https://registry.yarnpkg.com/@ant-design/pro-utils/-/pro-utils-2.18.0.tgz#50a6bcc95742b71a7a17252f7db6c2ccd2ec60f4" - integrity sha512-8+ikyrN8L8a8Ph4oeHTOJEiranTj18+9+WHCHjKNdEfukI7Rjn8xpYdLJWb2AUJkb9d4eoAqjd5+k+7w81Df0w== - dependencies: - "@ant-design/icons" "^5.0.0" - "@ant-design/pro-provider" "2.16.2" - "@babel/runtime" "^7.18.0" - classnames "^2.3.2" - dayjs "^1.11.10" - lodash "^4.17.21" - lodash-es "^4.17.21" - rc-util "^5.0.6" - safe-stable-stringify "^2.4.3" - swr "^2.0.0" - "@ant-design/react-slick@~2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@ant-design/react-slick/-/react-slick-2.0.0.tgz#2d1dac45e7fc94060355f7cc233e92ca247133dc" @@ -410,7 +323,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.28.6" -"@babel/runtime@^7.10.1", "@babel/runtime@^7.11.1", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.23.2", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.7", "@babel/runtime@^7.24.8", "@babel/runtime@^7.26.10", "@babel/runtime@^7.28.4": +"@babel/runtime@^7.10.1", "@babel/runtime@^7.11.1", "@babel/runtime@^7.18.0", "@babel/runtime@^7.20.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.7", "@babel/runtime@^7.26.10", "@babel/runtime@^7.28.4": version "7.29.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.2.tgz#9a6e2d05f4b6692e1801cd4fb176ad823930ed5e" integrity sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g== @@ -457,11 +370,6 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@ctrl/tinycolor@^3.4.0": - version "3.6.1" - resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz#b6c75a56a1947cc916ea058772d666a2c8932f31" - integrity sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA== - "@emnapi/core@^1.4.3": version "1.9.1" resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.9.1.tgz#2143069c744ca2442074f8078462e51edd63c7bd" @@ -2506,16 +2414,6 @@ "@typescript-eslint/types" "8.57.1" eslint-visitor-keys "^5.0.0" -"@umijs/route-utils@^4.0.0": - version "4.0.3" - resolved "https://registry.yarnpkg.com/@umijs/route-utils/-/route-utils-4.0.3.tgz#70779ee069ac048509786bcf316368d3516ca5fd" - integrity sha512-zPEcYhl1cSfkSRDzzGgoD1mDvGjxoOTJFvkn55srfgdQ3NZe2ZMCScCU6DEnOxuKP1XDVf8pqyqCDVd2+RCQIw== - -"@umijs/use-params@^1.0.9": - version "1.0.9" - resolved "https://registry.yarnpkg.com/@umijs/use-params/-/use-params-1.0.9.tgz#0ae4a87f4922d8e8e3fb4495b0f8f4de9ca38c52" - integrity sha512-QlN0RJSBVQBwLRNxbxjQ5qzqYIGn+K7USppMoIOVlf7fxXHsnQZ2bEsa6Pm74bt6DVQxpUE8HqvdStn6Y9FV1w== - "@ungap/structured-clone@^1.3.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" @@ -3247,11 +3145,6 @@ cjs-module-lexer@^2.1.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz#b3ca5101843389259ade7d88c77bd06ce55849ca" integrity sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ== -classnames@^2.2.1, classnames@^2.2.6, classnames@^2.3.1, classnames@^2.3.2: - version "2.5.1" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" - integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== - cli-cursor@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-5.0.0.tgz#24a4831ecf5a6b01ddeb32fb71a4b2088b0dce38" @@ -3469,7 +3362,7 @@ dataloader@^2.2.3: resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-2.2.3.tgz#42d10b4913515f5b37c6acedcb4960d6ae1b1517" integrity sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA== -dayjs@^1.0, dayjs@^1.11.10, dayjs@^1.11.11: +dayjs@^1.0, dayjs@^1.11.11: version "1.11.20" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.20.tgz#88d919fd639dc991415da5f4cb6f1b6650811938" integrity sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ== @@ -3536,11 +3429,6 @@ dependency-graph@^1.0.0: resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-1.0.0.tgz#bb5e85aec1310bc13b22dbd76e3196c4ee4c10d2" integrity sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg== -dequal@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" - integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== - detect-indent@^6.0.0: version "6.1.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" @@ -5436,17 +5324,12 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash-es@^4.17.21: - version "4.17.23" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.23.tgz#58c4360fd1b5d33afc6c0bbd3d1149349b1138e0" - integrity sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg== - lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== -lodash@^4.17.21, lodash@~4.17.0: +lodash@~4.17.0: version "4.17.23" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a" integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w== @@ -6008,11 +5891,6 @@ path-scurry@^1.11.1: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" -path-to-regexp@8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.2.0.tgz#73990cc29e57a3ff2a0d914095156df5db79e8b4" - integrity sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ== - path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -6102,24 +5980,6 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -rc-resize-observer@^1.1.0: - version "1.4.3" - resolved "https://registry.yarnpkg.com/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz#4fd41fa561ba51362b5155a07c35d7c89a1ea569" - integrity sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ== - dependencies: - "@babel/runtime" "^7.20.7" - classnames "^2.2.1" - rc-util "^5.44.1" - resize-observer-polyfill "^1.5.1" - -rc-util@^5.0.1, rc-util@^5.0.6, rc-util@^5.31.1, rc-util@^5.35.0, rc-util@^5.44.1: - version "5.44.4" - resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.44.4.tgz#89ee9037683cca01cd60f1a6bbda761457dd6ba5" - integrity sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w== - dependencies: - "@babel/runtime" "^7.18.3" - react-is "^18.2.0" - react-cookie@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/react-cookie/-/react-cookie-8.0.1.tgz#0d7f5112c9003b61c70d3d53f1f0a5b741e9f2e9" @@ -6259,11 +6119,6 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== -resize-observer-polyfill@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" - integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== - resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -6367,11 +6222,6 @@ safe-regex-test@^1.0.3, safe-regex-test@^1.1.0: es-errors "^1.3.0" is-regex "^1.2.1" -safe-stable-stringify@^2.4.3: - version "2.5.0" - resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd" - integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== - "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -6894,14 +6744,6 @@ swap-case@^2.0.2: dependencies: tslib "^2.0.3" -swr@^2.0.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/swr/-/swr-2.4.1.tgz#c9e48abff6bf4b04846342e2f1f6be108a078cf6" - integrity sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA== - dependencies: - dequal "^2.0.3" - use-sync-external-store "^1.6.0" - sync-fetch@0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/sync-fetch/-/sync-fetch-0.6.0.tgz#5759e775f3d5202e1b3d14821bc152fec32aa180" @@ -7200,11 +7042,6 @@ urlpattern-polyfill@^10.0.0: resolved "https://registry.yarnpkg.com/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz#1b2517e614136c73ba32948d5e7a3a063cba8e74" integrity sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw== -use-sync-external-store@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz#b174bfa65cb2b526732d9f2ac0a408027876f32d" - integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== - util-extend@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/util-extend/-/util-extend-1.0.3.tgz#a7c216d267545169637b3b6edc6ca9119e2ff93f" @@ -7239,13 +7076,6 @@ walker@^1.0.8: dependencies: makeerror "1.0.12" -warning@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" - integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== - dependencies: - loose-envify "^1.0.0" - web-streams-polyfill@^3.0.3: version "3.3.3" resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" From dab96d94429dda2904bce884f6ee4d942d7fe0b4 Mon Sep 17 00:00:00 2001 From: Simon Wacker Date: Sat, 28 Mar 2026 11:44:42 +0100 Subject: [PATCH 002/145] Ask for confirmation before deleting entities --- frontend/components/ContactInformation.tsx | 2 +- frontend/components/SafeDeleteButton.tsx | 60 ++++++++++++++++++ frontend/components/components/Component.tsx | 15 ++--- .../components/components/ComponentTable.tsx | 9 +-- .../components/RemoveComponentAssembly.tsx | 11 ++-- .../RemoveComponentGeneralization.tsx | 11 ++-- .../RemoveComponentManufacturer.tsx | 11 ++-- .../components/RemoveComponentVariant.tsx | 11 ++-- .../components/dataFormats/DataFormat.tsx | 22 +++---- frontend/components/databases/Database.tsx | 34 ++++------- .../components/databases/DatabaseTable.tsx | 8 +-- .../institutions/DeleteInstitution.tsx | 13 ++-- .../components/institutions/Institution.tsx | 2 +- .../RemoveInstitutionRepresentative.tsx | 25 ++------ frontend/components/methods/Method.tsx | 17 +++--- frontend/components/methods/MethodTable.tsx | 12 +--- .../RemoveInstitutionMethodDeveloper.tsx | 11 ++-- .../methods/RemoveUserMethodDeveloper.tsx | 11 ++-- .../DeleteOpenIdConnectApplication.tsx | 11 ++-- .../applications/OpenIdConnectApplication.tsx | 61 ++++++++----------- ...etOpenIdConnectApplicationClientSecret.tsx | 17 ++++-- .../DeleteOpenIdConnectAuthorization.tsx | 11 ++-- frontend/components/users/DeleteUser.tsx | 13 ++-- frontend/components/users/User.tsx | 42 +++---------- frontend/components/users/UserRoleTag.tsx | 14 +++-- frontend/queries/users.graphql | 1 + 26 files changed, 243 insertions(+), 212 deletions(-) create mode 100644 frontend/components/SafeDeleteButton.tsx diff --git a/frontend/components/ContactInformation.tsx b/frontend/components/ContactInformation.tsx index cb6d0fd32..a4b3d34a1 100644 --- a/frontend/components/ContactInformation.tsx +++ b/frontend/components/ContactInformation.tsx @@ -7,7 +7,7 @@ import { } from "@ant-design/icons"; import { ContactInformationPartialFragment } from "../queries/institutions.generated"; -export default function ContactCard({ +export default function ContactInformation({ contact, }: { contact: ContactInformationPartialFragment | null; diff --git a/frontend/components/SafeDeleteButton.tsx b/frontend/components/SafeDeleteButton.tsx new file mode 100644 index 000000000..64b9fe345 --- /dev/null +++ b/frontend/components/SafeDeleteButton.tsx @@ -0,0 +1,60 @@ +import { Button, Popconfirm, Tooltip } from "antd"; +import { DeleteOutlined, SyncOutlined } from "@ant-design/icons"; + +const capitalize = (s: T) => + (s[0].toUpperCase() + s.slice(1)) as Capitalize; + +export default function SafeDeleteButton({ + kind, + type = "text", + deleting, + onConfirm, +}: { + kind: "delete" | "remove"; + type: "icon" | "text"; + deleting: boolean; + onConfirm: (e?: React.MouseEvent) => void; +}) { + const title = capitalize(kind); + + const button = (() => { + switch (type) { + case "icon": + return ( + + + ); + } + })(); + + return ( + + {button} + + ); +} diff --git a/frontend/components/components/Component.tsx b/frontend/components/components/Component.tsx index a4c481125..90d44037e 100644 --- a/frontend/components/components/Component.tsx +++ b/frontend/components/components/Component.tsx @@ -1,5 +1,6 @@ import { useQuery } from "@apollo/client/react"; import { Scalars } from "../../__generated__/graphql"; +import PageHeader from "../PageHeader"; import { ComponentDocument } from "../../queries/components.generated"; import { Skeleton, @@ -11,7 +12,6 @@ import { Col, Space, } from "antd"; -import { PageHeader } from "@ant-design/pro-layout"; import { ReactNode } from "react"; import paths from "../../paths"; import Link from "next/link"; @@ -30,6 +30,7 @@ import { RemoveComponentGeneralization } from "./RemoveComponentGeneralization"; import { RemoveComponentVariant } from "./RemoveComponentVariant"; import { useQueryHandler } from "../../lib/hooks/useQueryHandler"; import JsonViewer from "../JsonViewer"; +import { isTruthy } from "../../lib/array"; interface ComponentProps { componentId: Scalars["Uuid"]["input"]; @@ -61,6 +62,7 @@ export default function Component({ componentId }: ComponentProps) { return ( <> ))} - extra={ - component.isAuthorizedToUpdateNode - ? [] - : [] - } - backIcon={false} + extra={[ + component.isAuthorizedToUpdateNode && ( + + ), + ].filter(isTruthy)} > {component.uuid} diff --git a/frontend/components/components/ComponentTable.tsx b/frontend/components/components/ComponentTable.tsx index dae86d29a..6e89d01b7 100644 --- a/frontend/components/components/ComponentTable.tsx +++ b/frontend/components/components/ComponentTable.tsx @@ -1,4 +1,3 @@ -import { Component } from "../../__generated__/graphql"; import { Table } from "antd"; import { useState } from "react"; import { setMapValue } from "../../lib/freeTextFilter"; @@ -11,14 +10,12 @@ import { getNameColumnProps, getUuidColumnProps, } from "../../lib/table"; +import { ComponentsPartialFragment } from "../../queries/components.generated"; interface ComponentTableProps { loading: boolean; - components: Pick< - Component, - "uuid" | "name" | "abbreviation" | "description" | "categories" - >[]; -}; + components: ComponentsPartialFragment[]; +} export function ComponentTable({ loading, components }: ComponentTableProps) { const [filterText, setFilterText] = useState(() => new Map()); diff --git a/frontend/components/components/RemoveComponentAssembly.tsx b/frontend/components/components/RemoveComponentAssembly.tsx index 3f2b30d0d..afa3ea28d 100644 --- a/frontend/components/components/RemoveComponentAssembly.tsx +++ b/frontend/components/components/RemoveComponentAssembly.tsx @@ -9,7 +9,7 @@ import { ComponentsDocument, } from "../../queries/components.generated"; import { useMutationHandler } from "../../lib/hooks/useMutationHandler"; -import { Button } from "antd"; +import SafeDeleteButton from "../SafeDeleteButton"; interface Props { assembledComponentId: Scalars["Uuid"]["input"]; @@ -65,8 +65,11 @@ export function RemoveComponentAssembly({ ); return ( - + ); } diff --git a/frontend/components/components/RemoveComponentGeneralization.tsx b/frontend/components/components/RemoveComponentGeneralization.tsx index f118e11df..5e503756c 100644 --- a/frontend/components/components/RemoveComponentGeneralization.tsx +++ b/frontend/components/components/RemoveComponentGeneralization.tsx @@ -9,7 +9,7 @@ import { ComponentsDocument, } from "../../queries/components.generated"; import { useMutationHandler } from "../../lib/hooks/useMutationHandler"; -import { Button } from "antd"; +import SafeDeleteButton from "../SafeDeleteButton"; interface Props { generalComponentId: Scalars["Uuid"]["input"]; @@ -65,8 +65,11 @@ export function RemoveComponentGeneralization({ ); return ( - + ); } diff --git a/frontend/components/components/RemoveComponentManufacturer.tsx b/frontend/components/components/RemoveComponentManufacturer.tsx index 8f9de84e2..af3a5bd6a 100644 --- a/frontend/components/components/RemoveComponentManufacturer.tsx +++ b/frontend/components/components/RemoveComponentManufacturer.tsx @@ -10,7 +10,7 @@ import { } from "../../queries/components.generated"; import { InstitutionDocument } from "../../queries/institutions.generated"; import { useMutationHandler } from "../../lib/hooks/useMutationHandler"; -import { Button } from "antd"; +import SafeDeleteButton from "../SafeDeleteButton"; interface Props { componentId: Scalars["Uuid"]["input"]; @@ -66,8 +66,11 @@ export function RemoveComponentManufacturer({ ); return ( - + ); } diff --git a/frontend/components/components/RemoveComponentVariant.tsx b/frontend/components/components/RemoveComponentVariant.tsx index b61ffb334..1b776b917 100644 --- a/frontend/components/components/RemoveComponentVariant.tsx +++ b/frontend/components/components/RemoveComponentVariant.tsx @@ -9,7 +9,7 @@ import { ComponentsDocument, } from "../../queries/components.generated"; import { useMutationHandler } from "../../lib/hooks/useMutationHandler"; -import { Button } from "antd"; +import SafeDeleteButton from "../SafeDeleteButton"; interface Props { oneComponentId: Scalars["Uuid"]["input"]; @@ -65,8 +65,11 @@ export function RemoveComponentVariant({ ); return ( - + ); } diff --git a/frontend/components/dataFormats/DataFormat.tsx b/frontend/components/dataFormats/DataFormat.tsx index af3b9ea7c..f12dd2e1a 100644 --- a/frontend/components/dataFormats/DataFormat.tsx +++ b/frontend/components/dataFormats/DataFormat.tsx @@ -1,16 +1,17 @@ import { Scalars } from "../../__generated__/graphql"; import { DataFormatDocument } from "../../queries/dataFormats.generated"; import { Skeleton, Result, Descriptions, Typography } from "antd"; -import { PageHeader } from "@ant-design/pro-layout"; import paths from "../../paths"; +import PageHeader from "../PageHeader"; import { Reference } from "../Reference"; import UpdateDataFormat from "./UpdateDataFormat"; import { useQuery } from "@apollo/client/react"; import { useQueryHandler } from "../../lib/hooks/useQueryHandler"; +import { isTruthy } from "../../lib/array"; interface DataFormatProps { dataFormatId: Scalars["Uuid"]["input"]; -}; +} export default function DataFormat({ dataFormatId }: DataFormatProps) { const { loading, error, data } = useQuery(DataFormatDocument, { @@ -38,19 +39,14 @@ export default function DataFormat({ dataFormatId }: DataFormatProps) { return ( <> , - ] - : [] - } - backIcon={false} + extra={[ + dataFormat.isAuthorizedToUpdateNode && ( + + ), + ].filter(isTruthy)} > {dataFormat.uuid} diff --git a/frontend/components/databases/Database.tsx b/frontend/components/databases/Database.tsx index 6b2a0b6cb..791a4f57f 100644 --- a/frontend/components/databases/Database.tsx +++ b/frontend/components/databases/Database.tsx @@ -4,18 +4,18 @@ import { } from "../../__generated__/graphql"; import { DatabaseDocument } from "../../queries/databases.generated"; import { Skeleton, Result, Descriptions, Typography, Tag } from "antd"; -import { PageHeader } from "@ant-design/pro-layout"; -import { ReactNode } from "react"; +import PageHeader from "../PageHeader"; import Link from "next/link"; import paths from "../../paths"; import UpdateDatabase from "./UpdateDatabase"; import VerifyDatabase from "./VerifyDatabase"; import { useQuery } from "@apollo/client/react"; import { useQueryHandler } from "../../lib/hooks/useQueryHandler"; +import { isTruthy } from "../../lib/array"; interface DatabaseProps { databaseId: Scalars["Uuid"]["input"]; -}; +} export default function Database({ databaseId }: DatabaseProps) { const { loading, error, data } = useQuery(DatabaseDocument, { @@ -43,31 +43,23 @@ export default function Database({ databaseId }: DatabaseProps) { return ( <> ] - : [], - ) - .concat( - database.isAuthorizedToVerifyNode && - database.verificationState == DatabaseVerificationState.Pending - ? [ - , - ] - : [], - )} + extra={[ + database.isAuthorizedToUpdateNode && ( + + ), + database.isAuthorizedToVerifyNode && + database.verificationState == DatabaseVerificationState.Pending && ( + + ), + ].filter(isTruthy)} tags={[ {database.verificationState} , ]} - backIcon={false} > {database.uuid} diff --git a/frontend/components/databases/DatabaseTable.tsx b/frontend/components/databases/DatabaseTable.tsx index 122abbae7..1da2c4c8a 100644 --- a/frontend/components/databases/DatabaseTable.tsx +++ b/frontend/components/databases/DatabaseTable.tsx @@ -9,15 +9,13 @@ import { getInternallyLinkedFilterableStringColumnProps, getUuidColumnProps, } from "../../lib/table"; -import { Database, Institution } from "../../__generated__/graphql"; +import { DatabasesPartialFragment } from "../../queries/databases.generated"; // TODO Pagination. See https://www.apollographql.com/docs/react/pagination/core-api/ interface DatabaseTableProps { loading: boolean; - databases: (Pick & { - operator: { node: Pick }; - })[]; -}; + databases: DatabasesPartialFragment[]; +} export function DatabaseTable({ loading, databases }: DatabaseTableProps) { const [filterText, setFilterText] = useState(() => new Map()); diff --git a/frontend/components/institutions/DeleteInstitution.tsx b/frontend/components/institutions/DeleteInstitution.tsx index c4dfe6cd2..55fe0db25 100644 --- a/frontend/components/institutions/DeleteInstitution.tsx +++ b/frontend/components/institutions/DeleteInstitution.tsx @@ -1,5 +1,4 @@ import { useMutation } from "@apollo/client/react"; -import { Button } from "antd"; import { useRouter } from "next/router"; import paths from "../../paths"; import { @@ -9,10 +8,11 @@ import { } from "../../queries/institutions.generated"; import { Scalars } from "../../__generated__/graphql"; import { useMutationHandler } from "../../lib/hooks/useMutationHandler"; +import SafeDeleteButton from "../SafeDeleteButton"; interface DeleteInstitutionProps { institutionId: Scalars["Uuid"]["input"]; -}; +} export default function DeleteInstitution({ institutionId, @@ -50,8 +50,11 @@ export default function DeleteInstitution({ }; return ( - + ); } diff --git a/frontend/components/institutions/Institution.tsx b/frontend/components/institutions/Institution.tsx index 9d351299f..b859b426d 100644 --- a/frontend/components/institutions/Institution.tsx +++ b/frontend/components/institutions/Institution.tsx @@ -351,7 +351,7 @@ export default function Institution({ institutionId }: Props) { institution.isAuthorizedToDeleteNode && ( ), - ].filter(Boolean)} + ].filter(isTruthy)} > {institution.extras != null && ( diff --git a/frontend/components/institutions/RemoveInstitutionRepresentative.tsx b/frontend/components/institutions/RemoveInstitutionRepresentative.tsx index 0cd345f56..696e0dfcd 100644 --- a/frontend/components/institutions/RemoveInstitutionRepresentative.tsx +++ b/frontend/components/institutions/RemoveInstitutionRepresentative.tsx @@ -1,5 +1,4 @@ import { useMutation } from "@apollo/client/react"; -import { Button, Popconfirm, Tooltip } from "antd"; import { InstitutionDocument } from "../../queries/institutions.generated"; import { Scalars } from "../../__generated__/graphql"; import { UserDocument } from "../../queries/users.generated"; @@ -8,7 +7,7 @@ import { RemoveInstitutionRepresentativeMutation, } from "../../queries/institutionRepresentatives.generated"; import { useMutationHandler } from "../../lib/hooks/useMutationHandler"; -import { DeleteOutlined } from "@ant-design/icons"; +import SafeDeleteButton from "../SafeDeleteButton"; interface Props { institutionId: Scalars["Uuid"]["input"]; @@ -64,23 +63,11 @@ export default function RemoveInstitutionRepresentative({ }; return ( - - - + ); } diff --git a/frontend/components/methods/RemoveUserMethodDeveloper.tsx b/frontend/components/methods/RemoveUserMethodDeveloper.tsx index 7ac8c7fce..4096b9fb8 100644 --- a/frontend/components/methods/RemoveUserMethodDeveloper.tsx +++ b/frontend/components/methods/RemoveUserMethodDeveloper.tsx @@ -1,5 +1,4 @@ import { useMutation } from "@apollo/client/react"; -import { Button } from "antd"; import { MethodDocument, MethodsDocument, @@ -11,6 +10,7 @@ import { RemoveUserMethodDeveloperMutation, } from "../../queries/userMethodDevelopers.generated"; import { useMutationHandler } from "../../lib/hooks/useMutationHandler"; +import SafeDeleteButton from "../SafeDeleteButton"; interface Props { methodId: Scalars["Uuid"]["input"]; @@ -64,8 +64,11 @@ export default function RemoveUserMethodDeveloper({ methodId, userId }: Props) { }; return ( - + ); } diff --git a/frontend/components/openIdConnect/applications/DeleteOpenIdConnectApplication.tsx b/frontend/components/openIdConnect/applications/DeleteOpenIdConnectApplication.tsx index 1ed628e6b..a50b82205 100644 --- a/frontend/components/openIdConnect/applications/DeleteOpenIdConnectApplication.tsx +++ b/frontend/components/openIdConnect/applications/DeleteOpenIdConnectApplication.tsx @@ -1,5 +1,4 @@ import { useMutation } from "@apollo/client/react"; -import { Button } from "antd"; import { ApplicationDocument, ApplicationsDocument, @@ -10,6 +9,7 @@ import { Scalars } from "../../../__generated__/graphql"; import { useMutationHandler } from "../../../lib/hooks/useMutationHandler"; import { useRouter } from "next/router"; import { Route } from "next"; +import SafeDeleteButton from "../../SafeDeleteButton"; interface DeleteApplicationProps { applicationId: Scalars["Uuid"]["input"]; @@ -61,8 +61,11 @@ export default function DeleteOpenIdConnectApplication({ }; return ( - + ); } diff --git a/frontend/components/openIdConnect/applications/OpenIdConnectApplication.tsx b/frontend/components/openIdConnect/applications/OpenIdConnectApplication.tsx index 3653680b9..5b50c61f8 100644 --- a/frontend/components/openIdConnect/applications/OpenIdConnectApplication.tsx +++ b/frontend/components/openIdConnect/applications/OpenIdConnectApplication.tsx @@ -1,16 +1,16 @@ -import { ReactNode } from "react"; import { Scalars } from "../../../__generated__/graphql"; import { Descriptions, Divider, Result, Skeleton, Typography } from "antd"; import UpdateOpenIdConnectApplication from "./UpdateOpenIdConnectApplication"; import OpenIdConnectAutorizationTable from "../authorizations/OpenIdConnectAuthorizationTable"; import OpenIdConnectTokenTable from "../tokens/OpenIdConnectTokenTable"; -import { PageHeader } from "@ant-design/pro-layout"; import DeleteOpenIdConnectApplication from "./DeleteOpenIdConnectApplication"; +import PageHeader from "../../PageHeader"; import { ApplicationDocument } from "../../../queries/openIdConnect.generated"; import ResetOpenIdConnectApplicationClientSecret from "./ResetOpenIdConnectApplicationClientSecret"; import { useQuery } from "@apollo/client/react"; import paths from "../../../paths"; import { useQueryHandler } from "../../../lib/hooks/useQueryHandler"; +import { isTruthy } from "../../../lib/array"; interface Props { applicationId: Scalars["Uuid"]["input"]; @@ -42,41 +42,30 @@ export default function OpenIdConnectApplication({ applicationId }: Props) { return ( <> , - ] - : [], - ) - .concat( - application.isAuthorizedToManageNode - ? [ - , - ] - : [], - ) - .concat( - application.isAuthorizedToManageNode - ? [ - , - ] - : [], - )} - backIcon={false} + extra={[ + application.isAuthorizedToManageNode && ( + + ), + application.isAuthorizedToManageNode && ( + + ), + application.isAuthorizedToManageNode && ( + + ), + ].filter(isTruthy)} > {application.uuid} diff --git a/frontend/components/openIdConnect/applications/ResetOpenIdConnectApplicationClientSecret.tsx b/frontend/components/openIdConnect/applications/ResetOpenIdConnectApplicationClientSecret.tsx index 7dc528a44..571b5b427 100644 --- a/frontend/components/openIdConnect/applications/ResetOpenIdConnectApplicationClientSecret.tsx +++ b/frontend/components/openIdConnect/applications/ResetOpenIdConnectApplicationClientSecret.tsx @@ -1,5 +1,5 @@ import { useMutation } from "@apollo/client/react"; -import { Button, App, Typography } from "antd"; +import { Button, App, Typography, Popconfirm } from "antd"; import { ResetApplicationClientSecretDocument, ResetApplicationClientSecretMutation, @@ -71,8 +71,17 @@ export default function ResetOpenIdConnectApplicationClientSecret({ }; return ( - + + + ); } diff --git a/frontend/components/openIdConnect/authorizations/DeleteOpenIdConnectAuthorization.tsx b/frontend/components/openIdConnect/authorizations/DeleteOpenIdConnectAuthorization.tsx index 990a3addd..547e3fdfc 100644 --- a/frontend/components/openIdConnect/authorizations/DeleteOpenIdConnectAuthorization.tsx +++ b/frontend/components/openIdConnect/authorizations/DeleteOpenIdConnectAuthorization.tsx @@ -1,5 +1,4 @@ import { useMutation } from "@apollo/client/react"; -import { Button } from "antd"; import { DeleteAuthorizationDocument, DeleteAuthorizationMutation, @@ -7,6 +6,7 @@ import { import { Scalars } from "../../../__generated__/graphql"; import { DocumentNode } from "graphql"; import { useMutationHandler } from "../../../lib/hooks/useMutationHandler"; +import SafeDeleteButton from "../../SafeDeleteButton"; interface DeleteAuthorizationProps { authorizationId: Scalars["Uuid"]["input"]; @@ -48,8 +48,11 @@ export default function DeleteOpenIdConnectAuthorization({ }; return ( - + ); } diff --git a/frontend/components/users/DeleteUser.tsx b/frontend/components/users/DeleteUser.tsx index e32f4491b..118ef8ae1 100644 --- a/frontend/components/users/DeleteUser.tsx +++ b/frontend/components/users/DeleteUser.tsx @@ -1,5 +1,4 @@ import { useMutation } from "@apollo/client/react"; -import { Button } from "antd"; import { useRouter } from "next/router"; import paths from "../../paths"; import { @@ -9,10 +8,11 @@ import { } from "../../queries/users.generated"; import { Scalars } from "../../__generated__/graphql"; import { useMutationHandler } from "../../lib/hooks/useMutationHandler"; +import SafeDeleteButton from "../SafeDeleteButton"; interface DeleteUserProps { userId: Scalars["Uuid"]["input"]; -}; +} export default function DeleteUser({ userId }: DeleteUserProps) { const router = useRouter(); @@ -48,8 +48,11 @@ export default function DeleteUser({ userId }: DeleteUserProps) { }; return ( - + ); } diff --git a/frontend/components/users/User.tsx b/frontend/components/users/User.tsx index c3747da94..d1abfd6d1 100644 --- a/frontend/components/users/User.tsx +++ b/frontend/components/users/User.tsx @@ -1,14 +1,7 @@ import { useQuery } from "@apollo/client/react"; -import { - Divider, - Typography, - Skeleton, - Descriptions, - List, - Result, -} from "antd"; -import { PageHeader } from "@ant-design/pro-layout"; +import { Divider, Typography, Skeleton, List, Result } from "antd"; import { UserDocument } from "../../queries/users.generated"; +import PageHeader from "../PageHeader"; import { Scalars } from "../../__generated__/graphql"; import paths from "../../paths"; import Link from "next/link"; @@ -18,10 +11,12 @@ import { UserRoleTag } from "./UserRoleTag"; import ConfirmUserMethodDeveloper from "../methods/ConfirmUserMethodDeveloper"; import ConfirmInstitutionRepresentative from "../institutions/ConfirmInstitutionRepresentative"; import DeleteUser from "./DeleteUser"; +import { isTruthy } from "../../lib/array"; +import ContactInformation from "../ContactInformation"; interface UserProps { userId: Scalars["Uuid"]["input"]; -}; +} export default function User({ userId }: UserProps) { const { loading, error, data } = useQuery(UserDocument, { @@ -51,6 +46,7 @@ export default function User({ userId }: UserProps) { return ( <> ( , - ].filter((x) => x != null)} - backIcon={false} + ].filter(isTruthy)} > - - {user.uuid} - {user.contact.emailAddress && ( - - - {user.contact.emailAddress} - - - )} - {user.contact.phoneNumber && ( - - {user.contact.phoneNumber} - - )} - {user.contact.websiteLocator && ( - - - {user.contact.websiteLocator} - - - )} - + {rolesCurrentUserCanAndMayWantToAdd && rolesCurrentUserCanAndMayWantToAdd.length >= 1 && ( } - closable={(!mutating && canRemove) || false} - onClose={() => remove()} + closable={canRemove} + closeIcon={ + + } color="magenta" > {role} diff --git a/frontend/queries/users.graphql b/frontend/queries/users.graphql index e813d72b2..5fea1b5c7 100644 --- a/frontend/queries/users.graphql +++ b/frontend/queries/users.graphql @@ -18,6 +18,7 @@ fragment UserPartial on User { phoneNumber isPhoneNumberConfirmed websiteLocator + postalAddress } roles rolesCurrentUserCanAdd From 3ce35bfb3b27eeb6674170aee8ff7524adfcbbb6 Mon Sep 17 00:00:00 2001 From: Simon Wacker Date: Sat, 28 Mar 2026 11:53:56 +0100 Subject: [PATCH 003/145] Tell what to do when migrations are pending or missing --- backend/src/Program.cs | 2 +- backend/test/Integration/MigrationTests.cs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/src/Program.cs b/backend/src/Program.cs index 87aafa794..c569a0428 100644 --- a/backend/src/Program.cs +++ b/backend/src/Program.cs @@ -164,7 +164,7 @@ IServiceProvider services var pendingMigrations = dbContext.Database.GetPendingMigrations(); 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`."); } } diff --git a/backend/test/Integration/MigrationTests.cs b/backend/test/Integration/MigrationTests.cs index f576aa4e9..feb28a1c9 100644 --- a/backend/test/Integration/MigrationTests.cs +++ b/backend/test/Integration/MigrationTests.cs @@ -44,7 +44,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 From 1dc44baa36eeee99021777dce38d2577653307f6 Mon Sep 17 00:00:00 2001 From: Simon Wacker Date: Sat, 28 Mar 2026 22:07:36 +0100 Subject: [PATCH 004/145] Corrext exists flags (now for real) --- ...ectExistsFlagsOfReferencesSecondAttempt.cs | 328 ++++++++++++++++++ .../ApplicationDbContextModelSnapshot.cs | 2 +- backend/src/Migrations/migrate.sql | 325 +++++++++++++++++ 3 files changed, 654 insertions(+), 1 deletion(-) create mode 100644 backend/src/Migrations/20260328153447_CorrectExistsFlagsOfReferencesSecondAttempt.cs 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/ApplicationDbContextModelSnapshot.cs b/backend/src/Migrations/ApplicationDbContextModelSnapshot.cs index 1d34b3a59..421851491 100644 --- a/backend/src/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/backend/src/Migrations/ApplicationDbContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using System.Text.Json; using Metabase.Data; diff --git a/backend/src/Migrations/migrate.sql b/backend/src/Migrations/migrate.sql index 6809f8f15..a17340535 100644 --- a/backend/src/Migrations/migrate.sql +++ b/backend/src/Migrations/migrate.sql @@ -1968,3 +1968,328 @@ 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; + From 97a5d12f1bb29c0383500af2c00c1dfe259da67a Mon Sep 17 00:00:00 2001 From: Simon Wacker Date: Sat, 28 Mar 2026 23:43:26 +0100 Subject: [PATCH 005/145] Notify on component creation, add tooltips with UUIDs to links, and clear global error messages on success --- frontend/components/Availability.tsx | 14 +++ frontend/components/CopyableText.tsx | 12 ++- frontend/components/Manager.tsx | 22 +++++ .../components/OpenEndedDateTimeRangeX.tsx | 16 ++-- frontend/components/TabLabel.tsx | 23 +++++ .../components/AddAssembledOfComponent.tsx | 3 +- .../components/AddComponentManufacturer.tsx | 3 +- .../AddConcretizationOfComponent.tsx | 3 +- .../AddGeneralizationOfComponent.tsx | 3 +- .../components/AddPartOfComponent.tsx | 3 +- .../components/AddVariantOfComponent.tsx | 3 +- frontend/components/components/Component.tsx | 26 +++-- .../components/components/CreateComponent.tsx | 80 ++++++++++++++-- .../dataFormats/CreateDataFormat.tsx | 3 +- .../components/databases/CreateDatabase.tsx | 3 +- .../AddGnuPgKeyFingerprint.tsx | 3 +- .../AddInstitutionRepresentative.tsx | 3 +- .../components/institutions/Institution.tsx | 94 ++++++++++++++----- .../methods/AddInstitutionMethodDeveloper.tsx | 3 +- .../methods/AddUserMethodDeveloper.tsx | 3 +- frontend/components/methods/CreateMethod.tsx | 3 +- frontend/components/users/AddUserRole.tsx | 3 +- frontend/lib/array.ts | 13 +++ frontend/package.json | 2 +- frontend/pages/me/manage/change-password.tsx | 1 + frontend/pages/me/manage/set-password.tsx | 1 + frontend/queries/components.graphql | 9 ++ frontend/queries/institutions.graphql | 15 +++ frontend/yarn.lock | 8 +- 29 files changed, 302 insertions(+), 76 deletions(-) create mode 100644 frontend/components/Availability.tsx create mode 100644 frontend/components/Manager.tsx create mode 100644 frontend/components/TabLabel.tsx diff --git a/frontend/components/Availability.tsx b/frontend/components/Availability.tsx new file mode 100644 index 000000000..76cd13330 --- /dev/null +++ b/frontend/components/Availability.tsx @@ -0,0 +1,14 @@ +import { OpenEndedDateTimeRange } from "../__generated__/graphql"; +import OpenEndedDateTimeRangeX from "./OpenEndedDateTimeRangeX"; + +export default function Availability({ + range, +}: { + range: OpenEndedDateTimeRange; +}) { + return ( +
+ Available +
+ ); +} diff --git a/frontend/components/CopyableText.tsx b/frontend/components/CopyableText.tsx index 17045ba44..bfb19f11d 100644 --- a/frontend/components/CopyableText.tsx +++ b/frontend/components/CopyableText.tsx @@ -1,8 +1,14 @@ import { Button, Space } from "antd"; import { CopyOutlined } from "@ant-design/icons"; -import { useState } from "react"; +import { ReactNode, useState } from "react"; -export default function CopyableText({ text }: { text: string }) { +export default function CopyableText({ + text, + children, +}: { + text: string; + children?: ReactNode; +}) { const [copied, setCopied] = useState(false); return ( @@ -11,7 +17,7 @@ export default function CopyableText({ text }: { text: string }) { fontFamily: "monospace", }} > - {text} + {children ? children : text} + + ); +} 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/JsonViewer.tsx b/frontend/components/JsonViewer.tsx index 56680d637..6f51f4469 100644 --- a/frontend/components/JsonViewer.tsx +++ b/frontend/components/JsonViewer.tsx @@ -1,13 +1,19 @@ +import CopyableBlock from "./CopyableBlock"; + export default function JsonViewer({ jsonData }: { jsonData: any }) { + const jsonString = JSON.stringify(jsonData, null, 2); + return ( -
-      
-        {JSON.stringify(jsonData, null, 2)}
-      
-    
+ +
+        
+          {jsonString}
+        
+      
+
); } diff --git a/frontend/components/PageHeader.tsx b/frontend/components/PageHeader.tsx index 9fea739bf..935207813 100644 --- a/frontend/components/PageHeader.tsx +++ b/frontend/components/PageHeader.tsx @@ -1,7 +1,8 @@ import { Breadcrumb, Button, Space, Typography } from "antd"; import { ArrowLeftOutlined } from "@ant-design/icons"; import { Scalars } from "../__generated__/graphql"; -import CopyableText from "./CopyableText"; +import Copyable from "./Copyable"; +import Id from "./Id"; const { Title, Text } = Typography; @@ -56,7 +57,9 @@ export default function PageHeader({
{id && (
- + + +
)} diff --git a/frontend/components/components/CreateComponent.tsx b/frontend/components/components/CreateComponent.tsx index 029da7dc1..73fdf1400 100644 --- a/frontend/components/components/CreateComponent.tsx +++ b/frontend/components/components/CreateComponent.tsx @@ -30,7 +30,8 @@ import { useMutationHandler } from "../../lib/hooks/useMutationHandler"; import Link from "next/link"; import paths from "../../paths"; import { pluralize, pluralizeIrregular } from "../../lib/array"; -import CopyableText from "../CopyableText"; +import Copyable from "../Copyable"; +import Id from "../Id"; type FormValues = { name: string; @@ -146,11 +147,11 @@ export default function CreateComponent({ description: ( <> - + - {component.uuid} + {" "} - + {component?.pendingManufacturers != null && component.pendingManufacturers.totalCount >= 1 && ( diff --git a/frontend/components/components/UpdateComponent.tsx b/frontend/components/components/UpdateComponent.tsx index 14d3ce4d3..09641e781 100644 --- a/frontend/components/components/UpdateComponent.tsx +++ b/frontend/components/components/UpdateComponent.tsx @@ -31,18 +31,8 @@ type FormValues = { }; interface UpdateComponentProps { - component: Pick< - ComponentPartialFragment, - | "uuid" - | "name" - | "abbreviation" - | "description" - | "availability" - | "categories" - | "prime" - | "switchableLayers" - >; -}; + component: ComponentPartialFragment; +} export default function UpdateComponent({ component }: UpdateComponentProps) { const [open, setOpen] = useState(false); diff --git a/frontend/components/dataFormats/UpdateDataFormat.tsx b/frontend/components/dataFormats/UpdateDataFormat.tsx index 8f50d18fc..dd504b980 100644 --- a/frontend/components/dataFormats/UpdateDataFormat.tsx +++ b/frontend/components/dataFormats/UpdateDataFormat.tsx @@ -22,18 +22,8 @@ type FormValues = { }; interface UpdateDataFormatProps { - dataFormat: Pick< - DataFormatPartialFragment, - | "uuid" - | "name" - | "extension" - | "description" - | "mediaType" - | "schemaLocator" - | "reference" - | "manager" - >; -}; + dataFormat: DataFormatPartialFragment; +} export default function UpdateDataFormat({ dataFormat, diff --git a/frontend/components/databases/UpdateDatabase.tsx b/frontend/components/databases/UpdateDatabase.tsx index 870e760b2..d0495e83f 100644 --- a/frontend/components/databases/UpdateDatabase.tsx +++ b/frontend/components/databases/UpdateDatabase.tsx @@ -18,11 +18,8 @@ type FormValues = { }; interface UpdateDatabaseProps { - database: Pick< - DatabasePartialFragment, - "uuid" | "name" | "description" | "locator" - >; -}; + database: DatabasePartialFragment; +} export default function UpdateDatabase({ database }: UpdateDatabaseProps) { const [open, setOpen] = useState(false); diff --git a/frontend/components/institutions/Institution.tsx b/frontend/components/institutions/Institution.tsx index 9ae9062a2..174cbd9cb 100644 --- a/frontend/components/institutions/Institution.tsx +++ b/frontend/components/institutions/Institution.tsx @@ -33,8 +33,6 @@ import OpenIdConnectApplicationTable from "../openIdConnect/applications/OpenIdC import CreateOpenIdConnectApplication from "../openIdConnect/applications/CreateOpenIdConnectApplication"; import GnuPgKeyFingerprintTable from "../gnuPgKeyFingerprints/GnuPgKeyFingerprintTable"; import AddGnuPgKeyFingerprint from "../gnuPgKeyFingerprints/AddGnuPgKeyFingerprint"; -import { GnuPgKeyFingerprintsPartialFragment } from "../../queries/gnuPgKeyFingerprints.generated"; -import { OpenIdConnectApplicationsPartialFragment } from "../../queries/openIdConnect.generated"; import RemoveInstitutionRepresentative from "./RemoveInstitutionRepresentative"; import { useQueryHandler } from "../../lib/hooks/useQueryHandler"; import ConfirmInstitutionMethodDeveloper from "../methods/ConfirmInstitutionMethodDeveloper"; @@ -137,11 +135,9 @@ export default function Institution({ institutionId }: Props) { children: ( e.node, - ) as GnuPgKeyFingerprintsPartialFragment[] - } + fingerprints={institution.gnuPgKeyFingerprints.edges.map( + (e) => e.node, + )} institutionId={institution.uuid} /> ), @@ -226,11 +222,9 @@ export default function Institution({ institutionId }: Props) { children: ( e.node, - ) as OpenIdConnectApplicationsPartialFragment[] - } + applications={institution.openIdConnectApplications.edges.map( + (e) => e.node, + )} /> ), }, diff --git a/frontend/components/institutions/UpdateInstitution.tsx b/frontend/components/institutions/UpdateInstitution.tsx index e83e91b35..4c4d37fac 100644 --- a/frontend/components/institutions/UpdateInstitution.tsx +++ b/frontend/components/institutions/UpdateInstitution.tsx @@ -25,11 +25,8 @@ type FormValues = { }; interface UpdateInstitutionProps { - institution: Pick< - InstitutionPartialFragment, - "uuid" | "name" | "abbreviation" | "description" | "contact" - >; -}; + institution: InstitutionPartialFragment; +} export default function UpdateInstitution({ institution, diff --git a/frontend/components/methods/UpdateMethod.tsx b/frontend/components/methods/UpdateMethod.tsx index 93f4f94e1..5ab41d039 100644 --- a/frontend/components/methods/UpdateMethod.tsx +++ b/frontend/components/methods/UpdateMethod.tsx @@ -34,18 +34,8 @@ type FormValues = { }; interface UpdateMethodProps { - method: Pick< - MethodPartialFragment, - | "uuid" - | "name" - | "description" - | "validity" - | "availability" - | "reference" - | "calculationLocator" - | "categories" - >; -}; + method: MethodPartialFragment; +} export default function UpdateMethod({ method }: UpdateMethodProps) { const [open, setOpen] = useState(false); From 9479e85a2e33af1dd03a86e74cbf7de71267dc1e Mon Sep 17 00:00:00 2001 From: Simon Wacker Date: Sun, 29 Mar 2026 22:12:00 +0200 Subject: [PATCH 007/145] Right-align user menu items (login/logout/register) --- frontend/components/Copyable.tsx | 2 +- frontend/components/CopyableBlock.tsx | 12 +++++++++--- frontend/components/NavBar.tsx | 25 +++++++++++++++++-------- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/frontend/components/Copyable.tsx b/frontend/components/Copyable.tsx index cd904d70f..a81a1bb7b 100644 --- a/frontend/components/Copyable.tsx +++ b/frontend/components/Copyable.tsx @@ -24,7 +24,7 @@ export default function Copyable({ setTimeout(() => setCopied(false), 2000); }} > - {copied ? "Copied!" : "Copy"} + {copied ? "Done" : "Copy"} ); diff --git a/frontend/components/CopyableBlock.tsx b/frontend/components/CopyableBlock.tsx index 6ec3872cd..745e5ad46 100644 --- a/frontend/components/CopyableBlock.tsx +++ b/frontend/components/CopyableBlock.tsx @@ -12,10 +12,16 @@ export default function CopyableBlock({ const [copied, setCopied] = useState(false); return ( -
+
{children}
); diff --git a/frontend/components/NavBar.tsx b/frontend/components/NavBar.tsx index b3c503e93..77d23085a 100644 --- a/frontend/components/NavBar.tsx +++ b/frontend/components/NavBar.tsx @@ -1,11 +1,11 @@ import { useQuery } from "@apollo/client/react"; import Link from "next/link"; import { useRouter } from "next/router"; -import { Menu, Button } from "antd"; +import { Menu, Button, Spin } from "antd"; import { CurrentUserDocument } from "../queries/currentUser.generated"; import paths from "../paths"; import { extractAntiforgeryTokenFromCookie } from "../lib/apollo"; -import { UserOutlined } from "@ant-design/icons"; +import { UserOutlined, LoadingOutlined } from "@ant-design/icons"; import type { Route } from "next"; type NavItemProps = @@ -18,11 +18,12 @@ type NavItemProps = interface NavBarProps { items: NavItemProps[]; -}; +} export default function NavBar({ items }: NavBarProps) { const router = useRouter(); - const currentUser = useQuery(CurrentUserDocument)?.data?.currentUser; + const { loading, data } = useQuery(CurrentUserDocument); + const currentUser = data?.currentUser; return ( <> @@ -43,10 +44,14 @@ export default function NavBar({ items }: NavBarProps) { ), )} - {/* I would like the following to be on the right but that is not possible at the moment, see issue https://github.com/ant-design/ant-design/issues/10749 */} - {currentUser ? ( + {loading ? ( + + } + /> + + ) : currentUser ? ( <> - {/* TODO Put information whether person is allowed to access OpenIdConnect information in query result of current user (using OpenIdConnectAuthorization) */} {currentUser?.isAuthorizedToManageOpenIdConnect && ( OpenId Connect @@ -56,6 +61,7 @@ export default function NavBar({ items }: NavBarProps) { title={currentUser.name} key={paths.me.manage.home} icon={} + style={{ marginLeft: "auto" }} > Profile @@ -83,7 +89,10 @@ export default function NavBar({ items }: NavBarProps) { ) : ( <> - + Login From 67a9985668527dede5a7f1d3224b21cc6846f3e8 Mon Sep 17 00:00:00 2001 From: Simon Wacker Date: Mon, 30 Mar 2026 20:03:44 +0200 Subject: [PATCH 008/145] Paginate, filter, and sort via GraphQL instead of in the UI, use source generated data loaders, use HotChocolate connections and pagination also for nested lists, and migrate to HotChocolate 16 --- backend/Directory.Build.props | 2 + backend/dotnet-tools.json | 4 +- .../Authentication/AuthenticationHandler.cs | 4 +- .../src/Authorization/CommonAuthorization.cs | 32 +- .../OpenIdConnectAuthorization.cs | 9 +- .../src/Configuration/AuthConfiguration.cs | 14 +- .../src/Configuration/GraphQlConfiguration.cs | 183 +- .../Controllers/AuthorizationController.cs | 5 +- backend/src/Data/ApplicationDbContext.cs | 156 +- backend/src/Data/Association.cs | 8 + backend/src/Data/AuditableAssociation.cs | 10 + backend/src/Data/AuditableEntity.cs | 21 + backend/src/Data/Component.cs | 3 +- backend/src/Data/ComponentAssembly.cs | 2 + ...omponentConcretizationAndGeneralization.cs | 1 + backend/src/Data/ComponentManufacturer.cs | 1 + backend/src/Data/ComponentVariant.cs | 2 + backend/src/Data/DataCopnstants.cs | 14 + backend/src/Data/DataFormat.cs | 3 +- backend/src/Data/Database.cs | 3 +- backend/src/Data/DbSeeder.cs | 54 +- backend/src/Data/Entity.cs | 4 +- backend/src/Data/GnuPgKeyFingerprint.cs | 13 +- backend/src/Data/IAssociation.cs | 7 + backend/src/Data/IAuditable.cs | 12 + backend/src/Data/IMethodDeveloper.cs | 1 + backend/src/Data/INamed.cs | 6 + backend/src/Data/Institution.cs | 4 +- .../src/Data/InstitutionMethodDeveloper.cs | 3 +- backend/src/Data/InstitutionRepresentative.cs | 1 + backend/src/Data/Method.cs | 3 +- .../OpenIdConnect/OpenIdConnectApplication.cs | 13 +- .../OpenIdConnectAuthorization.cs | 14 +- .../Data/OpenIdConnect/OpenIdConnectScope.cs | 13 +- .../Data/OpenIdConnect/OpenIdConnectToken.cs | 14 +- backend/src/Data/User.cs | 12 +- backend/src/Data/UserMethodDeveloper.cs | 3 +- backend/src/Extensions/LinqExtensions.cs | 190 ++ backend/src/Extensions/NodaTimeExtensions.cs | 10 +- backend/src/Extensions/StringExtensions.cs | 18 +- .../GraphQl/Associations/AssociationType.cs | 22 + .../AuditableAssociationFilterType.cs | 19 + .../AuditableAssociationSortType.cs | 19 + backend/src/GraphQl/AuthorizedConnection.cs | 14 +- .../GraphQl/AuthorizedPaginatedConnection.cs | 47 + .../CalorimetricDataX/CalorimetricData.cs | 77 + .../CalorimetricDataConnection.cs | 4 +- .../CalorimetricDataEdge.cs | 4 +- .../CalorimetricDataPropositionInput.cs | 3 +- .../CalorimetricDataQueries.cs | 95 + .../Common/OpenEndedDateTimeRangeType.cs | 4 + .../ComponentAssemblyFilterType.cs | 8 +- .../ComponentAssemblySortType.cs | 9 +- .../RemoveComponentAssemblyPayload.cs | 4 +- ...ncretizationAndGeneralizationFilterType.cs | 8 +- ...ConcretizationAndGeneralizationSortType.cs | 16 + .../RemoveComponentGeneralizationPayload.cs | 4 +- .../AddComponentManufacturerPayload.cs | 5 +- .../ComponentManufacturerFilterType.cs | 8 +- .../ComponentManufacturerMutations.cs | 6 +- .../ComponentManufacturerSortType.cs | 9 +- .../ConfirmComponentManufacturerPayload.cs | 5 +- .../RemoveComponentManufacturerPayload.cs | 4 +- .../ComponentVariantFilterType.cs | 8 +- .../ComponentVariantSortType.cs | 16 + .../RemoveComponentVariantPayload.cs | 4 +- .../ComponentAssembledOfConnection.cs | 15 +- .../Components/ComponentAssembledOfEdge.cs | 22 +- .../ComponentAssembledOfFilterType.cs | 1 - .../ComponentAssembledOfSortType.cs | 16 + .../Components/ComponentByIdDataLoader.cs | 20 - .../ComponentConcretizationOfConnection.cs | 5 +- .../ComponentConcretizationOfEdge.cs | 11 +- .../ComponentConcretizationOfFilterType.cs | 1 - .../ComponentConcretizationOfSortType.cs | 16 + ...tConcretizationsByComponentIdDataLoader.cs | 25 - .../Components/ComponentDataLoaders.cs | 165 ++ .../GraphQl/Components/ComponentFilterType.cs | 6 +- .../ComponentGeneralizationOfConnection.cs | 6 +- .../ComponentGeneralizationOfEdge.cs | 13 +- .../ComponentGeneralizationOfFilterType.cs | 1 - .../ComponentGeneralizationOfSortType.cs | 16 + ...tGeneralizationsByComponentIdDataLoader.cs | 25 - .../Components/ComponentManagerEdge.cs | 6 +- .../ComponentManufacturerConnection.cs | 29 +- .../Components/ComponentManufacturerEdge.cs | 16 +- .../ComponentManufacturerFilterType.cs | 1 - .../ComponentManufacturerSortType.cs | 16 + ...entManufacturersByComponentIdDataLoader.cs | 25 - .../ComponentPartOfByComponentIdDataLoader.cs | 25 - .../Components/ComponentPartOfConnection.cs | 6 +- .../GraphQl/Components/ComponentPartOfEdge.cs | 20 +- .../Components/ComponentPartOfFilterType.cs | 1 - .../Components/ComponentPartOfSortType.cs | 16 + .../ComponentPartsByComponentIdDataLoader.cs | 25 - .../GraphQl/Components/ComponentQueries.cs | 32 +- .../GraphQl/Components/ComponentSortType.cs | 4 +- .../src/GraphQl/Components/ComponentType.cs | 14 +- ...mponentVariantOfByComponentIdDataLoader.cs | 25 - .../ComponentVariantOfConnection.cs | 5 +- .../Components/ComponentVariantOfEdge.cs | 11 +- .../ComponentVariantOfFilterType.cs | 1 - .../Components/ComponentVariantOfSortType.cs | 16 + ...entManufacturersByComponentIdDataLoader.cs | 25 - backend/src/GraphQl/Connection.cs | 24 +- .../ContactInformationFilterType.cs | 2 + .../ContactInformationSortType.cs | 4 +- .../DataFormats/DataFormatByIdDataLoader.cs | 20 - .../DataFormats/DataFormatDataLoaders.cs | 31 + .../DataFormats/DataFormatFilterType.cs | 6 +- .../DataFormats/DataFormatManagerEdge.cs | 2 +- .../GraphQl/DataFormats/DataFormatQueries.cs | 24 +- .../GraphQl/DataFormats/DataFormatSortType.cs | 5 +- .../src/GraphQl/DataFormats/DataFormatType.cs | 4 +- backend/src/GraphQl/DataLoaders.cs | 126 + backend/src/GraphQl/DataX/AppliedMethod.cs | 12 +- backend/src/GraphQl/DataX/CalorimetricData.cs | 48 - .../DataX/CrossDatabaseDataReference.cs | 23 +- backend/src/GraphQl/DataX/Data.cs | 103 +- backend/src/GraphQl/DataX/DataApproval.cs | 33 +- .../src/GraphQl/DataX/DataConnectionBase.cs | 7 +- backend/src/GraphQl/DataX/DataEdgeBase.cs | 6 +- .../src/GraphQl/DataX/DataPropositionInput.cs | 6 + .../src/GraphQl/DataX/FileMetaInformation.cs | 11 +- backend/src/GraphQl/DataX/GeometricData.cs | 47 - backend/src/GraphQl/DataX/GetHttpsResource.cs | 20 +- backend/src/GraphQl/DataX/HygrothermalData.cs | 44 - backend/src/GraphQl/DataX/IApproval.cs | 2 - backend/src/GraphQl/DataX/IData.cs | 31 + backend/src/GraphQl/DataX/LifeCycleData.cs | 44 - .../GraphQl/DataX/OpenEndedDateTimeRange.cs | 1 - backend/src/GraphQl/DataX/OpticalData.cs | 66 - backend/src/GraphQl/DataX/PhotovoltaicData.cs | 44 - .../ToTreeVertexAppliedConversionMethod.cs | 14 +- .../Databases/DatabaseByIdDataLoader.cs | 20 - .../GraphQl/Databases/DatabaseDataLoaders.cs | 31 + .../GraphQl/Databases/DatabaseFilterType.cs | 6 +- .../GraphQl/Databases/DatabaseMutations.cs | 2 +- .../GraphQl/Databases/DatabaseOperatorEdge.cs | 2 +- .../src/GraphQl/Databases/DatabaseQueries.cs | 46 +- .../GraphQl/Databases/DatabaseResolvers.cs | 766 +----- .../src/GraphQl/Databases/DatabaseSortType.cs | 5 +- backend/src/GraphQl/Databases/DatabaseType.cs | 51 +- .../DescriptionOrReferenceFilterType.cs | 2 + .../DescriptionOrReferenceSortType.cs | 2 + .../DescriptionOrReferenceType.cs | 1 + backend/src/GraphQl/Edge.cs | 10 +- .../AssociationsByAssociateIdDataLoader.cs | 47 - ...erType.cs => AuditableEntityFilterType.cs} | 6 +- ...SortType.cs => AuditableEntitySortType.cs} | 7 +- .../GraphQl/Entities/EntityByIdDataLoader.cs | 39 - backend/src/GraphQl/Entities/EntityType.cs | 5 +- .../ErrorLoggingDiagnosticEventListener.cs | 187 +- .../src/GraphQl/Extensions/PageExtensions.cs | 66 + .../Extensions/ResolverContextExtensions.cs | 15 +- .../Extensions/SortingContextExtensions.cs | 24 - .../GraphQl/Filters/ScalarFilterInputTypes.cs | 65 +- .../GraphQl/GeometricDataX/GeometricData.cs | 77 + .../GeometricDataConnection.cs | 3 +- .../GeometricDataEdge.cs | 3 +- .../GeometricDataPropositionInput.cs | 3 +- .../GeometricDataX/GeometricDataQueries.cs | 95 + ...PgKeyFingerprintByFingerprintDataLoader.cs | 33 - .../GnuPgKeyFingerprintByIdDataLoader.cs | 20 - .../GnuPgKeyFingerprintDataLoaders.cs | 49 + .../GnuPgKeyFingerprintFilterType.cs | 6 +- .../GnuPgKeyFingerprintInstitutionEdge.cs | 2 +- .../GnuPgKeyFingerprintMutations.cs | 10 +- .../GnuPgKeyFingerprintQueries.cs | 21 +- .../GnuPgKeyFingerprintSortType.cs | 6 +- .../GnuPgKeyFingerprintType.cs | 4 +- .../GnuPgKeyFingerprintUserEdge.cs | 2 +- backend/src/GraphQl/GraphQlConstants.cs | 1 + backend/src/GraphQl/GraphQlThrowHelper.cs | 52 + backend/src/GraphQl/GraphQlTypeResources.cs | 8 + .../HygrothermalDataX/HygrothermalData.cs | 75 + .../HygrothermalDataConnection.cs | 3 +- .../HygrothermalDataEdge.cs | 4 +- .../HygrothermalDataPropositionInput.cs | 3 +- .../HygrothermalDataQueries.cs | 95 + .../AddInstitutionMethodDeveloperPayload.cs | 5 +- ...onfirmInstitutionMethodDeveloperPayload.cs | 5 +- .../InstitutionMethodDeveloperFilterType.cs | 8 +- .../InstitutionMethodDeveloperSortType.cs | 9 +- ...RemoveInstitutionMethodDeveloperPayload.cs | 5 +- .../InstitutionRepresentativeFilterType.cs | 8 +- .../InstitutionRepresentativeMutations.cs | 6 +- .../InstitutionRepresentativeSortType.cs | 9 +- .../RemoveInstitutionRepresentativePayload.cs | 2 +- ...eyFingerprintsByInstitutionIdDataLoader.cs | 25 - .../Institutions/InstitutionByIdDataLoader.cs | 20 - .../Institutions/InstitutionDataLoaders.cs | 289 +++ .../InstitutionDevelopedMethodConnection.cs | 16 +- .../InstitutionDevelopedMethodEdge.cs | 9 +- .../InstitutionDevelopedMethodSortType.cs | 17 + ...velopedMethodsByInstitutionIdDataLoader.cs | 25 - .../Institutions/InstitutionFilterType.cs | 6 +- ...nstitutionGnuPgKeyFingerprintConnection.cs | 6 +- ...nstitutionGnuPgKeyFingerprintFilterType.cs | 1 - .../InstitutionGnuPgKeyFingerprintSortType.cs | 17 + .../InstitutionManagedComponentConnection.cs | 10 +- .../InstitutionManagedComponentEdge.cs | 14 +- .../InstitutionManagedComponentSortType.cs | 16 + ...agedComponentsByInstitutionIdDataLoader.cs | 25 - .../InstitutionManagedDataFormatConnection.cs | 10 +- .../InstitutionManagedDataFormatEdge.cs | 14 +- .../InstitutionManagedDataFormatSortType.cs | 16 + ...gedDataFormatsByInstitutionIdDataLoader.cs | 25 - ...InstitutionManagedInstitutionConnection.cs | 14 +- .../InstitutionManagedInstitutionEdge.cs | 14 +- ...InstitutionManagedInstitutionFilterType.cs | 1 - .../InstitutionManagedInstitutionSortType.cs | 16 + ...edInstitutionsByInstitutionIdDataLoader.cs | 26 - .../InstitutionManagedMethodConnection.cs | 12 +- .../InstitutionManagedMethodEdge.cs | 14 +- .../InstitutionManagedMethodFilterType.cs | 1 - .../InstitutionManagedMethodSortType.cs | 17 + ...ManagedMethodsByInstitutionIdDataLoader.cs | 25 - .../Institutions/InstitutionManagerEdge.cs | 2 +- ...turedComponentByInstitutionIdDataLoader.cs | 25 - ...titutionManufacturedComponentConnection.cs | 38 +- .../InstitutionManufacturedComponentEdge.cs | 10 +- ...nstitutionManufacturedComponentSortType.cs | 17 + .../InstitutionOperatedDatabaseConnection.cs | 5 +- .../InstitutionOperatedDatabaseFilterType.cs | 1 - .../InstitutionOperatedDatabaseSortType.cs | 17 + ...ratedDatabasesByInstitutionIdDataLoader.cs | 25 - ...OwnedOpenIdConnectApplicationConnection.cs | 5 +- ...tutionOwnedOpenIdConnectApplicationEdge.cs | 2 + ...onOwnedOpenIdConnectApplicationSortType.cs | 17 + ...ctApplicationsByInstitutionIdDataLoader.cs | 26 - .../Institutions/InstitutionQueries.cs | 48 +- .../InstitutionRepresentativeConnection.cs | 7 +- .../InstitutionRepresentativeEdge.cs | 4 +- .../InstitutionRepresentativeFilterType.cs | 1 - .../InstitutionRepresentativeSortType.cs | 16 + ...epresentativesByInstitutionIdDataLoader.cs | 25 - .../Institutions/InstitutionSortType.cs | 5 +- .../GraphQl/Institutions/InstitutionType.cs | 71 +- ...velopedMethodsByInstitutionIdDataLoader.cs | 25 - ...uredComponentsByInstitutionIdDataLoader.cs | 25 - ...epresentativesByInstitutionIdDataLoader.cs | 25 - .../GraphQl/LifeCycleDataX/LifeCycleData.cs | 75 + .../LifeCycleDataConnection.cs | 3 +- .../LifeCycleDataEdge.cs | 4 +- .../LifeCycleDataPropositionInput.cs | 3 +- .../LifeCycleDataX/LifeCycleDataQueries.cs | 95 + .../Methods/InstitutionMethodDeveloperEdge.cs | 12 +- ...ionMethodDevelopersByMethodIdDataLoader.cs | 25 - .../GraphQl/Methods/MethodByIdDataLoader.cs | 20 - .../src/GraphQl/Methods/MethodDataLoaders.cs | 108 + .../Methods/MethodDeveloperConnection.cs | 29 +- .../GraphQl/Methods/MethodDeveloperEdge.cs | 6 +- .../Methods/MethodDeveloperFilterType.cs | 9 +- .../Methods/MethodDeveloperSortType.cs | 25 + .../src/GraphQl/Methods/MethodFilterType.cs | 6 +- .../src/GraphQl/Methods/MethodManagerEdge.cs | 2 +- backend/src/GraphQl/Methods/MethodQueries.cs | 24 +- backend/src/GraphQl/Methods/MethodSortType.cs | 5 +- backend/src/GraphQl/Methods/MethodType.cs | 6 +- ...ionMethodDevelopersByMethodIdDataLoader.cs | 25 - ...serMethodDevelopersByMethodIdDataLoader.cs | 25 - .../Methods/UserMethodDeveloperEdge.cs | 12 +- ...serMethodDevelopersByMethodIdDataLoader.cs | 25 - ...enIdConnectApplicationAuthorizationEdge.cs | 9 +- .../OpenIdConnectApplicationByIdDataLoader.cs | 26 - .../OpenIdConnectApplicationDataLoaders.cs | 32 + .../OpenIdConnectApplicationFilterType.cs | 8 +- ...licationGrantedAuthorizationConnection.cs} | 9 +- ...onnectApplicationIssuedTokenConnection.cs} | 9 +- .../OpenIdConnectApplicationMutations.cs | 24 +- .../OpenIdConnectApplicationOwnerEdge.cs | 2 +- .../OpenIdConnectApplicationQueries.cs | 36 +- .../OpenIdConnectApplicationSortType.cs | 22 + .../OpenIdConnectApplicationTokenEdge.cs | 9 +- .../OpenIdConnectApplicationType.cs | 46 +- .../OpenIdConnectEndpointExtensions.cs | 9 +- .../OpenIdConnectGrantTypeExtensions.cs | 9 +- .../OpenIdConnectResponseTypeExtensions.cs | 9 +- ...penIdConnectAuthorizationByIdDataLoader.cs | 26 - .../OpenIdConnectAuthorizationDataLoaders.cs | 32 + .../OpenIdConnectAuthorizationFilterType.cs | 10 +- ...nectAuthorizationIssuedTokenConnection.cs} | 15 +- .../OpenIdConnectAuthorizationMutations.cs | 1 - .../OpenIdConnectAuthorizationQueries.cs | 42 +- .../OpenIdConnectAuthorizationSortType.cs | 21 + .../OpenIdConnectAuthorizationTokenEdge.cs | 9 +- .../OpenIdConnectAuthorizationType.cs | 39 +- .../{Applications => }/OpenIdConnectScope.cs | 2 +- .../OpenIdConnectScopeExtensions.cs | 28 +- .../OpenIdConnectTokenByIdDataLoader.cs | 26 - .../Tokens/OpenIdConnectTokenDataLoaders.cs | 32 + .../Tokens/OpenIdConnectTokenFilterType.cs | 16 +- .../Tokens/OpenIdConnectTokenMutations.cs | 1 - .../Tokens/OpenIdConnectTokenQueries.cs | 39 +- .../Tokens/OpenIdConnectTokenSortType.cs | 23 + .../Tokens/OpenIdConnectTokenType.cs | 25 +- .../OpticalComponentSubtype.cs | 2 +- ...OpticalComponentSubtypePropositionInput.cs | 2 +- .../OpticalComponentType.cs | 2 +- .../OpticalComponentTypePropositionInput.cs | 2 +- .../src/GraphQl/OpticalDataX/OpticalData.cs | 85 + .../OpticalDataConnection.cs | 3 +- .../OpticalDataEdge.cs | 4 +- .../OpticalDataPropositionInput.cs | 3 +- .../OpticalDataX/OpticalDataQueries.cs | 95 + backend/src/GraphQl/PaginatedConnection.cs | 75 + backend/src/GraphQl/PaginatedEdge.cs | 31 + backend/src/GraphQl/PaginationHelpers.cs | 25 + .../PhotovoltaicDataX/PhotovoltaicData.cs | 75 + .../PhotovoltaicDataConnection.cs | 3 +- .../PhotovoltaicDataEdge.cs | 4 +- .../PhotovoltaicDataPropositionInput.cs | 3 +- .../PhotovoltaicDataQueries.cs | 95 + backend/src/GraphQl/Requests/DataQueries.cs | 1127 +++++++++ .../GraphQl/Requests/GraphQlRequestHelper.cs | 125 + .../Requests}/QueryingDatabases.cs | 3 +- .../src/GraphQl/{ => Scalars}/LocaleType.cs | 2 +- backend/src/GraphQl/Scalars/MyUriType.cs | 109 + .../src/GraphQl/Scalars/NonNegativeIntType.cs | 71 + backend/src/GraphQl/Sorting.cs | 18 + .../AddUserMethodDeveloperPayload.cs | 6 +- .../ConfirmUserMethodDeveloperPayload.cs | 5 +- .../RemoveUserMethodDeveloperPayload.cs | 5 +- .../UserMethodDeveloperFilterType.cs | 8 +- .../UserMethodDeveloperSortType.cs | 10 +- .../GnuPgKeyFingerprintsByUserIdDataLoader.cs | 25 - ...gUserDevelopedMethodsByUserIdDataLoader.cs | 25 - ...presentedInstitutionsByUserIdDataLoader.cs | 25 - .../Users/UseSignInManagerAttribute.cs | 2 +- .../GraphQl/Users/UseUserManagerAttribute.cs | 2 +- .../src/GraphQl/Users/UserByIdDataLoader.cs | 20 - backend/src/GraphQl/Users/UserDataLoaders.cs | 130 + .../Users/UserDevelopedMethodConnection.cs | 18 +- .../GraphQl/Users/UserDevelopedMethodEdge.cs | 10 +- .../Users/UserDevelopedMethodSortType.cs | 17 + .../UserDevelopedMethodsByUserIdDataLoader.cs | 25 - backend/src/GraphQl/Users/UserFilterType.cs | 9 +- .../UserGnuPgKeyFingerprintConnection.cs | 4 +- .../UserGnuPgKeyFingerprintFilterType.cs | 1 - .../Users/UserGnuPgKeyFingerprintSortType.cs | 17 + backend/src/GraphQl/Users/UserQueries.cs | 24 +- .../UserRepresentedInstitutionConnection.cs | 8 +- .../Users/UserRepresentedInstitutionEdge.cs | 22 +- .../UserRepresentedInstitutionFilterType.cs | 1 - .../UserRepresentedInstitutionSortType.cs | 17 + ...presentedInstitutionsByUserIdDataLoader.cs | 26 - backend/src/GraphQl/Users/UserSortType.cs | 7 +- backend/src/GraphQl/Users/UserType.cs | 27 +- ...ningAndEncryptionCertificateRotationJob.cs | 20 +- backend/src/Json/JsonSerializerSettings.cs | 7 + backend/src/Metabase.csproj | 69 +- ...18153447_CorrectExistsFlagsOfReferences.cs | 2 +- ...FlagsOfReferencesSecondAttempt.Designer.cs | 2023 +++++++++++++++ ...dUpdatedAndCreatedAtTimestamps.Designer.cs | 2243 +++++++++++++++++ ...204629_AddUpdatedAndCreatedAtTimestamps.cs | 693 +++++ .../ApplicationDbContextModelSnapshot.cs | 236 +- backend/src/Migrations/migrate.sql | 298 +++ ...04629_AddUpdatedAndCreatedAtTimestamps.sql | 298 +++ ...ctExistsFlagsOfReferencesSecondAttempt.sql | 256 ++ backend/src/Program.cs | 6 +- backend/src/Services/EmailSender.cs | 2 +- backend/src/Startup.cs | 38 +- backend/test/AuditableTests.cs | 75 + .../GraphQlSchemaTests.IsUnchanged.snap | 1595 +++++++----- backend/test/Metabase.Tests.csproj | 12 +- frontend/codegen.ts | 9 +- .../components/ActiveFilterAndSortBar.tsx | 128 + frontend/components/Availability.tsx | 14 - frontend/components/CielabColorViewer.tsx | 10 + frontend/components/CodeViewer.tsx | 32 + frontend/components/ContactInformation.tsx | 2 +- frontend/components/CopyButton.tsx | 73 + frontend/components/Copyable.tsx | 35 +- frontend/components/DeleteButton.tsx | 63 + frontend/components/EditButton.tsx | 30 + frontend/components/EnumTag.tsx | 16 + frontend/components/Float.tsx | 14 + .../components/FloatPropositionFormList.tsx | 118 - frontend/components/Footer.tsx | 9 +- frontend/components/Highlight.tsx | 6 +- frontend/components/Iconize.tsx | 16 + frontend/components/IdentifierItem.tsx | 95 + frontend/components/InlineList.tsx | 22 + frontend/components/JsonViewer.tsx | 50 +- frontend/components/JumpToId.tsx | 47 + frontend/components/Layout.tsx | 43 +- frontend/components/LazyTabs.tsx | 77 + frontend/components/Manager.tsx | 12 +- frontend/components/NavBar.tsx | 186 +- .../components/OpenEndedDateTimeRangeX.tsx | 14 +- frontend/components/PageHeader.tsx | 83 - frontend/components/PaginatedIdSelect.tsx | 162 ++ frontend/components/Pagination.tsx | 62 + frontend/components/QueryToolbar.tsx | 47 + frontend/components/Reference.tsx | 163 +- ...ReferenceForm.tsx => ReferenceSubform.tsx} | 107 +- frontend/components/SafeDeleteButton.tsx | 65 +- frontend/components/SearchSelect.tsx | 25 +- frontend/components/SelectComponentId.tsx | 49 - frontend/components/SelectInstitutionId.tsx | 49 - frontend/components/SelectUserId.tsx | 43 - frontend/components/SingleSignOnLayout.tsx | 2 +- frontend/components/SlideDown.tsx | 51 + frontend/components/TabLabel.tsx | 8 +- frontend/components/UnauthenticatedResult.tsx | 14 + frontend/components/UrlInput.tsx | 33 + frontend/components/UuidFormItem.tsx | 38 + .../components/UuidPropositionFormList.tsx | 103 - .../components/AddAssembledOfComponent.tsx | 66 +- .../components/AddComponentManufacturer.tsx | 40 +- .../AddConcretizationOfComponent.tsx | 40 +- .../AddGeneralizationOfComponent.tsx | 46 +- .../components/AddPartOfComponent.tsx | 66 +- .../components/AddVariantOfComponent.tsx | 40 +- frontend/components/components/Component.tsx | 456 +--- .../components/ComponentIdSelect.tsx | 17 + .../components/components/ComponentList.tsx | 24 + .../components/ComponentSummary.tsx | 303 +++ .../components/components/ComponentTable.tsx | 59 - .../components/components/CreateComponent.tsx | 28 +- .../components/PaginatedComponents.tsx | 86 + .../components/RemoveComponentAssembly.tsx | 2 +- .../RemoveComponentGeneralization.tsx | 2 +- .../RemoveComponentManufacturer.tsx | 2 +- .../components/RemoveComponentVariant.tsx | 2 +- .../components/components/UpdateComponent.tsx | 22 +- .../components/UpdateComponentAssembly.tsx | 10 +- frontend/components/data/DataSummary.tsx | 308 +++ .../data/calorimetric/CalorimetricData.tsx | 52 + .../calorimetric/CalorimetricDataList.tsx | 24 + .../calorimetric/CalorimetricDataSummary.tsx | 36 + .../PaginatedCalorimetricData.tsx | 68 + .../data/geometric/GeometricData.tsx | 46 + .../data/geometric/GeometricDataList.tsx | 24 + .../data/geometric/GeometricDataSummary.tsx | 27 + .../data/geometric/PaginatedGeometricData.tsx | 61 + .../data/hygrothermal/HygrothermalData.tsx | 52 + .../hygrothermal/HygrothermalDataList.tsx | 24 + .../hygrothermal/HygrothermalDataSummary.tsx | 10 + .../PaginatedHygrothermalData.tsx | 54 + .../data/lifeCycle/LifeCycleData.tsx | 46 + .../data/lifeCycle/LifeCycleDataList.tsx | 24 + .../data/lifeCycle/LifeCycleDataSummary.tsx | 10 + .../data/lifeCycle/PaginatedLifeCycleData.tsx | 54 + .../components/data/optical/OpticalData.tsx | 49 + .../data/optical/OpticalDataList.tsx | 27 + .../data/optical/OpticalDataRibbon.tsx | 26 + .../data/optical/OpticalDataSummary.tsx | 88 + .../data/optical/PaginatedOpticalData.tsx | 132 + .../PaginatedPhotovoltaicData.tsx | 54 + .../data/photovoltaic/PhotovoltaicData.tsx | 52 + .../photovoltaic/PhotovoltaicDataList.tsx | 24 + .../photovoltaic/PhotovoltaicDataSummary.tsx | 10 + .../dataFormats/CreateDataFormat.tsx | 4 +- .../components/dataFormats/DataFormat.tsx | 62 +- .../components/dataFormats/DataFormatList.tsx | 24 + .../dataFormats/DataFormatSummary.tsx | 60 + .../dataFormats/DataFormatTable.tsx | 89 - .../dataFormats/PaginatedDataFormats.tsx | 68 + .../dataFormats/UpdateDataFormat.tsx | 8 +- .../components/databases/CreateDatabase.tsx | 56 +- frontend/components/databases/Database.tsx | 73 +- .../components/databases/DatabaseList.tsx | 24 + .../components/databases/DatabaseSummary.tsx | 54 + .../components/databases/DatabaseTable.tsx | 76 - .../databases/PaginatedDatabases.tsx | 62 + .../databases/PendingDatabaseList.tsx | 24 + .../components/databases/PendingDatabases.tsx | 32 - .../components/databases/UpdateDatabase.tsx | 4 +- frontend/components/entities/EntityItem.tsx | 10 + frontend/components/entities/EntityLink.tsx | 31 + frontend/components/entities/EntityList.tsx | 57 + .../components/entities/EntitySummary.tsx | 110 + .../components/entities/PaginatedEntities.tsx | 341 +++ .../filtering/BaseFilterSubform.tsx | 56 + .../filtering/EnumFilterSubform.tsx | 61 + .../components/filtering/FilterSubform.tsx | 67 + .../filtering/FloatFilterSubform.tsx | 78 + .../components/filtering/IntFilterSubform.tsx | 77 + .../filtering/ListFilterSubform.tsx | 35 + .../filtering/ObjectFilterSubform.tsx | 58 + .../filtering/StringFilterSubform.tsx | 52 + .../components/filtering/UrlFilterSubform.tsx | 50 + .../filtering/UuidFilterSubform.tsx | 40 + .../AddGnuPgKeyFingerprint.tsx | 2 +- .../GnuPgKeyFingerprintList.tsx | 24 + .../GnuPgKeyFingerprintSummary.tsx | 68 + .../GnuPgKeyFingerprintTable.tsx | 85 - .../PaginatedGnuPgKeyFingerprints.tsx | 67 + .../AddInstitutionRepresentative.tsx | 74 +- .../institutions/CreateInstitution.tsx | 3 +- .../institutions/DeleteInstitution.tsx | 7 +- .../components/institutions/Institution.tsx | 422 ++-- .../institutions/InstitutionIdSelect.tsx | 17 + .../institutions/InstitutionList.tsx | 24 + .../institutions/InstitutionSummary.tsx | 113 + .../institutions/InstitutionTable.tsx | 55 - .../institutions/PaginatedInstitutions.tsx | 184 ++ .../institutions/PendingInstitutionList.tsx | 24 + .../institutions/PendingInstitutions.tsx | 32 - .../RemoveInstitutionRepresentative.tsx | 2 +- .../institutions/UpdateInstitution.tsx | 4 +- frontend/components/me/ChangeUserEmail.tsx | 1 + frontend/components/me/ManageLayout.tsx | 43 +- frontend/components/me/SetUserPhoneNumber.tsx | 1 + .../methods/AddInstitutionMethodDeveloper.tsx | 39 +- .../methods/AddUserMethodDeveloper.tsx | 39 +- .../methods/AppliedMethodViewer.tsx | 78 + frontend/components/methods/CreateMethod.tsx | 32 +- frontend/components/methods/Method.tsx | 220 +- frontend/components/methods/MethodList.tsx | 24 + .../methods/MethodParametersSubform.tsx | 108 + .../methods/MethodSourcesSubform.tsx | 75 + frontend/components/methods/MethodSummary.tsx | 163 ++ frontend/components/methods/MethodTable.tsx | 83 - .../components/methods/PaginatedMethods.tsx | 96 + .../RemoveInstitutionMethodDeveloper.tsx | 2 +- .../methods/RemoveUserMethodDeveloper.tsx | 2 +- frontend/components/methods/UpdateMethod.tsx | 54 +- .../CreateOpenIdConnectApplication.tsx | 3 +- .../DeleteOpenIdConnectApplication.tsx | 7 +- .../applications/OpenIdConnectApplication.tsx | 93 +- .../OpenIdConnectApplicationList.tsx | 24 + .../OpenIdConnectApplicationSummary.tsx | 115 + .../OpenIdConnectApplicationTable.tsx | 60 - .../PaginatedOpenIdConnectApplications.tsx | 62 + ...etOpenIdConnectApplicationClientSecret.tsx | 18 +- .../UpdateOpenIdConnectApplication.tsx | 6 +- .../DeleteOpenIdConnectAuthorization.tsx | 7 +- .../tokens/OpenIdConnectTokenTable.tsx | 4 +- .../tokens/RevokeOpenIdConnectToken.tsx | 10 +- frontend/components/sorting/SortSubform.tsx | 35 + frontend/components/users/AddUserRole.tsx | 55 +- frontend/components/users/DeleteUser.tsx | 7 +- frontend/components/users/PaginatedUsers.tsx | 77 + frontend/components/users/User.tsx | 173 +- frontend/components/users/UserIdSelect.tsx | 17 + frontend/components/users/UserList.tsx | 24 + frontend/components/users/UserRoleTag.tsx | 6 +- frontend/components/users/UserSummary.tsx | 102 + frontend/{jest.config.js => jest.config.mjs} | 2 +- frontend/lib/apollo.ts | 95 + frontend/lib/array.ts | 30 +- frontend/lib/assert.ts | 3 + frontend/lib/connection.ts | 12 + frontend/lib/debug.ts | 7 + frontend/lib/filter.ts | 480 ++++ frontend/lib/form.ts | 5 + frontend/lib/freeTextFilter.tsx | 107 - frontend/lib/hooks/useDebounce.ts | 26 + frontend/lib/hooks/useInfiniteScrollQuery.ts | 66 + frontend/lib/hooks/usePaginatedQuery.ts | 145 ++ frontend/lib/hooks/useRequireAuth.ts | 12 +- frontend/lib/recoveryCodesModal.tsx | 2 +- frontend/lib/sort.ts | 36 + frontend/lib/string.ts | 69 + frontend/lib/table.tsx | 685 ----- frontend/package.json | 3 +- frontend/pages/_app.tsx | 1 + frontend/pages/components/index.tsx | 24 +- frontend/pages/data-formats/index.tsx | 26 +- frontend/pages/data/calorimetric.tsx | 343 +-- frontend/pages/data/geometric.tsx | 279 +- frontend/pages/data/hygrothermal.tsx | 265 +- frontend/pages/data/index.tsx | 32 +- frontend/pages/data/life-cycle.tsx | 265 +- frontend/pages/data/optical.tsx | 473 +--- frontend/pages/data/photovoltaic.tsx | 266 +- .../[uuid]/data/calorimetric/[dataId].tsx | 22 + .../[uuid]/data/geometric/[dataId].tsx | 22 + .../[uuid]/data/hygrothermal/[dataId].tsx | 22 + .../[uuid]/data/life-cycle/[dataId].tsx | 22 + .../[uuid]/data/optical/[dataId].tsx | 22 + .../[uuid]/data/photovoltaic/[dataId].tsx | 22 + frontend/pages/databases/index.tsx | 99 +- frontend/pages/index.tsx | 615 ++--- frontend/pages/institutions/index.tsx | 41 +- frontend/pages/me/manage/email.tsx | 2 +- .../pages/me/manage/enable-authenticator.tsx | 8 +- frontend/pages/me/manage/personal-data.tsx | 2 +- frontend/pages/me/manage/profile.tsx | 2 +- frontend/pages/me/manage/set-password.tsx | 2 +- .../me/manage/two-factor-authentication.tsx | 2 +- frontend/pages/methods/index.tsx | 24 +- frontend/pages/open-id-connect/index.tsx | 16 +- ...our-inbox-after-password-reset-request.tsx | 2 +- .../check-your-inbox-after-registration.tsx | 2 +- frontend/pages/users/confirm-email-change.tsx | 2 +- frontend/pages/users/confirm-email.tsx | 2 +- frontend/pages/users/index.tsx | 50 +- frontend/pages/users/login/index.tsx | 1 + .../pages/users/login/with-recovery-code.tsx | 2 +- .../users/login/with-two-factor-code.tsx | 2 +- frontend/pages/users/register.tsx | 6 +- frontend/paths.ts | 107 +- frontend/queries/common.graphql | 34 + frontend/queries/components.graphql | 96 +- frontend/queries/currentUser.graphql | 1 + frontend/queries/data.graphql | 356 ++- frontend/queries/dataFormats.graphql | 72 +- frontend/queries/databases.graphql | 50 +- frontend/queries/gnuPgKeyFingerprints.graphql | 68 + frontend/queries/institutions.graphql | 115 +- frontend/queries/methods.graphql | 146 +- frontend/queries/openIdConnect.graphql | 88 +- frontend/queries/users.graphql | 70 +- frontend/styles/global.css | 12 +- frontend/type-defs.graphqls | 1595 +++++++----- frontend/yarn.lock | 19 +- nginx/templates/default.conf.template | 23 +- 611 files changed, 23865 insertions(+), 11183 deletions(-) create mode 100644 backend/src/Data/Association.cs create mode 100644 backend/src/Data/AuditableAssociation.cs create mode 100644 backend/src/Data/AuditableEntity.cs create mode 100644 backend/src/Data/DataCopnstants.cs create mode 100644 backend/src/Data/IAssociation.cs create mode 100644 backend/src/Data/IAuditable.cs create mode 100644 backend/src/Data/INamed.cs create mode 100644 backend/src/Extensions/LinqExtensions.cs create mode 100644 backend/src/GraphQl/Associations/AssociationType.cs create mode 100644 backend/src/GraphQl/Associations/AuditableAssociationFilterType.cs create mode 100644 backend/src/GraphQl/Associations/AuditableAssociationSortType.cs create mode 100644 backend/src/GraphQl/AuthorizedPaginatedConnection.cs create mode 100644 backend/src/GraphQl/CalorimetricDataX/CalorimetricData.cs rename backend/src/GraphQl/{DataX => CalorimetricDataX}/CalorimetricDataConnection.cs (80%) rename backend/src/GraphQl/{DataX => CalorimetricDataX}/CalorimetricDataEdge.cs (65%) rename backend/src/GraphQl/{DataX => CalorimetricDataX}/CalorimetricDataPropositionInput.cs (84%) create mode 100644 backend/src/GraphQl/CalorimetricDataX/CalorimetricDataQueries.cs create mode 100644 backend/src/GraphQl/ComponentGeneralizations/ComponentConcretizationAndGeneralizationSortType.cs create mode 100644 backend/src/GraphQl/ComponentVariants/ComponentVariantSortType.cs create mode 100644 backend/src/GraphQl/Components/ComponentAssembledOfSortType.cs delete mode 100644 backend/src/GraphQl/Components/ComponentByIdDataLoader.cs create mode 100644 backend/src/GraphQl/Components/ComponentConcretizationOfSortType.cs delete mode 100644 backend/src/GraphQl/Components/ComponentConcretizationsByComponentIdDataLoader.cs create mode 100644 backend/src/GraphQl/Components/ComponentDataLoaders.cs create mode 100644 backend/src/GraphQl/Components/ComponentGeneralizationOfSortType.cs delete mode 100644 backend/src/GraphQl/Components/ComponentGeneralizationsByComponentIdDataLoader.cs create mode 100644 backend/src/GraphQl/Components/ComponentManufacturerSortType.cs delete mode 100644 backend/src/GraphQl/Components/ComponentManufacturersByComponentIdDataLoader.cs delete mode 100644 backend/src/GraphQl/Components/ComponentPartOfByComponentIdDataLoader.cs create mode 100644 backend/src/GraphQl/Components/ComponentPartOfSortType.cs delete mode 100644 backend/src/GraphQl/Components/ComponentPartsByComponentIdDataLoader.cs delete mode 100644 backend/src/GraphQl/Components/ComponentVariantOfByComponentIdDataLoader.cs create mode 100644 backend/src/GraphQl/Components/ComponentVariantOfSortType.cs delete mode 100644 backend/src/GraphQl/Components/PendingComponentManufacturersByComponentIdDataLoader.cs delete mode 100644 backend/src/GraphQl/DataFormats/DataFormatByIdDataLoader.cs create mode 100644 backend/src/GraphQl/DataFormats/DataFormatDataLoaders.cs create mode 100644 backend/src/GraphQl/DataLoaders.cs delete mode 100644 backend/src/GraphQl/DataX/CalorimetricData.cs delete mode 100644 backend/src/GraphQl/DataX/GeometricData.cs delete mode 100644 backend/src/GraphQl/DataX/HygrothermalData.cs delete mode 100644 backend/src/GraphQl/DataX/LifeCycleData.cs delete mode 100644 backend/src/GraphQl/DataX/OpticalData.cs delete mode 100644 backend/src/GraphQl/DataX/PhotovoltaicData.cs delete mode 100644 backend/src/GraphQl/Databases/DatabaseByIdDataLoader.cs create mode 100644 backend/src/GraphQl/Databases/DatabaseDataLoaders.cs delete mode 100644 backend/src/GraphQl/Entities/AssociationsByAssociateIdDataLoader.cs rename backend/src/GraphQl/Entities/{EntityFilterType.cs => AuditableEntityFilterType.cs} (63%) rename backend/src/GraphQl/Entities/{EntitySortType.cs => AuditableEntitySortType.cs} (58%) delete mode 100644 backend/src/GraphQl/Entities/EntityByIdDataLoader.cs create mode 100644 backend/src/GraphQl/Extensions/PageExtensions.cs delete mode 100644 backend/src/GraphQl/Extensions/SortingContextExtensions.cs create mode 100644 backend/src/GraphQl/GeometricDataX/GeometricData.cs rename backend/src/GraphQl/{DataX => GeometricDataX}/GeometricDataConnection.cs (80%) rename backend/src/GraphQl/{DataX => GeometricDataX}/GeometricDataEdge.cs (67%) rename backend/src/GraphQl/{DataX => GeometricDataX}/GeometricDataPropositionInput.cs (83%) create mode 100644 backend/src/GraphQl/GeometricDataX/GeometricDataQueries.cs delete mode 100644 backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintByFingerprintDataLoader.cs delete mode 100644 backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintByIdDataLoader.cs create mode 100644 backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintDataLoaders.cs create mode 100644 backend/src/GraphQl/GraphQlThrowHelper.cs create mode 100644 backend/src/GraphQl/GraphQlTypeResources.cs create mode 100644 backend/src/GraphQl/HygrothermalDataX/HygrothermalData.cs rename backend/src/GraphQl/{DataX => HygrothermalDataX}/HygrothermalDataConnection.cs (80%) rename backend/src/GraphQl/{DataX => HygrothermalDataX}/HygrothermalDataEdge.cs (65%) rename backend/src/GraphQl/{DataX => HygrothermalDataX}/HygrothermalDataPropositionInput.cs (81%) create mode 100644 backend/src/GraphQl/HygrothermalDataX/HygrothermalDataQueries.cs delete mode 100644 backend/src/GraphQl/Institutions/GnuPgKeyFingerprintsByInstitutionIdDataLoader.cs delete mode 100644 backend/src/GraphQl/Institutions/InstitutionByIdDataLoader.cs create mode 100644 backend/src/GraphQl/Institutions/InstitutionDataLoaders.cs create mode 100644 backend/src/GraphQl/Institutions/InstitutionDevelopedMethodSortType.cs delete mode 100644 backend/src/GraphQl/Institutions/InstitutionDevelopedMethodsByInstitutionIdDataLoader.cs create mode 100644 backend/src/GraphQl/Institutions/InstitutionGnuPgKeyFingerprintSortType.cs create mode 100644 backend/src/GraphQl/Institutions/InstitutionManagedComponentSortType.cs delete mode 100644 backend/src/GraphQl/Institutions/InstitutionManagedComponentsByInstitutionIdDataLoader.cs create mode 100644 backend/src/GraphQl/Institutions/InstitutionManagedDataFormatSortType.cs delete mode 100644 backend/src/GraphQl/Institutions/InstitutionManagedDataFormatsByInstitutionIdDataLoader.cs create mode 100644 backend/src/GraphQl/Institutions/InstitutionManagedInstitutionSortType.cs delete mode 100644 backend/src/GraphQl/Institutions/InstitutionManagedInstitutionsByInstitutionIdDataLoader.cs create mode 100644 backend/src/GraphQl/Institutions/InstitutionManagedMethodSortType.cs delete mode 100644 backend/src/GraphQl/Institutions/InstitutionManagedMethodsByInstitutionIdDataLoader.cs delete mode 100644 backend/src/GraphQl/Institutions/InstitutionManufacturedComponentByInstitutionIdDataLoader.cs create mode 100644 backend/src/GraphQl/Institutions/InstitutionManufacturedComponentSortType.cs create mode 100644 backend/src/GraphQl/Institutions/InstitutionOperatedDatabaseSortType.cs delete mode 100644 backend/src/GraphQl/Institutions/InstitutionOperatedDatabasesByInstitutionIdDataLoader.cs create mode 100644 backend/src/GraphQl/Institutions/InstitutionOwnedOpenIdConnectApplicationSortType.cs delete mode 100644 backend/src/GraphQl/Institutions/InstitutionOwnedOpenIdConnectApplicationsByInstitutionIdDataLoader.cs create mode 100644 backend/src/GraphQl/Institutions/InstitutionRepresentativeSortType.cs delete mode 100644 backend/src/GraphQl/Institutions/InstitutionRepresentativesByInstitutionIdDataLoader.cs delete mode 100644 backend/src/GraphQl/Institutions/PendingInstitutionDevelopedMethodsByInstitutionIdDataLoader.cs delete mode 100644 backend/src/GraphQl/Institutions/PendingInstitutionManufacturedComponentsByInstitutionIdDataLoader.cs delete mode 100644 backend/src/GraphQl/Institutions/PendingInstitutionRepresentativesByInstitutionIdDataLoader.cs create mode 100644 backend/src/GraphQl/LifeCycleDataX/LifeCycleData.cs rename backend/src/GraphQl/{DataX => LifeCycleDataX}/LifeCycleDataConnection.cs (80%) rename backend/src/GraphQl/{DataX => LifeCycleDataX}/LifeCycleDataEdge.cs (65%) rename backend/src/GraphQl/{DataX => LifeCycleDataX}/LifeCycleDataPropositionInput.cs (81%) create mode 100644 backend/src/GraphQl/LifeCycleDataX/LifeCycleDataQueries.cs delete mode 100644 backend/src/GraphQl/Methods/InstitutionMethodDevelopersByMethodIdDataLoader.cs delete mode 100644 backend/src/GraphQl/Methods/MethodByIdDataLoader.cs create mode 100644 backend/src/GraphQl/Methods/MethodDataLoaders.cs create mode 100644 backend/src/GraphQl/Methods/MethodDeveloperSortType.cs delete mode 100644 backend/src/GraphQl/Methods/PendingInstitutionMethodDevelopersByMethodIdDataLoader.cs delete mode 100644 backend/src/GraphQl/Methods/PendingUserMethodDevelopersByMethodIdDataLoader.cs delete mode 100644 backend/src/GraphQl/Methods/UserMethodDevelopersByMethodIdDataLoader.cs delete mode 100644 backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationByIdDataLoader.cs create mode 100644 backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationDataLoaders.cs rename backend/src/GraphQl/OpenIdConnect/Applications/{OpenIdConnectApplicationAuthorizationConnection.cs => OpenIdConnectApplicationGrantedAuthorizationConnection.cs} (82%) rename backend/src/GraphQl/OpenIdConnect/Applications/{OpenIdConnectApplicationTokenConnection.cs => OpenIdConnectApplicationIssuedTokenConnection.cs} (83%) create mode 100644 backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationSortType.cs delete mode 100644 backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationByIdDataLoader.cs create mode 100644 backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationDataLoaders.cs rename backend/src/GraphQl/OpenIdConnect/Authorizations/{OpenIdConnectAuthorizationTokenConnection.cs => OpenIdConnectAuthorizationIssuedTokenConnection.cs} (71%) create mode 100644 backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationSortType.cs rename backend/src/GraphQl/OpenIdConnect/{Applications => }/OpenIdConnectScope.cs (87%) rename backend/src/GraphQl/OpenIdConnect/{Applications => }/OpenIdConnectScopeExtensions.cs (70%) delete mode 100644 backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenByIdDataLoader.cs create mode 100644 backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenDataLoaders.cs create mode 100644 backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenSortType.cs rename backend/src/GraphQl/{DataX => OpticalDataX}/OpticalComponentSubtype.cs (92%) rename backend/src/GraphQl/{DataX => OpticalDataX}/OpticalComponentSubtypePropositionInput.cs (86%) rename backend/src/GraphQl/{DataX => OpticalDataX}/OpticalComponentType.cs (77%) rename backend/src/GraphQl/{DataX => OpticalDataX}/OpticalComponentTypePropositionInput.cs (86%) create mode 100644 backend/src/GraphQl/OpticalDataX/OpticalData.cs rename backend/src/GraphQl/{DataX => OpticalDataX}/OpticalDataConnection.cs (82%) rename backend/src/GraphQl/{DataX => OpticalDataX}/OpticalDataEdge.cs (65%) rename backend/src/GraphQl/{DataX => OpticalDataX}/OpticalDataPropositionInput.cs (92%) create mode 100644 backend/src/GraphQl/OpticalDataX/OpticalDataQueries.cs create mode 100644 backend/src/GraphQl/PaginatedConnection.cs create mode 100644 backend/src/GraphQl/PaginatedEdge.cs create mode 100644 backend/src/GraphQl/PaginationHelpers.cs create mode 100644 backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicData.cs rename backend/src/GraphQl/{DataX => PhotovoltaicDataX}/PhotovoltaicDataConnection.cs (81%) rename backend/src/GraphQl/{DataX => PhotovoltaicDataX}/PhotovoltaicDataEdge.cs (65%) rename backend/src/GraphQl/{DataX => PhotovoltaicDataX}/PhotovoltaicDataPropositionInput.cs (81%) create mode 100644 backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataQueries.cs create mode 100644 backend/src/GraphQl/Requests/DataQueries.cs create mode 100644 backend/src/GraphQl/Requests/GraphQlRequestHelper.cs rename backend/src/{Services => GraphQl/Requests}/QueryingDatabases.cs (99%) rename backend/src/GraphQl/{ => Scalars}/LocaleType.cs (98%) create mode 100644 backend/src/GraphQl/Scalars/MyUriType.cs create mode 100644 backend/src/GraphQl/Scalars/NonNegativeIntType.cs create mode 100644 backend/src/GraphQl/Sorting.cs delete mode 100644 backend/src/GraphQl/Users/GnuPgKeyFingerprintsByUserIdDataLoader.cs delete mode 100644 backend/src/GraphQl/Users/PendingUserDevelopedMethodsByUserIdDataLoader.cs delete mode 100644 backend/src/GraphQl/Users/PendingUserRepresentedInstitutionsByUserIdDataLoader.cs delete mode 100644 backend/src/GraphQl/Users/UserByIdDataLoader.cs create mode 100644 backend/src/GraphQl/Users/UserDataLoaders.cs create mode 100644 backend/src/GraphQl/Users/UserDevelopedMethodSortType.cs delete mode 100644 backend/src/GraphQl/Users/UserDevelopedMethodsByUserIdDataLoader.cs create mode 100644 backend/src/GraphQl/Users/UserGnuPgKeyFingerprintSortType.cs create mode 100644 backend/src/GraphQl/Users/UserRepresentedInstitutionSortType.cs delete mode 100644 backend/src/GraphQl/Users/UserRepresentedInstitutionsByUserIdDataLoader.cs create mode 100644 backend/src/Migrations/20260328153447_CorrectExistsFlagsOfReferencesSecondAttempt.Designer.cs create mode 100644 backend/src/Migrations/20260402204629_AddUpdatedAndCreatedAtTimestamps.Designer.cs create mode 100644 backend/src/Migrations/20260402204629_AddUpdatedAndCreatedAtTimestamps.cs create mode 100644 backend/src/Migrations/migrate_from_20260328153447_CorrectExistsFlagsOfReferencesSecondAttempt_to_20260402204629_AddUpdatedAndCreatedAtTimestamps.sql create mode 100644 backend/src/Migrations/rollback_from_20260402204629_AddUpdatedAndCreatedAtTimestamps_to_20260328153447_CorrectExistsFlagsOfReferencesSecondAttempt.sql create mode 100644 backend/test/AuditableTests.cs create mode 100644 frontend/components/ActiveFilterAndSortBar.tsx delete mode 100644 frontend/components/Availability.tsx create mode 100644 frontend/components/CielabColorViewer.tsx create mode 100644 frontend/components/CodeViewer.tsx create mode 100644 frontend/components/CopyButton.tsx create mode 100644 frontend/components/DeleteButton.tsx create mode 100644 frontend/components/EditButton.tsx create mode 100644 frontend/components/EnumTag.tsx create mode 100644 frontend/components/Float.tsx delete mode 100644 frontend/components/FloatPropositionFormList.tsx create mode 100644 frontend/components/Iconize.tsx create mode 100644 frontend/components/IdentifierItem.tsx create mode 100644 frontend/components/InlineList.tsx create mode 100644 frontend/components/JumpToId.tsx create mode 100644 frontend/components/LazyTabs.tsx delete mode 100644 frontend/components/PageHeader.tsx create mode 100644 frontend/components/PaginatedIdSelect.tsx create mode 100644 frontend/components/Pagination.tsx create mode 100644 frontend/components/QueryToolbar.tsx rename frontend/components/{ReferenceForm.tsx => ReferenceSubform.tsx} (82%) delete mode 100644 frontend/components/SelectComponentId.tsx delete mode 100644 frontend/components/SelectInstitutionId.tsx delete mode 100644 frontend/components/SelectUserId.tsx create mode 100644 frontend/components/SlideDown.tsx create mode 100644 frontend/components/UnauthenticatedResult.tsx create mode 100644 frontend/components/UrlInput.tsx create mode 100644 frontend/components/UuidFormItem.tsx delete mode 100644 frontend/components/UuidPropositionFormList.tsx create mode 100644 frontend/components/components/ComponentIdSelect.tsx create mode 100644 frontend/components/components/ComponentList.tsx create mode 100644 frontend/components/components/ComponentSummary.tsx delete mode 100644 frontend/components/components/ComponentTable.tsx create mode 100644 frontend/components/components/PaginatedComponents.tsx create mode 100644 frontend/components/data/DataSummary.tsx create mode 100644 frontend/components/data/calorimetric/CalorimetricData.tsx create mode 100644 frontend/components/data/calorimetric/CalorimetricDataList.tsx create mode 100644 frontend/components/data/calorimetric/CalorimetricDataSummary.tsx create mode 100644 frontend/components/data/calorimetric/PaginatedCalorimetricData.tsx create mode 100644 frontend/components/data/geometric/GeometricData.tsx create mode 100644 frontend/components/data/geometric/GeometricDataList.tsx create mode 100644 frontend/components/data/geometric/GeometricDataSummary.tsx create mode 100644 frontend/components/data/geometric/PaginatedGeometricData.tsx create mode 100644 frontend/components/data/hygrothermal/HygrothermalData.tsx create mode 100644 frontend/components/data/hygrothermal/HygrothermalDataList.tsx create mode 100644 frontend/components/data/hygrothermal/HygrothermalDataSummary.tsx create mode 100644 frontend/components/data/hygrothermal/PaginatedHygrothermalData.tsx create mode 100644 frontend/components/data/lifeCycle/LifeCycleData.tsx create mode 100644 frontend/components/data/lifeCycle/LifeCycleDataList.tsx create mode 100644 frontend/components/data/lifeCycle/LifeCycleDataSummary.tsx create mode 100644 frontend/components/data/lifeCycle/PaginatedLifeCycleData.tsx create mode 100644 frontend/components/data/optical/OpticalData.tsx create mode 100644 frontend/components/data/optical/OpticalDataList.tsx create mode 100644 frontend/components/data/optical/OpticalDataRibbon.tsx create mode 100644 frontend/components/data/optical/OpticalDataSummary.tsx create mode 100644 frontend/components/data/optical/PaginatedOpticalData.tsx create mode 100644 frontend/components/data/photovoltaic/PaginatedPhotovoltaicData.tsx create mode 100644 frontend/components/data/photovoltaic/PhotovoltaicData.tsx create mode 100644 frontend/components/data/photovoltaic/PhotovoltaicDataList.tsx create mode 100644 frontend/components/data/photovoltaic/PhotovoltaicDataSummary.tsx create mode 100644 frontend/components/dataFormats/DataFormatList.tsx create mode 100644 frontend/components/dataFormats/DataFormatSummary.tsx delete mode 100644 frontend/components/dataFormats/DataFormatTable.tsx create mode 100644 frontend/components/dataFormats/PaginatedDataFormats.tsx create mode 100644 frontend/components/databases/DatabaseList.tsx create mode 100644 frontend/components/databases/DatabaseSummary.tsx delete mode 100644 frontend/components/databases/DatabaseTable.tsx create mode 100644 frontend/components/databases/PaginatedDatabases.tsx create mode 100644 frontend/components/databases/PendingDatabaseList.tsx delete mode 100644 frontend/components/databases/PendingDatabases.tsx create mode 100644 frontend/components/entities/EntityItem.tsx create mode 100644 frontend/components/entities/EntityLink.tsx create mode 100644 frontend/components/entities/EntityList.tsx create mode 100644 frontend/components/entities/EntitySummary.tsx create mode 100644 frontend/components/entities/PaginatedEntities.tsx create mode 100644 frontend/components/filtering/BaseFilterSubform.tsx create mode 100644 frontend/components/filtering/EnumFilterSubform.tsx create mode 100644 frontend/components/filtering/FilterSubform.tsx create mode 100644 frontend/components/filtering/FloatFilterSubform.tsx create mode 100644 frontend/components/filtering/IntFilterSubform.tsx create mode 100644 frontend/components/filtering/ListFilterSubform.tsx create mode 100644 frontend/components/filtering/ObjectFilterSubform.tsx create mode 100644 frontend/components/filtering/StringFilterSubform.tsx create mode 100644 frontend/components/filtering/UrlFilterSubform.tsx create mode 100644 frontend/components/filtering/UuidFilterSubform.tsx create mode 100644 frontend/components/gnuPgKeyFingerprints/GnuPgKeyFingerprintList.tsx create mode 100644 frontend/components/gnuPgKeyFingerprints/GnuPgKeyFingerprintSummary.tsx delete mode 100644 frontend/components/gnuPgKeyFingerprints/GnuPgKeyFingerprintTable.tsx create mode 100644 frontend/components/gnuPgKeyFingerprints/PaginatedGnuPgKeyFingerprints.tsx create mode 100644 frontend/components/institutions/InstitutionIdSelect.tsx create mode 100644 frontend/components/institutions/InstitutionList.tsx create mode 100644 frontend/components/institutions/InstitutionSummary.tsx delete mode 100644 frontend/components/institutions/InstitutionTable.tsx create mode 100644 frontend/components/institutions/PaginatedInstitutions.tsx create mode 100644 frontend/components/institutions/PendingInstitutionList.tsx delete mode 100644 frontend/components/institutions/PendingInstitutions.tsx create mode 100644 frontend/components/methods/AppliedMethodViewer.tsx create mode 100644 frontend/components/methods/MethodList.tsx create mode 100644 frontend/components/methods/MethodParametersSubform.tsx create mode 100644 frontend/components/methods/MethodSourcesSubform.tsx create mode 100644 frontend/components/methods/MethodSummary.tsx delete mode 100644 frontend/components/methods/MethodTable.tsx create mode 100644 frontend/components/methods/PaginatedMethods.tsx create mode 100644 frontend/components/openIdConnect/applications/OpenIdConnectApplicationList.tsx create mode 100644 frontend/components/openIdConnect/applications/OpenIdConnectApplicationSummary.tsx delete mode 100644 frontend/components/openIdConnect/applications/OpenIdConnectApplicationTable.tsx create mode 100644 frontend/components/openIdConnect/applications/PaginatedOpenIdConnectApplications.tsx create mode 100644 frontend/components/sorting/SortSubform.tsx create mode 100644 frontend/components/users/PaginatedUsers.tsx create mode 100644 frontend/components/users/UserIdSelect.tsx create mode 100644 frontend/components/users/UserList.tsx create mode 100644 frontend/components/users/UserSummary.tsx rename frontend/{jest.config.js => jest.config.mjs} (95%) create mode 100644 frontend/lib/assert.ts create mode 100644 frontend/lib/connection.ts create mode 100644 frontend/lib/debug.ts create mode 100644 frontend/lib/filter.ts delete mode 100644 frontend/lib/freeTextFilter.tsx create mode 100644 frontend/lib/hooks/useDebounce.ts create mode 100644 frontend/lib/hooks/useInfiniteScrollQuery.ts create mode 100644 frontend/lib/hooks/usePaginatedQuery.ts create mode 100644 frontend/lib/sort.ts create mode 100644 frontend/lib/string.ts delete mode 100644 frontend/lib/table.tsx create mode 100644 frontend/pages/databases/[uuid]/data/calorimetric/[dataId].tsx create mode 100644 frontend/pages/databases/[uuid]/data/geometric/[dataId].tsx create mode 100644 frontend/pages/databases/[uuid]/data/hygrothermal/[dataId].tsx create mode 100644 frontend/pages/databases/[uuid]/data/life-cycle/[dataId].tsx create mode 100644 frontend/pages/databases/[uuid]/data/optical/[dataId].tsx create mode 100644 frontend/pages/databases/[uuid]/data/photovoltaic/[dataId].tsx create mode 100644 frontend/queries/common.graphql 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/dotnet-tools.json b/backend/dotnet-tools.json index 6761c3045..5695b5050 100644 --- a/backend/dotnet-tools.json +++ b/backend/dotnet-tools.json @@ -24,7 +24,7 @@ "rollForward": false }, "dotnet-ef": { - "version": "10.0.5", + "version": "10.0.7", "commands": [ "dotnet-ef" ], @@ -66,7 +66,7 @@ "rollForward": false }, "jetbrains.resharper.globaltools": { - "version": "2025.3.3", + "version": "2026.1.1", "commands": [ "jb" ], diff --git a/backend/src/Authentication/AuthenticationHandler.cs b/backend/src/Authentication/AuthenticationHandler.cs index 88924d810..db9e1766e 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; diff --git a/backend/src/Authorization/CommonAuthorization.cs b/backend/src/Authorization/CommonAuthorization.cs index 21b36d645..e8147f87f 100644 --- a/backend/src/Authorization/CommonAuthorization.cs +++ b/backend/src/Authorization/CommonAuthorization.cs @@ -21,14 +21,42 @@ public abstract class CommonAuthorization( IDbContextFactory dbContextFactory, UserManager userManager, OpenIddictApplicationManager applicationManager - ) +) +: IDisposable, IAsyncDisposable { - protected ApplicationDbContext Context { get => dbContextFactory.CreateDbContext(); } + 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); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + Context.Dispose(); + } + } + + protected virtual ValueTask DisposeAsyncCore() + { + return Context.DisposeAsync(); + } + public async Task SwitchUserOrApplicationAsync( ClaimsPrincipal claimsPrincipal, Func> handleUser, diff --git a/backend/src/Authorization/OpenIdConnectAuthorization.cs b/backend/src/Authorization/OpenIdConnectAuthorization.cs index 859b4f67a..ed82ab89e 100644 --- a/backend/src/Authorization/OpenIdConnectAuthorization.cs +++ b/backend/src/Authorization/OpenIdConnectAuthorization.cs @@ -14,7 +14,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 +76,6 @@ CancellationToken cancellationToken internal async Task IsAuthorizedToManageAuthorization( ClaimsPrincipal claimsPrincipal, Guid authorizationId, - OpenIddictAuthorizationManager authorizationManager, CancellationToken cancellationToken ) { @@ -97,17 +98,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 ) { diff --git a/backend/src/Configuration/AuthConfiguration.cs b/backend/src/Configuration/AuthConfiguration.cs index 37ff2b1d9..c3cff0a9f 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; @@ -35,7 +36,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 @@ -55,7 +56,8 @@ private static void BootstrapCertificates() { store.Add( JwtSigningAndEncryptionCertificateRotationJob.CreateSigningCertificate( - distinguishedName + distinguishedName, + clock ) ); } @@ -74,7 +76,8 @@ private static void BootstrapCertificates() { store.Add( JwtSigningAndEncryptionCertificateRotationJob.CreateEncryptionCertificate( - distinguishedName + distinguishedName, + clock ) ); } @@ -113,10 +116,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); diff --git a/backend/src/Configuration/GraphQlConfiguration.cs b/backend/src/Configuration/GraphQlConfiguration.cs index e9962bcc3..19685c0d7 100644 --- a/backend/src/Configuration/GraphQlConfiguration.cs +++ b/backend/src/Configuration/GraphQlConfiguration.cs @@ -1,4 +1,5 @@ using System; +using HotChocolate.AspNetCore; using HotChocolate.Configuration; using HotChocolate.Data; using HotChocolate.Data.Filters; @@ -8,14 +9,16 @@ 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; @@ -32,8 +35,7 @@ IWebHostEnvironment environment { // Automatic-Persisted-Queries Services services - .AddMemoryCache() - .AddSha256DocumentHashProvider(HashFormat.Hex); // https://chillicream.com/docs/hotchocolate/v15/security/#fips-compliance + .AddMemoryCache(); // GraphQL Server var serverBuilder = services .AddGraphQLServer(); @@ -43,59 +45,83 @@ 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() .AddMutationConventions(new MutationConventionOptions { ApplyToAllMutations = false }) // Extensions - .AddProjections() + .AddNodaTime() + // .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; + _.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.None; + _.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`. + // options.QueryCacheSize = ...; + // options.UseComplexityMultipliers = ...; + // options.EnableSchemaFileSupport = true; + } + ) + .ModifyCostOptions(_ => + { + _.MaxFieldCost = 10000; + _.MaxTypeCost = 10000; } ) // Configure @@ -108,11 +134,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,24 +146,12 @@ 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() - // Register converters between NodaTime's `OffsetDateTime` and .NET's - // `DateTimeOffset` to reuse the existing `DateTimeType` - // https://chillicream.com/docs/hotchocolate/v15/defining-a-schema/scalars#custom-converters - .BindRuntimeType() - .AddTypeConverter( - _ => _.ToDateTimeOffset() - ) - .AddTypeConverter( - _ => OffsetDateTime.FromDateTimeOffset(_) - ) + .BindRuntimeType() // Object Types .AddType() // Query, Mutation, Subscription, Object, and Input Types @@ -155,8 +164,8 @@ await httpContext.RequestServices .AddDbContextCursorPagingProvider() .ModifyPagingOptions(_ => { - _.MaxPageSize = int.MaxValue - 1; - _.DefaultPageSize = 100; + _.MaxPageSize = (int)GraphQlConstants.MaximumPageSize; + _.DefaultPageSize = (int)GraphQlConstants.MaximumPageSize; _.IncludeTotalCount = true; _.IncludeNodesField = false; _.InferConnectionNameFromField = true; @@ -167,6 +176,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 +195,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,14 +206,14 @@ public override void OnBeforeInitialize(ITypeDiscoveryContext discoveryContext) Console.WriteLine($"[INIT] Discovered type '{discoveryContext.Type.GetType().Name}'"); } - public override void OnBeforeCompleteName(ITypeCompletionContext completionContext, DefinitionBase definition) + public override void OnBeforeCompleteName(ITypeCompletionContext completionContext, TypeSystemConfiguration configuration) { - Console.WriteLine($"[NAME] Finalizing name '{definition.Name}' for type '{completionContext.Type.GetType().Name}'"); + Console.WriteLine($"[NAME] Finalizing name '{configuration.Name}' for type '{completionContext.Type.GetType().Name}'"); } - public override void OnAfterCompleteType(ITypeCompletionContext completionContext, DefinitionBase definition) + public override void OnAfterCompleteType(ITypeCompletionContext completionContext, TypeSystemConfiguration configuration) { - Console.WriteLine($"[DONE] Completed type '{completionContext.Type.GetType().Name}' with name '{definition.Name}'"); + Console.WriteLine($"[DONE] Completed type '{completionContext.Type.GetType().Name}' with name '{configuration.Name}'"); } } @@ -254,7 +251,9 @@ protected override void Configure(IFilterConventionDescriptor descriptor) descriptor.Provider( new QueryableFilterProvider(_ => _ .AddDefaultFieldHandlers() - .AddFieldHandler>() + .AddFieldHandler>(context => + new QueryableComparableInClosedIntervalHandler(context.TypeConverter, context.InputParser) + ) ) ); } @@ -396,16 +395,24 @@ this IFilterConventionDescriptor descriptor .BindRuntimeType() .BindRuntimeType() .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType() .BindRuntimeType() .BindRuntimeType() - // .BindRuntimeType() - // .BindRuntimeType() - // .BindRuntimeType() - // .BindRuntimeType() - .BindRuntimeType() - .BindRuntimeType() - .BindRuntimeType() - .BindRuntimeType(); + .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType() + .BindRuntimeType(); } } diff --git a/backend/src/Controllers/AuthorizationController.cs b/backend/src/Controllers/AuthorizationController.cs index dded00788..a42e2bdd8 100644 --- a/backend/src/Controllers/AuthorizationController.cs +++ b/backend/src/Controllers/AuthorizationController.cs @@ -15,6 +15,7 @@ using Metabase.Authorization; using Metabase.Data; using Metabase.Data.OpenIdConnect; +using Metabase.Extensions; 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,6 +40,7 @@ namespace Metabase.Controllers; public sealed class AuthorizationController( + IClock clock, OpenIddictApplicationManager applicationManager, OpenIddictAuthorizationManager authorizationManager, OpenIddictScopeManager scopeManager, @@ -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 diff --git a/backend/src/Data/ApplicationDbContext.cs b/backend/src/Data/ApplicationDbContext.cs index 8a4b749ba..69ec6da83 100644 --- a/backend/src/Data/ApplicationDbContext.cs +++ b/backend/src/Data/ApplicationDbContext.cs @@ -3,10 +3,13 @@ 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 SchemaNameOptionsExtension = Metabase.Data.Extensions.SchemaNameOptionsExtension; +using Metabase.Extensions; namespace Metabase.Data; @@ -14,11 +17,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 +34,8 @@ public sealed class ApplicationDbContext internal const string StandardizerTypeName = "standardizer"; public ApplicationDbContext( - DbContextOptions options + DbContextOptions options, + IClock clock ) : base(options) { @@ -38,6 +43,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 +120,48 @@ 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(); + foreach (var entry in entries) + { + switch (entry.State) + { + case EntityState.Added: + 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 +169,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"); @@ -386,43 +415,74 @@ 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); + foreach (var entityType in builder.Model.GetEntityTypes()) + { + if (typeof(IEntity).IsAssignableFrom(entityType.ClrType)) + { + var entity = builder.Entity(entityType.ClrType); + // https://www.npgsql.org/efcore/modeling/generated-properties.html#guiduuid-generation + entity + .Property(nameof(IEntity.Id)) + .HasDefaultValueSql("gen_random_uuid()"); + // https://www.npgsql.org/efcore/modeling/concurrency.html#the-postgresql-xmin-system-column + entity + .Property(nameof(IEntity.Version)) + .IsRowVersion(); + } + if (typeof(IAssociation).IsAssignableFrom(entityType.ClrType)) + { + var association = 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(); + } + } } } \ 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..7d19140bd --- /dev/null +++ b/backend/src/Data/AuditableAssociation.cs @@ -0,0 +1,10 @@ +using NodaTime; + +namespace Metabase.Data; + +public abstract class AuditableAssociation +: Association, IAuditable +{ + public OffsetDateTime CreatedAt { get; set; } + public OffsetDateTime UpdatedAt { get; set; } +} diff --git a/backend/src/Data/AuditableEntity.cs b/backend/src/Data/AuditableEntity.cs new file mode 100644 index 000000000..0416283f4 --- /dev/null +++ b/backend/src/Data/AuditableEntity.cs @@ -0,0 +1,21 @@ +using System; +using NodaTime; + +namespace Metabase.Data; + +public abstract class AuditableEntity +: Entity, IAuditable +{ + public AuditableEntity() + : base() + { + } + + public AuditableEntity(Guid id) + : base(id) + { + } + + public OffsetDateTime CreatedAt { get; set; } + public OffsetDateTime UpdatedAt { get; set; } +} 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..7d2870ef9 --- /dev/null +++ b/backend/src/Data/DataCopnstants.cs @@ -0,0 +1,14 @@ +namespace Metabase.Data; + +public static class DataConstants +{ + public const string TestlabSolarFacadesOpenIdConnectClientId = "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 TestlabDatabaseUuid = "8a27aa0d-6026-4124-b185-4efd5cead953"; + public const string IgsdbDatabaseUuid = "48994b60-670d-488d-aaf7-53333a64f1d6"; +} \ 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..6be341dd0 100644 --- a/backend/src/Data/DbSeeder.cs +++ b/backend/src/Data/DbSeeder.cs @@ -60,9 +60,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), @@ -78,13 +75,6 @@ public static readonly (string Name, string EmailAddress, Enumerations.UserRole 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"; - - private const string TestlabDatabaseUuid = "8a27aa0d-6026-4124-b185-4efd5cead953"; - private const string IgsdbDatabaseUuid = "48994b60-670d-488d-aaf7-53333a64f1d6"; - public static async Task DoAsync( IServiceProvider services ) @@ -172,11 +162,11 @@ IWebHostEnvironment environment { 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(); if (iseInstitution is null) { iseInstitution = new Institution( - new Guid(IseInstitutionUuid), + new Guid(DataConstants.IseInstitutionUuid), "Fraunhofer ISE", "ISE", "Fraunhofer Institute for Solar Energy Systems (ISE)", @@ -210,10 +200,10 @@ IWebHostEnvironment environment } if (environment.IsDevelopment()) { - if (!await context.Institutions.Where(x => x.Id == new Guid(TestlabInstitutionUuid)).AnyAsync()) + if (!await context.Institutions.Where(x => x.Id == new Guid(DataConstants.TestlabInstitutionUuid)).AnyAsync()) { var institution = new Institution( - new Guid(TestlabInstitutionUuid), + new Guid(DataConstants.TestlabInstitutionUuid), "TestLab Solar Facades", "TLSF", "This institution represents the TestLab Solar Facades of Fraunhofer ISE", @@ -233,7 +223,7 @@ IWebHostEnvironment environment ManagerId = iseInstitution.Id }; - var application = await manager.FindByClientIdAsync(TestlabSolarFacadesOpenIdConnectClientId).AsTask(); + var application = await manager.FindByClientIdAsync(DataConstants.TestlabSolarFacadesOpenIdConnectClientId).AsTask(); if (application is not null) { institution.OpenIdConnectApplications.Add(application); @@ -241,10 +231,10 @@ IWebHostEnvironment environment context.Institutions.Add(institution); await context.SaveChangesAsync(); } - if (!await context.Institutions.Where(x => x.Id == new Guid(LbnlInstitutionUuid)).AnyAsync()) + if (!await context.Institutions.Where(x => x.Id == new Guid(DataConstants.LbnlInstitutionUuid)).AnyAsync()) { var institution = new Institution( - new Guid(LbnlInstitutionUuid), + new Guid(DataConstants.LbnlInstitutionUuid), "LBNL", "LBNL", "Lawrence Berkeley National Laboratory", @@ -278,39 +268,39 @@ AppSettings appSettings if (environment.IsDevelopment()) { var context = services.GetRequiredService(); - if (!await context.Databases.Where(x => x.Id == new Guid(TestlabDatabaseUuid)).AnyAsync()) + if (!await context.Databases.Where(x => x.Id == new Guid(DataConstants.TestlabDatabaseUuid)).AnyAsync()) { 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(x => x.Id == new Guid(DataConstants.IgsdbDatabaseUuid)).AnyAsync()) { 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); @@ -528,7 +518,7 @@ AppSettings appSettings .AddResourcePermissions(appSettings.GraphQlEndpoint.AbsoluteUri); var application = new OpenIdConnectApplication { - OwnerId = new Guid(IseInstitutionUuid) + OwnerId = new Guid(DataConstants.IseInstitutionUuid) }; await manager.PopulateAsync(application, descriptor); // The secret is used in tests, see `IntegrationTests#RequestAuthToken` and in @@ -538,13 +528,13 @@ AppSettings appSettings if (environment.IsDevelopment()) { - if (await manager.FindByClientIdAsync(TestlabSolarFacadesOpenIdConnectClientId) is null) + if (await manager.FindByClientIdAsync(DataConstants.TestlabSolarFacadesOpenIdConnectClientId) is null) { - logger.CreatingApplicationClient(TestlabSolarFacadesOpenIdConnectClientId); + logger.CreatingApplicationClient(DataConstants.TestlabSolarFacadesOpenIdConnectClientId); var host = appSettings.TestlabSolarFacades.Uri; var descriptor = new OpenIddictApplicationDescriptor { - ClientId = TestlabSolarFacadesOpenIdConnectClientId, + ClientId = DataConstants.TestlabSolarFacadesOpenIdConnectClientId, ClientSecret = null, ConsentType = OpenIddictConstants.ConsentTypes.Explicit, DisplayName = "Testlab-Solar-Facades client application", @@ -586,7 +576,7 @@ AppSettings appSettings .AddAudiencePermissions(OpenIdConnectConstants.Client.MetabaseClientId); var application = new OpenIdConnectApplication { - OwnerId = new Guid(TestlabInstitutionUuid) + OwnerId = new Guid(DataConstants.TestlabInstitutionUuid) }; await manager.PopulateAsync(application, descriptor); // The secret is used in the database client, see @@ -594,12 +584,12 @@ AppSettings appSettings await manager.CreateAsync(application, appSettings.TestlabSolarFacades.OpenIdConnectClientSecret); } - if (await manager.FindByClientIdAsync(IgsdbOpenIdConnectClientId) is null) + if (await manager.FindByClientIdAsync(DataConstants.IgsdbOpenIdConnectClientId) 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", @@ -630,7 +620,7 @@ 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); 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..e9205aa93 --- /dev/null +++ b/backend/src/Data/IAuditable.cs @@ -0,0 +1,12 @@ +using NodaTime; + +namespace Metabase.Data; + +public interface IAuditable +{ + public OffsetDateTime CreatedAt { get; set; } + public OffsetDateTime UpdatedAt { get; set; } + + // soft delete + // public Instant? DeletedAt { get; set; } +} 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/OpenIdConnectApplication.cs b/backend/src/Data/OpenIdConnect/OpenIdConnectApplication.cs index 3323d8d77..51f335b2f 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 NodaTime; using OpenIddict.EntityFrameworkCore.Models; namespace Metabase.Data.OpenIdConnect; public sealed class OpenIdConnectApplication : OpenIddictEntityFrameworkCoreApplication, - IEntity + IEntity, + IAuditable { public Guid OwnerId { get; set; } [InverseProperty(nameof(Institution.OpenIdConnectApplications))] public Institution Owner { get; set; } = null!; - [Timestamp] + public OffsetDateTime CreatedAt { get; set; } + public OffsetDateTime 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..ffc22de68 100644 --- a/backend/src/Data/OpenIdConnect/OpenIdConnectAuthorization.cs +++ b/backend/src/Data/OpenIdConnect/OpenIdConnectAuthorization.cs @@ -1,13 +1,19 @@ using System; -using System.ComponentModel.DataAnnotations; +using NodaTime; 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 OffsetDateTime CreatedAt { get; set; } + public OffsetDateTime 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..90cfc7345 100644 --- a/backend/src/Data/OpenIdConnect/OpenIdConnectScope.cs +++ b/backend/src/Data/OpenIdConnect/OpenIdConnectScope.cs @@ -1,5 +1,5 @@ using System; -using System.ComponentModel.DataAnnotations; +using NodaTime; using OpenIddict.Abstractions; using OpenIddict.EntityFrameworkCore.Models; @@ -7,7 +7,8 @@ namespace Metabase.Data.OpenIdConnect; public sealed class OpenIdConnectScope : OpenIddictEntityFrameworkCoreScope, - IEntity + IEntity, + IAuditable { private const string ScopeSeparator = ":"; @@ -45,6 +46,10 @@ public sealed class OpenIdConnectScope ManageUserApiScope, ]; - [Timestamp] + public OffsetDateTime CreatedAt { get; set; } + public OffsetDateTime 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..2a12d55c2 100644 --- a/backend/src/Data/OpenIdConnect/OpenIdConnectToken.cs +++ b/backend/src/Data/OpenIdConnect/OpenIdConnectToken.cs @@ -1,13 +1,19 @@ using System; -using System.ComponentModel.DataAnnotations; +using NodaTime; 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 OffsetDateTime CreatedAt { get; set; } + public OffsetDateTime 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/User.cs b/backend/src/Data/User.cs index 0bf8130ec..58e1c0758 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 NodaTime; using Guid = System.Guid; // 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 { #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 OffsetDateTime CreatedAt { get; set; } + public OffsetDateTime 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/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..498b73049 100644 --- a/backend/src/Extensions/NodaTimeExtensions.cs +++ b/backend/src/Extensions/NodaTimeExtensions.cs @@ -4,13 +4,13 @@ namespace Metabase.Extensions; public static class NodaTimeExtensions { - extension(OffsetDateTime) + public static OffsetDateTime GetUtcNow(this IClock clock) { - public static OffsetDateTime UtcNow => - SystemClock.Instance - .GetCurrentInstant() - .WithOffset(Offset.Zero); + return clock.GetCurrentInstant().WithOffset(Offset.Zero); + } + extension(OffsetDateTime) + { public static bool operator >(OffsetDateTime x, OffsetDateTime y) { return OffsetDateTime.Comparer.Instant.Compare(x, y) > 0; diff --git a/backend/src/Extensions/StringExtensions.cs b/backend/src/Extensions/StringExtensions.cs index 62aeeb098..51cd17f58 100644 --- a/backend/src/Extensions/StringExtensions.cs +++ b/backend/src/Extensions/StringExtensions.cs @@ -2,12 +2,18 @@ namespace Metabase.Extensions; public static class StringExtensions { - public static string FirstCharToLower(this string str) + public static string FirstCharToLower(this string value) { - return string.IsNullOrEmpty(str) - || !char.IsLetter(str, 0) - || char.IsLower(str, 0) - ? str - : char.ToLowerInvariant(str[0]) + str[1..]; + return string.IsNullOrEmpty(value) + || !char.IsLetter(value, 0) + || char.IsLower(value, 0) + ? value + : char.ToLowerInvariant(value[0]) + value[1..]; } + + public static string? NullIfEmpty(this string value) + => string.IsNullOrEmpty(value) ? null : value; + + public static string? NullIfWhitespace(this string value) + => string.IsNullOrWhiteSpace(value) ? null : value; } \ 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..206336393 --- /dev/null +++ b/backend/src/GraphQl/Associations/AssociationType.cs @@ -0,0 +1,22 @@ +using HotChocolate.Types; +using Metabase.Data; + +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..60fb2f752 --- /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(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Associations/AuditableAssociationSortType.cs b/backend/src/GraphQl/Associations/AuditableAssociationSortType.cs new file mode 100644 index 000000000..3cd30dcb6 --- /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(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/AuthorizedConnection.cs b/backend/src/GraphQl/AuthorizedConnection.cs 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 80% rename from backend/src/GraphQl/DataX/CalorimetricDataConnection.cs rename to backend/src/GraphQl/CalorimetricDataX/CalorimetricDataConnection.cs index def1f6f26..422ac856a 100644 --- a/backend/src/GraphQl/DataX/CalorimetricDataConnection.cs +++ b/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataConnection.cs @@ -1,8 +1,8 @@ -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, diff --git a/backend/src/GraphQl/DataX/CalorimetricDataEdge.cs b/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataEdge.cs similarity index 65% rename from backend/src/GraphQl/DataX/CalorimetricDataEdge.cs rename to backend/src/GraphQl/CalorimetricDataX/CalorimetricDataEdge.cs index 53d5448b7..ceee36e33 100644 --- a/backend/src/GraphQl/DataX/CalorimetricDataEdge.cs +++ b/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataEdge.cs @@ -1,4 +1,6 @@ -namespace Metabase.GraphQl.DataX; +using Metabase.GraphQl.DataX; + +namespace Metabase.GraphQl.CalorimetricDataX; public sealed record CalorimetricDataEdge( string Cursor, diff --git a/backend/src/GraphQl/DataX/CalorimetricDataPropositionInput.cs b/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataPropositionInput.cs similarity index 84% rename from backend/src/GraphQl/DataX/CalorimetricDataPropositionInput.cs rename to backend/src/GraphQl/CalorimetricDataX/CalorimetricDataPropositionInput.cs index 53e04afcb..aae0a7629 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, diff --git a/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataQueries.cs b/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataQueries.cs new file mode 100644 index 000000000..aa7b5be03 --- /dev/null +++ b/backend/src/GraphQl/CalorimetricDataX/CalorimetricDataQueries.cs @@ -0,0 +1,95 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using Metabase.Data; +using Metabase.GraphQl.Requests; +using Microsoft.EntityFrameworkCore; + +namespace Metabase.GraphQl.CalorimetricDataX; + +[ExtendObjectType(nameof(Query))] +public sealed class CalorimetricDataQueries +{ + public async Task GetCalorimetricDataAsync( + Guid databaseId, + Guid id, + 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 + ); + } + + public Task GetAllCalorimetricDataAsync( + CalorimetricDataPropositionInput? where, + string? locale, + uint? first, + string? after, + uint? 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, + 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..ea7bee0f9 100644 --- a/backend/src/GraphQl/ComponentAssemblies/ComponentAssemblyFilterType.cs +++ b/backend/src/GraphQl/ComponentAssemblies/ComponentAssemblyFilterType.cs @@ -1,16 +1,20 @@ 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(); + base.Configure(descriptor); + // TODO Remove CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); descriptor.Field(x => x.AssembledComponent); descriptor.Field(x => x.PartComponent); descriptor.Field(x => x.Index); diff --git a/backend/src/GraphQl/ComponentAssemblies/ComponentAssemblySortType.cs b/backend/src/GraphQl/ComponentAssemblies/ComponentAssemblySortType.cs index 043174587..d7e4cfc4b 100644 --- a/backend/src/GraphQl/ComponentAssemblies/ComponentAssemblySortType.cs +++ b/backend/src/GraphQl/ComponentAssemblies/ComponentAssemblySortType.cs @@ -1,18 +1,17 @@ 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); + base.Configure(descriptor); descriptor.Field(x => x.Index); descriptor.Field(x => x.PrimeSurface); } 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..fa4202087 100644 --- a/backend/src/GraphQl/ComponentGeneralizations/ComponentConcretizationAndGeneralizationFilterType.cs +++ b/backend/src/GraphQl/ComponentGeneralizations/ComponentConcretizationAndGeneralizationFilterType.cs @@ -1,16 +1,20 @@ 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(); + base.Configure(descriptor); + // TODO Remove CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); descriptor.Field(x => x.ConcreteComponent); descriptor.Field(x => x.GeneralComponent); } 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..e3332786b 100644 --- a/backend/src/GraphQl/ComponentManufacturers/ComponentManufacturerFilterType.cs +++ b/backend/src/GraphQl/ComponentManufacturers/ComponentManufacturerFilterType.cs @@ -1,16 +1,20 @@ 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(); + base.Configure(descriptor); + // TODO Remove CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); descriptor.Field(x => x.Component); descriptor.Field(x => x.Institution); } diff --git a/backend/src/GraphQl/ComponentManufacturers/ComponentManufacturerMutations.cs b/backend/src/GraphQl/ComponentManufacturers/ComponentManufacturerMutations.cs index 8c4eca552..3637a7093 100644 --- a/backend/src/GraphQl/ComponentManufacturers/ComponentManufacturerMutations.cs +++ b/backend/src/GraphQl/ComponentManufacturers/ComponentManufacturerMutations.cs @@ -59,9 +59,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..544aadd15 100644 --- a/backend/src/GraphQl/ComponentVariants/ComponentVariantFilterType.cs +++ b/backend/src/GraphQl/ComponentVariants/ComponentVariantFilterType.cs @@ -1,16 +1,20 @@ 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(); + base.Configure(descriptor); + // TODO Remove CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); descriptor.Field(x => x.OfComponent); descriptor.Field(x => x.ToComponent); } 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..eb0b9eba4 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; 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..449fc9c61 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; 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..489209d7a 100644 --- a/backend/src/GraphQl/Components/ComponentFilterType.cs +++ b/backend/src/GraphQl/Components/ComponentFilterType.cs @@ -5,13 +5,17 @@ namespace Metabase.GraphQl.Components; public class ComponentFilterType - : 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(x => x.Id); + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); descriptor.Field(x => x.Name); descriptor.Field(x => x.Abbreviation); descriptor.Field(x => x.Description); 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..b8793c388 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; 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..6c7404b7b 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; 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/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..663f40c5c 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; 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..fbc8d53cb 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..15da7d0d8 100644 --- a/backend/src/GraphQl/Components/ComponentSortType.cs +++ b/backend/src/GraphQl/Components/ComponentSortType.cs @@ -4,8 +4,8 @@ namespace Metabase.GraphQl.Components; -public sealed class ComponentSortType - : EntitySortType +public class ComponentSortType + : AuditableEntitySortType { protected override void Configure( ISortInputTypeDescriptor descriptor diff --git a/backend/src/GraphQl/Components/ComponentType.cs b/backend/src/GraphQl/Components/ComponentType.cs index e62961e66..65b61e017 100644 --- a/backend/src/GraphQl/Components/ComponentType.cs +++ b/backend/src/GraphQl/Components/ComponentType.cs @@ -1,6 +1,7 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using GreenDonut.Data; using HotChocolate; using HotChocolate.Types; using Metabase.Authorization; @@ -13,7 +14,7 @@ namespace Metabase.GraphQl.Components; public sealed class ComponentType - : EntityType + : EntityType { protected override void Configure( IObjectTypeDescriptor descriptor @@ -29,6 +30,7 @@ IObjectTypeDescriptor descriptor descriptor .Field("prime") .Type>() + .Cost(0) .Resolve(context => { var component = context.Parent(); @@ -54,6 +56,7 @@ IObjectTypeDescriptor descriptor .Field(t => t.Manufacturers) .Type>>() .UseFiltering() + .UseSorting() .Resolve(context => new ComponentManufacturerConnection( context.Parent(), @@ -65,6 +68,7 @@ IObjectTypeDescriptor descriptor .Type>() .Authorize(AuthorizationPolicies.WriteScopePolicy) .UseFiltering() + .UseSorting() .Resolve(context => new PendingComponentManufacturerConnection( context.Parent(), @@ -78,6 +82,7 @@ IObjectTypeDescriptor descriptor .Name("assembledOf") .Type>>() .UseFiltering() + .UseSorting() .Resolve(context => new ComponentAssembledOfConnection( context.Parent(), @@ -90,6 +95,7 @@ IObjectTypeDescriptor descriptor .Field(t => t.PartOf) .Type>>() .UseFiltering() + .UseSorting() .Resolve(context => new ComponentPartOfConnection( context.Parent(), @@ -103,6 +109,7 @@ IObjectTypeDescriptor descriptor .Name("concretizationOf") .Type>>() .UseFiltering() + .UseSorting() .Resolve(context => new ComponentConcretizationOfConnection( context.Parent(), @@ -117,6 +124,7 @@ IObjectTypeDescriptor descriptor .Name("generalizationOf") .Type>>() .UseFiltering() + .UseSorting() .Resolve(context => new ComponentGeneralizationOfConnection( context.Parent(), @@ -135,6 +143,7 @@ IObjectTypeDescriptor descriptor .Field(t => t.VariantOf) .Type>>() .UseFiltering() + .UseSorting() .Resolve(context => new ComponentVariantOfConnection( context.Parent(), @@ -145,6 +154,7 @@ IObjectTypeDescriptor descriptor .Field(t => t.VariantOfEdges).Ignore(); descriptor .Field("isAuthorizedToUpdateNode") + .Cost(1) .ResolveWith(x => ComponentResolvers.IsAuthorizedToUpdateNodeAsync(default!, default!, default!, default!)) .UseUserManager(); @@ -152,7 +162,7 @@ IObjectTypeDescriptor descriptor private sealed class ComponentResolvers { - public static Task IsAuthorizedToUpdateNodeAsync( + 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..41a9e5ef4 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; 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..c2606a0fb 100644 --- a/backend/src/GraphQl/Connection.cs +++ b/backend/src/GraphQl/Connection.cs @@ -6,34 +6,44 @@ 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; + [Cost(0)] public async Task GetTotalCountAsync( - TAssociationsByAssociateIdDataLoader dataLoader, + TAssociationsByOneIdDataLoader dataLoader, CancellationToken cancellationToken ) { - return (uint)(await dataLoader.With(queryContext).LoadRequiredAsync(Subject.Id, cancellationToken)).Length; + return (uint)( + ( + await dataLoader + .With(queryContext) + .LoadAsync(Subject.Id, cancellationToken) + ) + ?.Length ?? 0 + ); } + [Cost(0)] public async IAsyncEnumerable GetEdgesAsync( - TAssociationsByAssociateIdDataLoader dataLoader, + TAssociationsByOneIdDataLoader dataLoader, [EnumeratorCancellation] CancellationToken cancellationToken ) { - foreach (var association in await dataLoader.With(queryContext).LoadRequiredAsync(Subject.Id, cancellationToken)) + foreach (var association in await dataLoader.With(queryContext).LoadAsync(Subject.Id, cancellationToken) ?? []) { yield return createEdge(association); } 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/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/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..6e54baf76 100644 --- a/backend/src/GraphQl/DataFormats/DataFormatFilterType.cs +++ b/backend/src/GraphQl/DataFormats/DataFormatFilterType.cs @@ -5,13 +5,17 @@ namespace Metabase.GraphQl.DataFormats; public class DataFormatFilterType - : 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(x => x.Id); + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); descriptor.Field(x => x.Name); descriptor.Field(x => x.Extension); descriptor.Field(x => x.Description); 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/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..4284db9a4 100644 --- a/backend/src/GraphQl/DataFormats/DataFormatSortType.cs +++ b/backend/src/GraphQl/DataFormats/DataFormatSortType.cs @@ -4,8 +4,8 @@ namespace Metabase.GraphQl.DataFormats; -public sealed class DataFormatSortType - : EntitySortType +public class DataFormatSortType + : AuditableEntitySortType { protected override void Configure( ISortInputTypeDescriptor descriptor @@ -17,6 +17,5 @@ ISortInputTypeDescriptor descriptor descriptor.Field(x => x.Description); descriptor.Field(x => x.MediaType); descriptor.Field(x => x.SchemaLocator); - descriptor.Field(x => x.Manager); } } \ No newline at end of file diff --git a/backend/src/GraphQl/DataFormats/DataFormatType.cs b/backend/src/GraphQl/DataFormats/DataFormatType.cs index 51c77cf0b..6359b97eb 100644 --- a/backend/src/GraphQl/DataFormats/DataFormatType.cs +++ b/backend/src/GraphQl/DataFormats/DataFormatType.cs @@ -13,7 +13,7 @@ namespace Metabase.GraphQl.DataFormats; public sealed class DataFormatType - : EntityType + : EntityType { protected override void Configure( IObjectTypeDescriptor descriptor @@ -23,6 +23,7 @@ IObjectTypeDescriptor descriptor descriptor .Field(t => t.Reference) .Type() + .Cost(0) .Resolve(context => context .Parent() .Reference? @@ -41,6 +42,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..4659b1614 --- /dev/null +++ b/backend/src/GraphQl/DataLoaders.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using GreenDonut.Data; +using LinqKit; +using 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 = + databaseContextFactory.CreateDbContext(); + return await getEntities(databaseContext) + .AsNoTrackingWithIdentityResolution() + .Where(_ => ids.Contains(_.Id)) + .With(queryContext, Sorting.DefaultEntityOrder) + .ToDictionaryAsync(_ => _.Id, cancellationToken); + } + + public static async ValueTask> GetManyByOneIdAsync( + IReadOnlyList ids, + Func> getMany, + Expression> getOneId, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + where TMany : class, IEntity, IAuditable + { + await using var databaseContext = + databaseContextFactory.CreateDbContext(); + return + await getMany(databaseContext) + .AsExpandable() + .AsNoTracking() + .Where(_ => ids.Contains(getOneId.Invoke(_))) + .With(queryContext, Sorting.DefaultEntityOrder) + .GroupBy(getOneId) + .Select(_ => new { _.Key, Items = _.ToArray() }) + .ToDictionaryAsync(_ => _.Key, _ => _.Items, cancellationToken); + } + + public static async ValueTask>> GetManyByOneIdAsync( + IReadOnlyList ids, + Func> getMany, + Expression> getOneId, + PagingArguments pagingArguments, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + where TMany : class, IEntity, IAuditable + { + await using var databaseContext = + databaseContextFactory.CreateDbContext(); + return + await getMany(databaseContext) + .AsExpandable() + .AsNoTracking() + .Where(_ => ids.Contains(getOneId.Invoke(_))) + .With(queryContext, Sorting.DefaultEntityOrder) + .ToBatchPageAsync(getOneId, pagingArguments, cancellationToken); + } + + public static async ValueTask> GetAssociationsByOneIdAsync( + IReadOnlyList ids, + Func> getAssociations, + Expression> getOneId, + Expression> getOtherId, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + where TAssociation : class, IAssociation, IAuditable + { + await using var databaseContext = + databaseContextFactory.CreateDbContext(); + return + await getAssociations(databaseContext) + .AsExpandable() + .AsNoTracking() + .Where(_ => ids.Contains(getOneId.Invoke(_))) + .With(queryContext, _ => _.AddDescending(getOneId).AddDescending(getOtherId)) + .GroupBy(getOneId) + .Select(_ => new { _.Key, Items = _.ToArray() }) + .ToDictionaryAsync(_ => _.Key, _ => _.Items, cancellationToken); + } + + public static async ValueTask>> GetAssociationsByOneIdAsync( + IReadOnlyList ids, + Func> getAssociations, + Expression> getOneId, + Expression> getOtherId, + PagingArguments pagingArguments, + QueryContext queryContext, + IDbContextFactory databaseContextFactory, + CancellationToken cancellationToken + ) + where TAssociation : class, IAssociation, IAuditable + { + await using var databaseContext = + databaseContextFactory.CreateDbContext(); + return + await getAssociations(databaseContext) + .AsExpandable() + .AsNoTracking() + .Where(_ => ids.Contains(getOneId.Invoke(_))) + .With(queryContext, _ => _.AddDescending(getOneId).AddDescending(getOtherId)) + .ToBatchPageAsync(getOneId, pagingArguments, cancellationToken); + } +} 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..8e8beb5f6 100644 --- a/backend/src/GraphQl/DataX/Data.cs +++ b/backend/src/GraphQl/DataX/Data.cs @@ -1,63 +1,84 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Text; +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 => Convert.ToBase64String( + Encoding.UTF8.GetBytes($"{DatabaseId}:{Uuid}:{Locale ?? ""}:{DataId}") + ); - 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 +88,7 @@ CancellationToken cancellationToken } public Task GetComponentAsync( - ComponentByIdDataLoader componentById, + IComponentByIdDataLoader componentById, CancellationToken cancellationToken ) { @@ -78,7 +99,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..3aef4aeb5 100644 --- a/backend/src/GraphQl/DataX/DataApproval.cs +++ b/backend/src/GraphQl/DataX/DataApproval.cs @@ -8,29 +8,20 @@ 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, + 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/DataConnectionBase.cs b/backend/src/GraphQl/DataX/DataConnectionBase.cs index 30cc4612e..ac51995af 100644 --- a/backend/src/GraphQl/DataX/DataConnectionBase.cs +++ b/backend/src/GraphQl/DataX/DataConnectionBase.cs @@ -1,10 +1,11 @@ using System.Collections.Generic; +using HotChocolate.CostAnalysis.Types; using HotChocolate.Types.Pagination; namespace Metabase.GraphQl.DataX; public abstract record DataConnectionBase( - IReadOnlyList Edges, - uint TotalCount, - ConnectionPageInfo PageInfo + [property: Cost(0)] IReadOnlyList Edges, + [property: Cost(0)] uint TotalCount, + [property: Cost(0)] ConnectionPageInfo PageInfo ); \ No newline at end of file diff --git a/backend/src/GraphQl/DataX/DataEdgeBase.cs b/backend/src/GraphQl/DataX/DataEdgeBase.cs index 78da81a18..0d538bbc8 100644 --- a/backend/src/GraphQl/DataX/DataEdgeBase.cs +++ b/backend/src/GraphQl/DataX/DataEdgeBase.cs @@ -1,6 +1,8 @@ +using HotChocolate.CostAnalysis.Types; + namespace Metabase.GraphQl.DataX; public abstract record DataEdgeBase( - string Cursor, - TData Node + [property: Cost(0)] string Cursor, + [property: Cost(0)] 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/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..ed0a292fa 100644 --- a/backend/src/GraphQl/DataX/IApproval.cs +++ b/backend/src/GraphQl/DataX/IApproval.cs @@ -1,8 +1,6 @@ using System; using System.Text.Json; -using System.Text.Json.Serialization; using HotChocolate.Types; -using Metabase.Configuration; using NodaTime; namespace Metabase.GraphQl.DataX; 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/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/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/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..c11999870 100644 --- a/backend/src/GraphQl/Databases/DatabaseFilterType.cs +++ b/backend/src/GraphQl/Databases/DatabaseFilterType.cs @@ -5,13 +5,17 @@ namespace Metabase.GraphQl.Databases; public class DatabaseFilterType - : 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(x => x.Id); + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); descriptor.Field(x => x.Name); descriptor.Field(x => x.Description); descriptor.Field(x => x.Locator); diff --git a/backend/src/GraphQl/Databases/DatabaseMutations.cs b/backend/src/GraphQl/Databases/DatabaseMutations.cs index bf0b7c3e4..d8c52dd67 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; 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..06e69e008 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,46 @@ 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 ) { - sorting.StabilizeOrder(); - return - context.Databases.AsNoTracking() - .Where(d => d.VerificationState == DatabaseVerificationState.PENDING); + return databaseContext.Databases + .AsNoTracking() + .Where(d => d.VerificationState == DatabaseVerificationState.PENDING) + .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..3e7e51e2d 100644 --- a/backend/src/GraphQl/Databases/DatabaseResolvers.cs +++ b/backend/src/GraphQl/Databases/DatabaseResolvers.cs @@ -1,206 +1,26 @@ 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.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; 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,226 +41,97 @@ 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, 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, + DataPropositionInput? where, string? locale, - QueryingDatabases queryingDatabases, 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, 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, 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, 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, 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, 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, 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( + public Task GetAllCalorimetricDataAsync( [Parent] Database database, CalorimetricDataPropositionInput? where, string? locale, @@ -448,36 +139,14 @@ await QueryingDatabases.ConstructQuery( string? after, uint? 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( + public Task GetAllGeometricDataAsync( [Parent] Database database, GeometricDataPropositionInput? where, string? locale, @@ -485,35 +154,14 @@ await QueryingDatabases.ConstructQuery( string? after, uint? 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( + public Task GetAllHygrothermalDataAsync( [Parent] Database database, HygrothermalDataPropositionInput? where, string? locale, @@ -521,36 +169,14 @@ await QueryingDatabases.ConstructQuery( string? after, uint? 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( + public Task GetAllLifeCycleDataAsync( [Parent] Database database, LifeCycleDataPropositionInput? where, string? locale, @@ -558,36 +184,14 @@ await QueryingDatabases.ConstructQuery( string? after, uint? 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( + public Task GetAllOpticalDataAsync( [Parent] Database database, OpticalDataPropositionInput? where, string? locale, @@ -595,35 +199,14 @@ await QueryingDatabases.ConstructQuery( string? after, uint? 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( + public Task GetAllPhotovoltaicDataAsync( [Parent] Database database, PhotovoltaicDataPropositionInput? where, string? locale, @@ -631,327 +214,76 @@ await QueryingDatabases.ConstructQuery( string? after, uint? 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, 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, 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, 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, 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, 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, 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..be860564f 100644 --- a/backend/src/GraphQl/Databases/DatabaseSortType.cs +++ b/backend/src/GraphQl/Databases/DatabaseSortType.cs @@ -4,8 +4,8 @@ namespace Metabase.GraphQl.Databases; -public sealed class DatabaseSortType - : EntitySortType +public class DatabaseSortType + : AuditableEntitySortType { protected override void Configure( ISortInputTypeDescriptor descriptor @@ -15,6 +15,5 @@ ISortInputTypeDescriptor descriptor descriptor.Field(x => x.Name); descriptor.Field(x => x.Description); descriptor.Field(x => x.Locator); - descriptor.Field(x => x.Operator); } } \ No newline at end of file diff --git a/backend/src/GraphQl/Databases/DatabaseType.cs b/backend/src/GraphQl/Databases/DatabaseType.cs index efe337d93..e9a8fe89e 100644 --- a/backend/src/GraphQl/Databases/DatabaseType.cs +++ b/backend/src/GraphQl/Databases/DatabaseType.cs @@ -3,13 +3,20 @@ using HotChocolate.Types; 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.GraphQl.Entities; +using Metabase.GraphQl.Scalars; using Metabase.GraphQl.Users; namespace Metabase.GraphQl.Databases; public sealed class DatabaseType - : EntityType + : EntityType { protected override void Configure( IObjectTypeDescriptor descriptor @@ -30,113 +37,115 @@ IObjectTypeDescriptor descriptor ConfigureDataField( descriptor, "data", - _ => _.GetDataAsync(default!, default!, default, default, default!, default!, default) + _ => _.GetDataAsync(default!, default!, default, default!, default!, default) ) .Argument("kind", _ => _.Type>>()); ConfigureHasDataField( descriptor, "hasData", - _ => _.HasDataAsync(default!, default!, default!, default, default!, default!, default) + _ => _.HasDataAsync(default!, default!, default!, default!, default!, default) ) .Argument("kind", _ => _.Type>>()); ConfigureDataField( descriptor, "calorimetricData", - _ => _.GetCalorimetricDataAsync(default!, default, default, default!, default!, default) + _ => _.GetCalorimetricDataAsync(default!, default, default!, default!, default) ); ConfigureAllDataField( descriptor, "allCalorimetricData", - _ => _.GetAllCalorimetricDataAsync(default!, default, default, default, default, default, default, default!, default!, default) + _ => _.GetAllCalorimetricDataAsync(default!, default, default, default, default, default, default!, default!, default) ); ConfigureHasDataField( descriptor, "hasCalorimetricData", - _ => _.HasCalorimetricDataAsync(default!, default, default, default!, default!, default) + _ => _.HasCalorimetricDataAsync(default!, default, default!, default!, default) ); ConfigureDataField( descriptor, "geometricData", - _ => _.GetGeometricDataAsync(default!, default, default, default!, default!, default) + _ => _.GetGeometricDataAsync(default!, default, default!, default!, default) ); ConfigureAllDataField( descriptor, "allGeometricData", - _ => _.GetAllGeometricDataAsync(default!, default, default, default, default, default, default, default!, default!, default) + _ => _.GetAllGeometricDataAsync(default!, default, default, default, default, default, default!, default!, default) ); ConfigureHasDataField( descriptor, "hasGeometricData", - _ => _.HasGeometricDataAsync(default!, default, default, default!, default!, default) + _ => _.HasGeometricDataAsync(default!, default, default!, default!, default) ); ConfigureDataField( descriptor, "hygrothermalData", - _ => _.GetHygrothermalDataAsync(default!, default, default, default!, default!, default) + _ => _.GetHygrothermalDataAsync(default!, default, default!, default!, default) ); ConfigureAllDataField( descriptor, "allHygrothermalData", - _ => _.GetAllHygrothermalDataAsync(default!, default, default, default, default, default, default, default!, default!, default) + _ => _.GetAllHygrothermalDataAsync(default!, default, default, default, default, default, default!, default!, default) ); ConfigureHasDataField( descriptor, "hasHygrothermalData", - _ => _.HasHygrothermalDataAsync(default!, default, default, default!, default!, default) + _ => _.HasHygrothermalDataAsync(default!, default, default!, default!, default) ); ConfigureDataField( descriptor, "lifeCycleData", - _ => _.GetLifeCycleDataAsync(default!, default, default, default!, default!, default) + _ => _.GetLifeCycleDataAsync(default!, default, default!, default!, default) ); ConfigureAllDataField( descriptor, "allLifeCycleData", - _ => _.GetAllLifeCycleDataAsync(default!, default, default, default, default, default, default, default!, default!, default) + _ => _.GetAllLifeCycleDataAsync(default!, default, default, default, default, default, default!, default!, default) ); ConfigureHasDataField( descriptor, "hasLifeCycleData", - _ => _.HasLifeCycleDataAsync(default!, default, default, default!, default!, default) + _ => _.HasLifeCycleDataAsync(default!, default, default!, default!, default) ); ConfigureDataField( descriptor, "opticalData", - _ => _.GetOpticalDataAsync(default!, default, default, default!, default!, default) + _ => _.GetOpticalDataAsync(default!, default, default!, default!, default) ); ConfigureAllDataField( descriptor, "allOpticalData", - _ => _.GetAllOpticalDataAsync(default!, default, default, default, default, default, default, + _ => _.GetAllOpticalDataAsync(default!, default, default, default, default, default, default!, default!, default) ); ConfigureHasDataField( descriptor, "hasOpticalData", - _ => _.HasOpticalDataAsync(default!, default, default, default!, default!, default) + _ => _.HasOpticalDataAsync(default!, default, default!, default!, default) ); ConfigureDataField( descriptor, "photovoltaicData", - _ => _.GetPhotovoltaicDataAsync(default!, default, default, default!, default!, default) + _ => _.GetPhotovoltaicDataAsync(default!, default, default!, default!, default) ); ConfigureAllDataField( descriptor, "allPhotovoltaicData", - _ => _.GetAllPhotovoltaicDataAsync(default!, default, default, default, default, default, default, default!, default!, default) + _ => _.GetAllPhotovoltaicDataAsync(default!, default, default, default, default, default, default!, default!, default) ); ConfigureHasDataField( descriptor, "hasPhotovoltaicData", - _ => _.HasPhotovoltaicDataAsync(default!, default, default, default!, default!, default) + _ => _.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(); diff --git a/backend/src/GraphQl/DescriptionOrReferences/DescriptionOrReferenceFilterType.cs b/backend/src/GraphQl/DescriptionOrReferences/DescriptionOrReferenceFilterType.cs index 9400616e2..7e70be1da 100644 --- a/backend/src/GraphQl/DescriptionOrReferences/DescriptionOrReferenceFilterType.cs +++ b/backend/src/GraphQl/DescriptionOrReferences/DescriptionOrReferenceFilterType.cs @@ -10,6 +10,8 @@ protected override void Configure( IFilterInputTypeDescriptor descriptor ) { + base.Configure(descriptor); + descriptor.Name(nameof(DescriptionOrReferenceFilterType)[..^"FilterType".Length] + GraphQlConstants.FilterInputSuffix); descriptor.BindFieldsExplicitly(); descriptor.Field(x => x.Description); } diff --git a/backend/src/GraphQl/DescriptionOrReferences/DescriptionOrReferenceSortType.cs b/backend/src/GraphQl/DescriptionOrReferences/DescriptionOrReferenceSortType.cs index 7f848c06f..396a1678e 100644 --- a/backend/src/GraphQl/DescriptionOrReferences/DescriptionOrReferenceSortType.cs +++ b/backend/src/GraphQl/DescriptionOrReferences/DescriptionOrReferenceSortType.cs @@ -10,6 +10,8 @@ protected override void Configure( ISortInputTypeDescriptor descriptor ) { + base.Configure(descriptor); + descriptor.Name(nameof(DescriptionOrReferenceSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); descriptor.BindFieldsExplicitly(); descriptor.Field(x => x.Description); } 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/EntityFilterType.cs b/backend/src/GraphQl/Entities/AuditableEntityFilterType.cs similarity index 63% rename from backend/src/GraphQl/Entities/EntityFilterType.cs rename to backend/src/GraphQl/Entities/AuditableEntityFilterType.cs index 1c00bb230..093748032 100644 --- a/backend/src/GraphQl/Entities/EntityFilterType.cs +++ b/backend/src/GraphQl/Entities/AuditableEntityFilterType.cs @@ -3,9 +3,9 @@ namespace Metabase.GraphQl.Entities; -public abstract class EntityFilterType +public abstract class AuditableEntityFilterType : FilterInputType - where TEntity : IEntity + where TEntity : IEntity, IAuditable { protected override void Configure( IFilterInputTypeDescriptor descriptor @@ -13,5 +13,7 @@ IFilterInputTypeDescriptor descriptor { descriptor.BindFieldsExplicitly(); descriptor.Field(x => x.Id); + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); } } \ No newline at end of file diff --git a/backend/src/GraphQl/Entities/EntitySortType.cs b/backend/src/GraphQl/Entities/AuditableEntitySortType.cs similarity index 58% rename from backend/src/GraphQl/Entities/EntitySortType.cs rename to backend/src/GraphQl/Entities/AuditableEntitySortType.cs index 8cb694f00..643e90695 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(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); } } \ No newline at end of file diff --git a/backend/src/GraphQl/Entities/EntityByIdDataLoader.cs b/backend/src/GraphQl/Entities/EntityByIdDataLoader.cs deleted file mode 100644 index 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/EntityType.cs b/backend/src/GraphQl/Entities/EntityType.cs index 73b056cc1..d87af8132 100644 --- a/backend/src/GraphQl/Entities/EntityType.cs +++ b/backend/src/GraphQl/Entities/EntityType.cs @@ -8,12 +8,13 @@ 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 +22,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..1256eb48b 100644 --- a/backend/src/GraphQl/ErrorLoggingDiagnosticEventListener.cs +++ b/backend/src/GraphQl/ErrorLoggingDiagnosticEventListener.cs @@ -1,15 +1,13 @@ using System; using System.Collections.Generic; -using System.Globalization; -using System.Text; using System.Text.RegularExpressions; using 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; namespace Metabase.GraphQl; @@ -27,45 +25,47 @@ public static partial void RequestError( [LoggerMessage( Level = LogLevel.Error, - Message = "Resolver error. Field: '{FieldName}'. Operation: '{Operation}'." + Message = "Request error. Document: {Document}" )] - public static partial void ResolverError( + public static partial void RequestError( this ILogger logger, - Exception? exception, - string fieldName, - IOperation operation, + IOperationDocument? document, [TagProvider(typeof(HotChocolateIErrorTagProvider), nameof(HotChocolateIErrorTagProvider.RecordTags))] IError error ); [LoggerMessage( Level = LogLevel.Error, - Message = "Subscription event error. Operation: {Operation}" + Message = "Resolver error. Field: '{FieldName}'. Document: {Document}" )] - public static partial void SubscriptionEventError( + public static partial void ResolverError( this ILogger logger, - Exception exception, - IOperation operation + string fieldName, + DocumentNode document, + [TagProvider(typeof(HotChocolateIErrorTagProvider), nameof(HotChocolateIErrorTagProvider.RecordTags))] IError error, + Exception? exception ); [LoggerMessage( Level = LogLevel.Error, - Message = "Subscription event error. Operation: {Operation}" + Message = "Resolver error. Field: '{FieldName}'. Document: {Document}" )] - public static partial void SubscriptionTransportError( + public static partial void ResolverError( this ILogger logger, - Exception exception, - IOperation operation + Exception? exception, + string fieldName, + IOperationDocument? document, + [TagProvider(typeof(HotChocolateIErrorTagProvider), nameof(HotChocolateIErrorTagProvider.RecordTags))] IError error ); [LoggerMessage( Level = LogLevel.Error, - Message = "Syntax error. Document: {Document}" + Message = "Subscription event error. Id: '{SubscriptionId}'. Operation: {Document}" )] - public static partial void SyntaxError( + public static partial void SubscriptionEventError( this ILogger logger, - Exception? exception, - IOperationDocument? document, - [TagProvider(typeof(HotChocolateIErrorTagProvider), nameof(HotChocolateIErrorTagProvider.RecordTags))] IError error + Exception exception, + ulong subscriptionId, + IOperationDocument? document ); [LoggerMessage( @@ -128,55 +128,57 @@ ILogger logger : ExecutionDiagnosticEventListener { // this diagnostic event is raised when a request is executed ... - public override IDisposable ExecuteRequest(IRequestContext context) + public override IDisposable ExecuteRequest(RequestContext context) { // ... we will return an activity scope that is used to signal when the request is finished. return new RequestScope(logger, context); } public override void RequestError( - IRequestContext context, - Exception exception + RequestContext context, + Exception error ) { - logger.RequestError(exception, context.Request.Document); - base.RequestError(context, exception); + logger.RequestError(error, context.Request.Document); + base.RequestError(context, error); } - public override void ResolverError( - IMiddlewareContext context, + public override void RequestError( + RequestContext context, IError error ) { - logger.ResolverError(error.Exception, context.Selection.Field.Name, context.Operation, error); - base.ResolverError(context, error); + logger.RequestError(context.Request.Document, error); + base.RequestError(context, error); } - public override void SubscriptionEventError( - SubscriptionEventContext context, - Exception exception + public override void ResolverError( + IMiddlewareContext context, + IError error ) { - logger.SubscriptionEventError(exception, context.Subscription.Operation); - base.SubscriptionEventError(context, exception); + logger.ResolverError(context.Selection.Field.Name, context.Operation.Document, error, error.Exception); + base.ResolverError(context, error); } - public override void SubscriptionTransportError( - ISubscription subscription, - Exception exception + public override void ResolverError( + RequestContext context, + ISelection selection, + IError error ) { - logger.SubscriptionTransportError(exception, subscription.Operation); - base.SubscriptionTransportError(subscription, exception); + logger.ResolverError(error.Exception, selection.Field.Name, context.Request?.Document, error); + base.ResolverError(context, selection, error); } - public override void SyntaxError( - IRequestContext context, - IError error + public override void SubscriptionEventError( + RequestContext context, + ulong subscriptionId, + Exception exception ) { - logger.SyntaxError(error.Exception, context.Request.Document, error); - base.SyntaxError(context, error); + logger.SubscriptionEventError(exception, subscriptionId, context.Request.Document); + base.SubscriptionEventError(context, subscriptionId, exception); } public override void TaskError( @@ -189,7 +191,7 @@ IError error } public override void ValidationErrors( - IRequestContext context, + RequestContext context, IReadOnlyList errors ) { @@ -200,7 +202,7 @@ IReadOnlyList errors base.ValidationErrors(context, errors); } - private sealed partial class RequestScope(ILogger logger, IRequestContext context) : IDisposable + private sealed partial class RequestScope(ILogger logger, RequestContext context) : IDisposable { [GeneratedRegex(@"apiKey|authKey|privateKey|password|passphrase|secret|secure|security|token", RegexOptions.IgnoreCase, "")] private static partial Regex SecretRegex(); @@ -209,43 +211,45 @@ private sealed partial class RequestScope(ILogger" - : variableValue.Value.ToString() - ); - stringBuilder.Append('\''); - stringBuilder.AppendFormat(CultureInfo.InvariantCulture, $"{Environment.NewLine}"); - } - catch (Exception exception) - { - // all input type records will land here. - stringBuilder.AppendFormat(CultureInfo.InvariantCulture, $"Failed stringifying the value: {exception.Message}"); - stringBuilder.AppendFormat(CultureInfo.InvariantCulture, $"{Environment.NewLine}"); - } - } - } - _variables = stringBuilder.ToString(); + // TODO Where are the variables now if not anymore in context.Variables? + // if (_variables is not null) + // { + // return _variables; + // } + // if (context.Variables is null) + // { + // return null; + // } + // StringBuilder stringBuilder = new(); + // foreach (var variableValueCollection in context.Variables) + // { + // foreach (var variableValue in variableValueCollection) + // { + // try + // { + // stringBuilder.AppendFormat( + // CultureInfo.InvariantCulture, + // $"{variableValue.Name} : {variableValue.Type} = " + // ); + // stringBuilder.Append('\''); + // stringBuilder.Append( + // SecretRegex().IsMatch(variableValue.Name) + // ? "" + // : variableValue.Value.ToString() + // ); + // stringBuilder.Append('\''); + // stringBuilder.AppendFormat(CultureInfo.InvariantCulture, $"{Environment.NewLine}"); + // } + // catch (Exception exception) + // { + // // all input type records will land here. + // stringBuilder.AppendFormat(CultureInfo.InvariantCulture, $"Failed stringifying the value: {exception.Message}"); + // stringBuilder.AppendFormat(CultureInfo.InvariantCulture, $"{Environment.NewLine}"); + // } + // } + // } + // _variables = stringBuilder.ToString(); + _variables = null; return _variables; } @@ -253,28 +257,29 @@ public void Dispose() { if (logger.IsEnabled(LogLevel.Debug)) { - if (context.Document is not null) + if (context.OperationDocumentInfo.Document is not null) { #pragma warning disable CA1873 // Evaluation of this argument may be expensive and unnecessary if logging is disabled (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1873) logger.Executed( - context.Document.ToString(true), + context.OperationDocumentInfo.Document.ToString(true), StringifyVariables() ); #pragma warning restore CA1873 } } // when the request is finished it will dispose the activity scope - if (context.Result is IOperationResult { Errors.Count: > 0 } operationResult) + if (context.Result is OperationResult { Errors.Count: > 0 } operationResult) { foreach (var error in operationResult.Errors) { logger.OperationError(context.Request.Document, StringifyVariables(), error); } } - if (context.Exception is { }) - { - logger.UnexpectedExecutionException(context.Request.Document, StringifyVariables(), context.Exception); - } + // TODO Where is the exception now? + // if (context.Exception is { }) + // { + // logger.UnexpectedExecutionException(context.Request.Document, StringifyVariables(), context.Exception); + // } } } } \ No newline at end of file diff --git a/backend/src/GraphQl/Extensions/PageExtensions.cs b/backend/src/GraphQl/Extensions/PageExtensions.cs new file mode 100644 index 000000000..dd1840af0 --- /dev/null +++ b/backend/src/GraphQl/Extensions/PageExtensions.cs @@ -0,0 +1,66 @@ +using System.Threading.Tasks; +using GreenDonut.Data; +using HotChocolate.Types.Pagination; + +namespace 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 +{ + public static async ValueTask GetTotalCountAsync( + this Task?> pagePromise + ) + { + return (await pagePromise)?.TotalCount ?? 0; + } + + public static async ValueTask GetPageInfoAsync( + this Task?> pagePromise + ) + { + var page = await pagePromise; + return new ConnectionPageInfo( + page?.HasNextPage ?? false, + page?.HasPreviousPage ?? false, + page?.CreateStartCursor(), + page?.CreateEndCursor() + ); + } + + // public static async Task> ToConnectionAsync( + // this Task> pagePromise, + // Func, PageEntry, Task>> createEdgeAsync, + // Func>, ConnectionPageInfo, int, Connection> createConnection + // ) + // where TTarget : class + // where TSource : class + // { + // return await CreateConnectionAsync(await pagePromise, createEdgeAsync, createConnection); + // } + + // private static async Task> CreateConnectionAsync( + // Page? page, + // Func, PageEntry, Task>> createEdgeAsync, + // Func>, ConnectionPageInfo, int, Connection> createConnection + // ) + // where TTarget : class + // { + // page ??= Page.Empty; + // var entries = page.Entries; + // IEdge[] edges = entries.IsEmpty + // ? [] + // : await Task.WhenAll( + // entries + // .Select(entry => createEdgeAsync(page, entry)) + // .ToList() + // ); + // return createConnection( + // edges, + // new ConnectionPageInfo( + // page.HasNextPage, + // page.HasPreviousPage, + // page.CreateStartCursor(), + // page.CreateEndCursor()), + // page.TotalCount ?? 0); + // } +} \ No newline at end of file diff --git a/backend/src/GraphQl/Extensions/ResolverContextExtensions.cs b/backend/src/GraphQl/Extensions/ResolverContextExtensions.cs index 2c068ddbc..49b2a5ba6 100644 --- a/backend/src/GraphQl/Extensions/ResolverContextExtensions.cs +++ b/backend/src/GraphQl/Extensions/ResolverContextExtensions.cs @@ -1,4 +1,5 @@ using GreenDonut.Data; +using HotChocolate; using HotChocolate.Data.Filters; using HotChocolate.Data.Sorting; using HotChocolate.Resolvers; @@ -8,16 +9,28 @@ 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 public static QueryContext GetQueryContext(this IResolverContext context) { var selection = context.Selection; var filterContext = context.GetFilterContext(); var sortContext = context.GetSortingContext(); - // TODO Make selection work return new QueryContext( null, // selection.AsSelector(), filterContext?.AsPredicate(), sortContext?.AsSortDefinition()); } + + // Using `PagingArguments pagingArguments,` in resolvers results in the parameter `pagingArguments: PagingArgumentsInput` in the GraphQL schema + public static PagingArguments GetPagingArguments(this IResolverContext context) + { + return new( + context.ArgumentValue("first"), + context.ArgumentValue("after"), + context.ArgumentValue("last"), + context.ArgumentValue("before"), + includeTotalCount: true + ); + } } \ No newline at end of file diff --git a/backend/src/GraphQl/Extensions/SortingContextExtensions.cs b/backend/src/GraphQl/Extensions/SortingContextExtensions.cs deleted file mode 100644 index 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..587d362fc 100644 --- a/backend/src/GraphQl/Filters/ScalarFilterInputTypes.cs +++ b/backend/src/GraphQl/Filters/ScalarFilterInputTypes.cs @@ -1,8 +1,15 @@ using HotChocolate.Data.Filters; using HotChocolate.Types; +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 +83,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 +113,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 +133,12 @@ protected override void Configure(IFilterInputTypeDescriptor descriptor) } } -public sealed class TimeSpanFilterInputType - : ExtendedComparableOperationFilterInputType +public sealed class DurationFilterInputType + : ExtendedComparableOperationFilterInputType { protected override void Configure(IFilterInputTypeDescriptor descriptor) { - descriptor.Name($"TimeSpan{GraphQlConstants.FilterInputSuffix}"); + descriptor.Name($"Duration{GraphQlConstants.FilterInputSuffix}"); base.Configure(descriptor); } } @@ -146,12 +163,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 80% rename from backend/src/GraphQl/DataX/GeometricDataConnection.cs rename to backend/src/GraphQl/GeometricDataX/GeometricDataConnection.cs index 430e4d7c7..5b8bb265f 100644 --- a/backend/src/GraphQl/DataX/GeometricDataConnection.cs +++ b/backend/src/GraphQl/GeometricDataX/GeometricDataConnection.cs @@ -1,7 +1,8 @@ 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, diff --git a/backend/src/GraphQl/DataX/GeometricDataEdge.cs b/backend/src/GraphQl/GeometricDataX/GeometricDataEdge.cs similarity index 67% rename from backend/src/GraphQl/DataX/GeometricDataEdge.cs rename to backend/src/GraphQl/GeometricDataX/GeometricDataEdge.cs index 40c3383f4..18c93f19e 100644 --- a/backend/src/GraphQl/DataX/GeometricDataEdge.cs +++ b/backend/src/GraphQl/GeometricDataX/GeometricDataEdge.cs @@ -1,6 +1,7 @@ using System; +using Metabase.GraphQl.DataX; -namespace Metabase.GraphQl.DataX; +namespace Metabase.GraphQl.GeometricDataX; public sealed record GeometricDataEdge( string Cursor, diff --git a/backend/src/GraphQl/DataX/GeometricDataPropositionInput.cs b/backend/src/GraphQl/GeometricDataX/GeometricDataPropositionInput.cs similarity index 83% rename from backend/src/GraphQl/DataX/GeometricDataPropositionInput.cs rename to backend/src/GraphQl/GeometricDataX/GeometricDataPropositionInput.cs index 404ef5ad7..f14091bf3 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, diff --git a/backend/src/GraphQl/GeometricDataX/GeometricDataQueries.cs b/backend/src/GraphQl/GeometricDataX/GeometricDataQueries.cs new file mode 100644 index 000000000..5a2a091e4 --- /dev/null +++ b/backend/src/GraphQl/GeometricDataX/GeometricDataQueries.cs @@ -0,0 +1,95 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using Metabase.Data; +using Metabase.GraphQl.Requests; +using Microsoft.EntityFrameworkCore; + +namespace Metabase.GraphQl.GeometricDataX; + +[ExtendObjectType(nameof(Query))] +public sealed class GeometricDataQueries +{ + public async Task GetGeometricDataAsync( + Guid databaseId, + Guid id, + 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 + ); + } + + public Task GetAllGeometricDataAsync( + GeometricDataPropositionInput? where, + string? locale, + uint? first, + string? after, + uint? 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, + 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..69ca3178a --- /dev/null +++ b/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintDataLoaders.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 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 = + databaseContextFactory.CreateDbContext(); + return await databaseContext.GnuPgKeyFingerprints + .AsNoTrackingWithIdentityResolution() + .Where(_ => fingerprints.Contains(_.Fingerprint)) + .With(queryContext, Sorting.DefaultEntityOrder) + .ToDictionaryAsync(_ => _.Fingerprint, cancellationToken); + } +} diff --git a/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintFilterType.cs b/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintFilterType.cs index 753e9cb62..112cc252b 100644 --- a/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintFilterType.cs +++ b/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintFilterType.cs @@ -5,13 +5,17 @@ namespace Metabase.GraphQl.GnuPgKeyFingerprints; public class GnuPgKeyFingerprintFilterType - : 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(x => x.Id); + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); descriptor.Field(x => x.Fingerprint); descriptor.Field(x => x.CreatedAt); descriptor.Field(x => x.AllowedAt); 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..959bb31e2 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 ) { @@ -123,7 +125,7 @@ CancellationToken cancellationToken ) ) { - fingerprint.Allow(); + fingerprint.Allow(clock); } context.GnuPgKeyFingerprints.Add(fingerprint); await context.SaveChangesAsync(cancellationToken); @@ -137,6 +139,7 @@ public async Task AllowGnuPgKeyFingerprintAsync ClaimsPrincipal claimsPrincipal, GnuPgKeyFingerprintAuthorization authorization, ApplicationDbContext context, + IClock clock, CancellationToken cancellationToken ) { @@ -170,7 +173,7 @@ CancellationToken cancellationToken ) ); } - fingerprint.Allow(); + fingerprint.Allow(clock); await context.SaveChangesAsync(cancellationToken); return new AllowGnuPgKeyFingerprintPayload(fingerprint); } @@ -182,6 +185,7 @@ public async Task ForbidGnuPgKeyFingerprintAsy ClaimsPrincipal claimsPrincipal, GnuPgKeyFingerprintAuthorization authorization, ApplicationDbContext context, + IClock clock, CancellationToken cancellationToken ) { @@ -215,7 +219,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..cec7c6fad 100644 --- a/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintQueries.cs +++ b/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintQueries.cs @@ -1,9 +1,11 @@ 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; @@ -16,23 +18,28 @@ 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, + QueryContext queryContext, + 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, + QueryContext queryContext, CancellationToken cancellationToken ) { diff --git a/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintSortType.cs b/backend/src/GraphQl/GnuPgKeyFingerprints/GnuPgKeyFingerprintSortType.cs index aca5a3d33..9dbbd76bd 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.Name(nameof(GnuPgKeyFingerprintSortType)[..^"SortType".Length] + GraphQlConstants.SortInputSuffix); 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); } } \ 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 80% rename from backend/src/GraphQl/DataX/HygrothermalDataConnection.cs rename to backend/src/GraphQl/HygrothermalDataX/HygrothermalDataConnection.cs index 8ebb2a439..9515fef03 100644 --- a/backend/src/GraphQl/DataX/HygrothermalDataConnection.cs +++ b/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataConnection.cs @@ -1,7 +1,8 @@ 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, diff --git a/backend/src/GraphQl/DataX/HygrothermalDataEdge.cs b/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataEdge.cs similarity index 65% rename from backend/src/GraphQl/DataX/HygrothermalDataEdge.cs rename to backend/src/GraphQl/HygrothermalDataX/HygrothermalDataEdge.cs index e84d43ae7..8133577a2 100644 --- a/backend/src/GraphQl/DataX/HygrothermalDataEdge.cs +++ b/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataEdge.cs @@ -1,4 +1,6 @@ -namespace Metabase.GraphQl.DataX; +using Metabase.GraphQl.DataX; + +namespace Metabase.GraphQl.HygrothermalDataX; public sealed record HygrothermalDataEdge( string Cursor, diff --git a/backend/src/GraphQl/DataX/HygrothermalDataPropositionInput.cs b/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataPropositionInput.cs similarity index 81% rename from backend/src/GraphQl/DataX/HygrothermalDataPropositionInput.cs rename to backend/src/GraphQl/HygrothermalDataX/HygrothermalDataPropositionInput.cs index 9acad7f8b..b52f9f332 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, diff --git a/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataQueries.cs b/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataQueries.cs new file mode 100644 index 000000000..0363cb3dc --- /dev/null +++ b/backend/src/GraphQl/HygrothermalDataX/HygrothermalDataQueries.cs @@ -0,0 +1,95 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using Metabase.Data; +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, + 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 + ); + } + + public Task GetAllHygrothermalDataAsync( + HygrothermalDataPropositionInput? where, + string? locale, + uint? first, + string? after, + uint? 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, + 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..e66bb230c 100644 --- a/backend/src/GraphQl/InstitutionMethodDevelopers/InstitutionMethodDeveloperFilterType.cs +++ b/backend/src/GraphQl/InstitutionMethodDevelopers/InstitutionMethodDeveloperFilterType.cs @@ -1,16 +1,20 @@ 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(); + base.Configure(descriptor); + // TODO Remove CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); descriptor.Field(x => x.Method); descriptor.Field(x => x.Institution); } 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..ba7a60634 100644 --- a/backend/src/GraphQl/InstitutionMethodDevelopers/RemoveInstitutionMethodDeveloperPayload.cs +++ b/backend/src/GraphQl/InstitutionMethodDevelopers/RemoveInstitutionMethodDeveloperPayload.cs @@ -11,7 +11,10 @@ public RemoveInstitutionMethodDeveloperPayload( 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/InstitutionRepresentatives/InstitutionRepresentativeFilterType.cs b/backend/src/GraphQl/InstitutionRepresentatives/InstitutionRepresentativeFilterType.cs index 7d46ecb5f..14c1139a1 100644 --- a/backend/src/GraphQl/InstitutionRepresentatives/InstitutionRepresentativeFilterType.cs +++ b/backend/src/GraphQl/InstitutionRepresentatives/InstitutionRepresentativeFilterType.cs @@ -1,16 +1,20 @@ 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(); + base.Configure(descriptor); + // TODO Remove CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); descriptor.Field(x => x.Institution); descriptor.Field(x => x.User); descriptor.Field(x => x.Role); 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..c325aba29 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); + base.Configure(descriptor); descriptor.Field(x => x.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..14c4f7329 100644 --- a/backend/src/GraphQl/Institutions/InstitutionDevelopedMethodEdge.cs +++ b/backend/src/GraphQl/Institutions/InstitutionDevelopedMethodEdge.cs @@ -4,8 +4,11 @@ namespace Metabase.GraphQl.Institutions; public sealed class InstitutionDevelopedMethodEdge( - InstitutionMethodDeveloper association - ) - : Edge(association.MethodId) + InstitutionMethodDeveloper association, + string cursor +) +: PaginatedEdge( + association.MethodId, cursor +) { } \ 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..5be294271 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(x => x.Id); + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); descriptor.Field(_ => _.Name); descriptor.Field(_ => _.Abbreviation); descriptor.Field(_ => _.Description); 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..ea50eae6e 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; 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/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/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..ec5bc9af4 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; 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..fd215c188 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; 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 0585366d8..631267972 100644 --- a/backend/src/GraphQl/Institutions/InstitutionManufacturedComponentConnection.cs +++ b/backend/src/GraphQl/Institutions/InstitutionManufacturedComponentConnection.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; @@ -9,30 +10,35 @@ 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/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/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..037a31c85 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; 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/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..0fc6bd964 100644 --- a/backend/src/GraphQl/Institutions/InstitutionQueries.cs +++ b/backend/src/GraphQl/Institutions/InstitutionQueries.cs @@ -3,9 +3,11 @@ 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 +48,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..39a2a57f2 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; 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 80% rename from backend/src/GraphQl/DataX/LifeCycleDataConnection.cs rename to backend/src/GraphQl/LifeCycleDataX/LifeCycleDataConnection.cs index 77f77a724..91828fb9e 100644 --- a/backend/src/GraphQl/DataX/LifeCycleDataConnection.cs +++ b/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataConnection.cs @@ -1,7 +1,8 @@ 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, diff --git a/backend/src/GraphQl/DataX/LifeCycleDataEdge.cs b/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataEdge.cs similarity index 65% rename from backend/src/GraphQl/DataX/LifeCycleDataEdge.cs rename to backend/src/GraphQl/LifeCycleDataX/LifeCycleDataEdge.cs index 6118ae4cf..40da02411 100644 --- a/backend/src/GraphQl/DataX/LifeCycleDataEdge.cs +++ b/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataEdge.cs @@ -1,4 +1,6 @@ -namespace Metabase.GraphQl.DataX; +using Metabase.GraphQl.DataX; + +namespace Metabase.GraphQl.LifeCycleDataX; public sealed record LifeCycleDataEdge( string Cursor, diff --git a/backend/src/GraphQl/DataX/LifeCycleDataPropositionInput.cs b/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataPropositionInput.cs similarity index 81% rename from backend/src/GraphQl/DataX/LifeCycleDataPropositionInput.cs rename to backend/src/GraphQl/LifeCycleDataX/LifeCycleDataPropositionInput.cs index fa000e806..66c6d0292 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, diff --git a/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataQueries.cs b/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataQueries.cs new file mode 100644 index 000000000..411e60af1 --- /dev/null +++ b/backend/src/GraphQl/LifeCycleDataX/LifeCycleDataQueries.cs @@ -0,0 +1,95 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using Metabase.Data; +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, + 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 + ); + } + + public Task GetAllLifeCycleDataAsync( + LifeCycleDataPropositionInput? where, + string? locale, + uint? first, + string? after, + uint? 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, + 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/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..495e3c4ea 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; @@ -20,8 +21,8 @@ QueryContext queryContext ) { public async Task GetTotalCountAsync( - InstitutionMethodDevelopersByMethodIdDataLoader institutionMethodDevelopersDataLoader, - UserMethodDevelopersByMethodIdDataLoader userMethodDevelopersDataLoader, + 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 @@ -135,8 +138,8 @@ QueryContext queryContext ) { public async Task GetTotalCountAsync( - PendingInstitutionMethodDevelopersByMethodIdDataLoader pendingInstitutionMethodDevelopersDataLoader, - PendingUserMethodDevelopersByMethodIdDataLoader pendingUserMethodDevelopersDataLoader, + 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..0c1d39490 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(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); // Disjunctively compose the filters following filters taking into // account the "lifting" done in `MethodDeveloperConnection`. // descriptor 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..c24e584da 100644 --- a/backend/src/GraphQl/Methods/MethodFilterType.cs +++ b/backend/src/GraphQl/Methods/MethodFilterType.cs @@ -5,13 +5,17 @@ namespace Metabase.GraphQl.Methods; public class MethodFilterType - : 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(x => x.Id); + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); descriptor.Field(x => x.Name); descriptor.Field(x => x.Description); descriptor.Field(x => x.CalculationLocator); 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/MethodQueries.cs b/backend/src/GraphQl/Methods/MethodQueries.cs index db41e543d..0f22c3e4e 100644 --- a/backend/src/GraphQl/Methods/MethodQueries.cs +++ b/backend/src/GraphQl/Methods/MethodQueries.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.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..ba43b3c84 100644 --- a/backend/src/GraphQl/Methods/MethodSortType.cs +++ b/backend/src/GraphQl/Methods/MethodSortType.cs @@ -4,8 +4,8 @@ namespace Metabase.GraphQl.Methods; -public sealed class MethodSortType - : EntitySortType +public class MethodSortType + : AuditableEntitySortType { protected override void Configure( ISortInputTypeDescriptor descriptor @@ -15,6 +15,5 @@ ISortInputTypeDescriptor descriptor descriptor.Field(x => x.Name); descriptor.Field(x => x.Description); descriptor.Field(x => x.CalculationLocator); - descriptor.Field(x => x.Manager); } } \ 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/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..6d0b3ed8f --- /dev/null +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationDataLoaders.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.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 + ); + } +} \ 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..9e8b26d53 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationFilterType.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationFilterType.cs @@ -5,14 +5,17 @@ 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); + // TODO Remove Id, CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(x => x.Id); + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); descriptor.Field(x => x.ApplicationType); descriptor.Field(x => x.ClientId); descriptor.Field(x => x.ConsentType); @@ -20,5 +23,6 @@ IFilterInputTypeDescriptor descriptor // descriptor.Field(x => x.PostLogoutRedirectUris); // descriptor.Field(x => x.RedirectUris); // descriptor.Field(x => x.Requirements); + descriptor.Field(x => x.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 82% rename from backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationAuthorizationConnection.cs rename to backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationGrantedAuthorizationConnection.cs index 7e1c2e5e4..5ae3f4f2b 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationAuthorizationConnection.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationGrantedAuthorizationConnection.cs @@ -4,15 +4,17 @@ 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 ) { + [Cost(0)] public async Task GetTotalCountAsync( ClaimsPrincipal claimsPrincipal, Authorization.OpenIdConnectAuthorization authorization, @@ -27,7 +29,8 @@ CancellationToken cancellationToken return (uint)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 83% rename from backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationTokenConnection.cs rename to backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationIssuedTokenConnection.cs index b9b906386..433594589 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationTokenConnection.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationIssuedTokenConnection.cs @@ -4,15 +4,17 @@ 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 ) { + [Cost(0)] public async Task GetTotalCountAsync( ClaimsPrincipal claimsPrincipal, Authorization.OpenIdConnectAuthorization authorization, @@ -27,7 +29,8 @@ CancellationToken cancellationToken return (uint)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..f3e7bc7a7 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationMutations.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationMutations.cs @@ -89,10 +89,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(x => x.ToPermissionString()) + .Concat(input.GrantTypes.Select(x => x.ToPermissionString())) + .Concat(input.ResponseTypes.Select(x => x.ToPermissionString())) + .Concat(input.Scopes.Select(x => x.ToPermissionString())) ); if (input.RedirectUri is not null) { @@ -254,7 +254,7 @@ OpenIddictApplicationDescriptor descriptor { try { - permission.ToOpenIdConnectEndpoint(); + permission.PermissionToOpenIdConnectEndpoint(); return true; } catch (ArgumentOutOfRangeException) @@ -266,7 +266,7 @@ OpenIddictApplicationDescriptor descriptor { try { - permission.ToOpenIdConnectGrantType(); + permission.PermissionToOpenIdConnectGrantType(); return true; } catch (ArgumentOutOfRangeException) @@ -278,7 +278,7 @@ OpenIddictApplicationDescriptor descriptor { try { - permission.ToOpenIdConnectResponseType(); + permission.PermissionToOpenIdConnectResponseType(); return true; } catch (ArgumentOutOfRangeException) @@ -290,7 +290,7 @@ OpenIddictApplicationDescriptor descriptor { try { - permission.ToOpenIdConnectScope(); + permission.PermissionToOpenIdConnectScope(); return true; } catch (ArgumentOutOfRangeException) @@ -299,10 +299,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(x => x.ToPermissionString()) + .Concat(input.GrantTypes.Select(x => x.ToPermissionString())) + .Concat(input.ResponseTypes.Select(x => x.ToPermissionString())) + .Concat(input.Scopes.Select(x => x.ToPermissionString())) ); } } \ 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..07f31a124 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; @@ -42,33 +46,37 @@ CancellationToken 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, + public async 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; + return HotChocolate.Types.Pagination.Connection.Empty(); } + return await databaseContext.OpenIdConnectApplications + .AsNoTracking() + .With(resolverContext.GetQueryContext(), Sorting.DefaultEntityOrder) + .ToPageAsync(resolverContext.GetPagingArguments(), cancellationToken) + .ToConnectionAsync(); } [UseUserManager] [Authorize(Policy = AuthorizationPolicies.ManageOpenIdConnectScopePolicy)] public async Task GetOpenIdConnectApplicationAsync( Guid id, + IOpenIdConnectApplicationByIdDataLoader byId, ClaimsPrincipal claimsPrincipal, Authorization.OpenIdConnectAuthorization authorization, - OpenIddictApplicationManager applicationManager, CancellationToken cancellationToken ) { @@ -76,6 +84,6 @@ CancellationToken cancellationToken { return null; } - return await applicationManager.FindByIdAsync(id.ToString(), cancellationToken: cancellationToken); + return await byId.LoadAsync(id, 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..a95ca6155 --- /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(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); + } +} \ No newline at end of file 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..d082427d3 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationType.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectApplicationType.cs @@ -7,18 +7,14 @@ 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; namespace Metabase.GraphQl.OpenIdConnect.Applications; public sealed class OpenIdConnectApplicationType - : EntityType + : EntityType { protected override void Configure( IObjectTypeDescriptor descriptor @@ -35,6 +31,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 +39,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,6 +50,7 @@ IObjectTypeDescriptor descriptor descriptor .Field("endpoints") .Type>>>>() + .Cost(0) .Resolve(context => { var application = context.Parent(); @@ -64,7 +63,7 @@ IObjectTypeDescriptor descriptor { try { - permission.ToOpenIdConnectEndpoint(); + permission.PermissionToOpenIdConnectEndpoint(); return true; } catch (ArgumentOutOfRangeException) @@ -72,12 +71,13 @@ IObjectTypeDescriptor descriptor return false; } }) - ?.Select(endpoint => endpoint.ToOpenIdConnectEndpoint()) + ?.Select(endpointPermission => endpointPermission.PermissionToOpenIdConnectEndpoint()) .ToList() ?? []; }); descriptor .Field("grantTypes") .Type>>>>() + .Cost(0) .Resolve(context => { var application = context.Parent(); @@ -90,7 +90,7 @@ IObjectTypeDescriptor descriptor { try { - permission.ToOpenIdConnectGrantType(); + permission.PermissionToOpenIdConnectGrantType(); return true; } catch (ArgumentOutOfRangeException) @@ -98,12 +98,13 @@ IObjectTypeDescriptor descriptor return false; } }) - ?.Select(grantType => grantType.ToOpenIdConnectGrantType()) + ?.Select(grantTypePermission => grantTypePermission.PermissionToOpenIdConnectGrantType()) .ToList() ?? []; }); descriptor .Field("responseTypes") .Type>>>>() + .Cost(0) .Resolve(context => { var application = context.Parent(); @@ -116,7 +117,7 @@ IObjectTypeDescriptor descriptor { try { - permission.ToOpenIdConnectResponseType(); + permission.PermissionToOpenIdConnectResponseType(); return true; } catch (ArgumentOutOfRangeException) @@ -124,12 +125,13 @@ IObjectTypeDescriptor descriptor return false; } }) - ?.Select(responseType => responseType.ToOpenIdConnectResponseType()) + ?.Select(responseTypePermission => responseTypePermission.PermissionToOpenIdConnectResponseType()) .ToList() ?? []; }); descriptor .Field("scopes") .Type>>>>() + .Cost(0) .Resolve(context => { var application = context.Parent(); @@ -142,7 +144,7 @@ IObjectTypeDescriptor descriptor { try { - permission.ToOpenIdConnectScope(); + permission.PermissionToOpenIdConnectScope(); return true; } catch (ArgumentOutOfRangeException) @@ -150,12 +152,13 @@ IObjectTypeDescriptor descriptor return false; } }) - ?.Select(scope => scope.ToOpenIdConnectScope()) + ?.Select(scopePermission => scopePermission.PermissionToOpenIdConnectScope()) .ToList() ?? []; }); descriptor .Field(application => application.Requirements) .Type>>>>() + .Cost(0) .Resolve(context => { var application = context.Parent(); @@ -170,16 +173,19 @@ IObjectTypeDescriptor descriptor 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 +196,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(); diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectEndpointExtensions.cs b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectEndpointExtensions.cs index 98f83179a..e44a91112 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectEndpointExtensions.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectEndpointExtensions.cs @@ -1,14 +1,13 @@ using System; -using Metabase.Configuration; using OpenIddict.Abstractions; namespace Metabase.GraphQl.OpenIdConnect.Applications; public static class OpenIdConnectEndpointExtensions { - public static OpenIdConnectEndpoint ToOpenIdConnectEndpoint(this string endpoint) + public static OpenIdConnectEndpoint PermissionToOpenIdConnectEndpoint(this string endpointPermission) { - return endpoint switch + return endpointPermission switch { OpenIddictConstants.Permissions.Endpoints.Authorization => OpenIdConnectEndpoint.AUTHORIZATION, OpenIddictConstants.Permissions.Endpoints.EndSession => OpenIdConnectEndpoint.END_SESSION, @@ -16,11 +15,11 @@ 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) + 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..b7708007c 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectGrantTypeExtensions.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectGrantTypeExtensions.cs @@ -1,24 +1,23 @@ using System; -using Metabase.Configuration; using OpenIddict.Abstractions; namespace Metabase.GraphQl.OpenIdConnect.Applications; public static class OpenIdConnectGrantTypeExtensions { - public static OpenIdConnectGrantType ToOpenIdConnectGrantType(this string grantType) + public static OpenIdConnectGrantType PermissionToOpenIdConnectGrantType(this string grantTypePermission) { - return grantType switch + 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) + public static string ToPermissionString(this OpenIdConnectGrantType grantType) { return grantType switch { diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectResponseTypeExtensions.cs b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectResponseTypeExtensions.cs index 097b28c25..fd1fcfbc8 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectResponseTypeExtensions.cs +++ b/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectResponseTypeExtensions.cs @@ -1,23 +1,22 @@ using System; -using Metabase.Configuration; using OpenIddict.Abstractions; namespace Metabase.GraphQl.OpenIdConnect.Applications; public static class OpenIdConnectResponseTypeExtensions { - public static OpenIdConnectResponseType ToOpenIdConnectResponseType(this string responseType) + public static OpenIdConnectResponseType PermissionToOpenIdConnectResponseType(this string responseTypePermission) { - return responseType switch + 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) + public static string ToPermissionString(this OpenIdConnectResponseType responseType) { return responseType switch { 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..e6d92b81c 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); + // TODO Remove Id, CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(x => x.Id); + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); + descriptor.Field(x => x.CreationDate).Ignore(); 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); + descriptor.Field(x => x.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 71% rename from backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationTokenConnection.cs rename to backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationIssuedTokenConnection.cs index 7375b6a7a..8eb94e58b 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 ) { + [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); } - 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..b1d42536b 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 ) ) diff --git a/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationQueries.cs b/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationQueries.cs index 8467835b4..7b2f40408 100644 --- a/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationQueries.cs +++ b/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationQueries.cs @@ -1,53 +1,63 @@ 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 { + // TODO In all queries, instead of returning nothing, report as authentication error to client. + [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; + 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, CancellationToken cancellationToken ) { - if (!await authorization.IsAuthorizedToManageAuthorization(claimsPrincipal, id, authorizationManager, cancellationToken)) + if (!await authorization.IsAuthorizedToManageAuthorization(claimsPrincipal, id, cancellationToken)) { 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..8fb567d9c --- /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(x => x.CreationDate).Ignore(); + descriptor.Field(x => x.Status); + descriptor.Field(x => x.Subject); + descriptor.Field(x => x.Type); + } +} \ No newline at end of file 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..6dcf9c908 100644 --- a/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationType.cs +++ b/backend/src/GraphQl/OpenIdConnect/Authorizations/OpenIdConnectAuthorizationType.cs @@ -6,12 +6,14 @@ 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; namespace Metabase.GraphQl.OpenIdConnect.Authorizations; public sealed class OpenIdConnectAuthorizationType - : EntityType + : EntityType { protected override void Configure( IObjectTypeDescriptor descriptor @@ -20,8 +22,23 @@ 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(authorization => authorization.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>>() @@ -32,17 +49,18 @@ IObjectTypeDescriptor descriptor ); 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 @@ -51,11 +69,10 @@ 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/Applications/OpenIdConnectScope.cs b/backend/src/GraphQl/OpenIdConnect/OpenIdConnectScope.cs similarity index 87% rename from backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectScope.cs rename to backend/src/GraphQl/OpenIdConnect/OpenIdConnectScope.cs index a2eec4c6e..f9ccbf8a9 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectScope.cs +++ b/backend/src/GraphQl/OpenIdConnect/OpenIdConnectScope.cs @@ -1,6 +1,6 @@ using System.Diagnostics.CodeAnalysis; -namespace Metabase.GraphQl.OpenIdConnect.Applications; +namespace Metabase.GraphQl.OpenIdConnect; [SuppressMessage("Naming", "CA1707")] public enum OpenIdConnectScope diff --git a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectScopeExtensions.cs b/backend/src/GraphQl/OpenIdConnect/OpenIdConnectScopeExtensions.cs similarity index 70% rename from backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectScopeExtensions.cs rename to backend/src/GraphQl/OpenIdConnect/OpenIdConnectScopeExtensions.cs index 6d016a412..3a1449dcb 100644 --- a/backend/src/GraphQl/OpenIdConnect/Applications/OpenIdConnectScopeExtensions.cs +++ b/backend/src/GraphQl/OpenIdConnect/OpenIdConnectScopeExtensions.cs @@ -1,13 +1,35 @@ using System; using OpenIddict.Abstractions; -namespace Metabase.GraphQl.OpenIdConnect.Applications; +namespace Metabase.GraphQl.OpenIdConnect; public static class OpenIdConnectScopeExtensions { 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.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.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}`") + }; + } + + public static OpenIdConnectScope PermissionToOpenIdConnectScope(this string scopePermission) + { + return scopePermission switch { OpenIddictConstants.Permissions.Scopes.Address => OpenIdConnectScope.ADDRESS, OpenIddictConstants.Permissions.Scopes.Email => OpenIdConnectScope.EMAIL, @@ -23,11 +45,11 @@ public static OpenIdConnectScope ToOpenIdConnectScope(this string scope) 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}`") + _ => throw new ArgumentOutOfRangeException(nameof(scopePermission), $"Unsupported scope `{scopePermission}`") }; } - public static string ToStringScope(this OpenIdConnectScope scope) + public static string ToPermissionString(this OpenIdConnectScope scope) { return scope switch { 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..21bd2fdff 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); + // TODO Remove Id, CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(x => x.Id); + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); + descriptor.Field(x => x.CreationDate).Ignore(); + descriptor.Field(x => x.ExpirationDate).Name(OpenIdConnectTokenType.ExpiredAtName); + descriptor.Field(x => x.RedemptionDate).Name(OpenIdConnectTokenType.RedeemedAtName); 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); + descriptor.Field(x => x.Authorization); + descriptor.Field(x => x.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..59bdfd2bf 100644 --- a/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenQueries.cs +++ b/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenQueries.cs @@ -1,55 +1,64 @@ 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.Tokens; [ExtendObjectType(nameof(Query))] public sealed class OpenIdConnectTokenQueries { + // TODO In all queries, instead of returning nothing, report as authentication error to client. + [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; + 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, CancellationToken cancellationToken ) { - if (!await authorization.IsAuthorizedToManageToken(claimsPrincipal, id, tokenManager, cancellationToken)) + if (!await authorization.IsAuthorizedToManageToken(claimsPrincipal, id, cancellationToken)) { 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..bc5abfcda --- /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(x => x.CreationDate).Ignore(); + descriptor.Field(x => x.ExpirationDate).Name(OpenIdConnectTokenType.ExpiredAtName); + descriptor.Field(x => x.RedemptionDate).Name(OpenIdConnectTokenType.RedeemedAtName); + descriptor.Field(x => x.Status); + descriptor.Field(x => x.Subject); + descriptor.Field(x => x.Type); + } +} \ No newline at end of file diff --git a/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenType.cs b/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenType.cs index 4e426cb9b..fa390c47a 100644 --- a/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenType.cs +++ b/backend/src/GraphQl/OpenIdConnect/Tokens/OpenIdConnectTokenType.cs @@ -6,13 +6,15 @@ using Metabase.Data.OpenIdConnect; using Metabase.GraphQl.Users; using Metabase.GraphQl.Entities; -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,7 +24,14 @@ 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(t => t.Application) .Type>>() @@ -40,10 +49,11 @@ IObjectTypeDescriptor descriptor ) ); 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 @@ -52,11 +62,10 @@ 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/DataX/OpticalComponentSubtype.cs b/backend/src/GraphQl/OpticalDataX/OpticalComponentSubtype.cs similarity index 92% rename from backend/src/GraphQl/DataX/OpticalComponentSubtype.cs rename to backend/src/GraphQl/OpticalDataX/OpticalComponentSubtype.cs index 161f72f28..05bc892ea 100644 --- a/backend/src/GraphQl/DataX/OpticalComponentSubtype.cs +++ b/backend/src/GraphQl/OpticalDataX/OpticalComponentSubtype.cs @@ -1,6 +1,6 @@ using System.Diagnostics.CodeAnalysis; -namespace Metabase.GraphQl.DataX; +namespace Metabase.GraphQl.OpticalDataX; [SuppressMessage("Naming", "CA1707")] public enum OpticalComponentSubtype diff --git a/backend/src/GraphQl/DataX/OpticalComponentSubtypePropositionInput.cs b/backend/src/GraphQl/OpticalDataX/OpticalComponentSubtypePropositionInput.cs similarity index 86% rename from backend/src/GraphQl/DataX/OpticalComponentSubtypePropositionInput.cs rename to backend/src/GraphQl/OpticalDataX/OpticalComponentSubtypePropositionInput.cs index 4181e3163..db15d9aea 100644 --- a/backend/src/GraphQl/DataX/OpticalComponentSubtypePropositionInput.cs +++ b/backend/src/GraphQl/OpticalDataX/OpticalComponentSubtypePropositionInput.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace Metabase.GraphQl.DataX; +namespace Metabase.GraphQl.OpticalDataX; public sealed record OpticalComponentSubtypePropositionInput( OpticalComponentSubtype? EqualTo, 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 86% rename from backend/src/GraphQl/DataX/OpticalComponentTypePropositionInput.cs rename to backend/src/GraphQl/OpticalDataX/OpticalComponentTypePropositionInput.cs index 6c95d03da..9000a26f6 100644 --- a/backend/src/GraphQl/DataX/OpticalComponentTypePropositionInput.cs +++ b/backend/src/GraphQl/OpticalDataX/OpticalComponentTypePropositionInput.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace Metabase.GraphQl.DataX; +namespace Metabase.GraphQl.OpticalDataX; public sealed record OpticalComponentTypePropositionInput( OpticalComponentType? EqualTo, diff --git a/backend/src/GraphQl/OpticalDataX/OpticalData.cs b/backend/src/GraphQl/OpticalDataX/OpticalData.cs new file mode 100644 index 000000000..d1e0625c1 --- /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 82% rename from backend/src/GraphQl/DataX/OpticalDataConnection.cs rename to backend/src/GraphQl/OpticalDataX/OpticalDataConnection.cs index 61f82e621..374158af6 100644 --- a/backend/src/GraphQl/DataX/OpticalDataConnection.cs +++ b/backend/src/GraphQl/OpticalDataX/OpticalDataConnection.cs @@ -2,8 +2,9 @@ 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, diff --git a/backend/src/GraphQl/DataX/OpticalDataEdge.cs b/backend/src/GraphQl/OpticalDataX/OpticalDataEdge.cs similarity index 65% rename from backend/src/GraphQl/DataX/OpticalDataEdge.cs rename to backend/src/GraphQl/OpticalDataX/OpticalDataEdge.cs index d864d96f2..cab889744 100644 --- a/backend/src/GraphQl/DataX/OpticalDataEdge.cs +++ b/backend/src/GraphQl/OpticalDataX/OpticalDataEdge.cs @@ -1,6 +1,6 @@ -using System; +using Metabase.GraphQl.DataX; -namespace Metabase.GraphQl.DataX; +namespace Metabase.GraphQl.OpticalDataX; public sealed record OpticalDataEdge( string Cursor, diff --git a/backend/src/GraphQl/DataX/OpticalDataPropositionInput.cs b/backend/src/GraphQl/OpticalDataX/OpticalDataPropositionInput.cs similarity index 92% rename from backend/src/GraphQl/DataX/OpticalDataPropositionInput.cs rename to backend/src/GraphQl/OpticalDataX/OpticalDataPropositionInput.cs index 47c818191..3a642faac 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, diff --git a/backend/src/GraphQl/OpticalDataX/OpticalDataQueries.cs b/backend/src/GraphQl/OpticalDataX/OpticalDataQueries.cs new file mode 100644 index 000000000..78bb125b6 --- /dev/null +++ b/backend/src/GraphQl/OpticalDataX/OpticalDataQueries.cs @@ -0,0 +1,95 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using Metabase.Data; +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, + 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 + ); + } + + public Task GetAllOpticalDataAsync( + OpticalDataPropositionInput? where, + string? locale, + uint? first, + string? after, + uint? 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, + 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..0d02b7dc5 --- /dev/null +++ b/backend/src/GraphQl/PaginationHelpers.cs @@ -0,0 +1,25 @@ +using System; +using System.Text; + +namespace Metabase.GraphQl; + +public static class PaginationHelpers +{ + public static string ConstructCursor(Guid id) + { + return Convert.ToBase64String( + Encoding.UTF8.GetBytes( + id.ToString("D") + ) + ); + } + + public static string ConstructCursor(Guid id1, Guid id2) + { + return Convert.ToBase64String( + Encoding.UTF8.GetBytes( + $"{id1:D}:{id2:D}" + ) + ); + } +} \ 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 81% rename from backend/src/GraphQl/DataX/PhotovoltaicDataConnection.cs rename to backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataConnection.cs index 70db869cc..fcaaf3cce 100644 --- a/backend/src/GraphQl/DataX/PhotovoltaicDataConnection.cs +++ b/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataConnection.cs @@ -1,8 +1,9 @@ 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, diff --git a/backend/src/GraphQl/DataX/PhotovoltaicDataEdge.cs b/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataEdge.cs similarity index 65% rename from backend/src/GraphQl/DataX/PhotovoltaicDataEdge.cs rename to backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataEdge.cs index d23d5e4e0..25316e7e8 100644 --- a/backend/src/GraphQl/DataX/PhotovoltaicDataEdge.cs +++ b/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataEdge.cs @@ -1,4 +1,6 @@ -namespace Metabase.GraphQl.DataX; +using Metabase.GraphQl.DataX; + +namespace Metabase.GraphQl.PhotovoltaicDataX; public sealed record PhotovoltaicDataEdge( string Cursor, diff --git a/backend/src/GraphQl/DataX/PhotovoltaicDataPropositionInput.cs b/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataPropositionInput.cs similarity index 81% rename from backend/src/GraphQl/DataX/PhotovoltaicDataPropositionInput.cs rename to backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataPropositionInput.cs index f0d573649..891ce3e0f 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, diff --git a/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataQueries.cs b/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataQueries.cs new file mode 100644 index 000000000..9dd93fb23 --- /dev/null +++ b/backend/src/GraphQl/PhotovoltaicDataX/PhotovoltaicDataQueries.cs @@ -0,0 +1,95 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using HotChocolate.Resolvers; +using HotChocolate.Types; +using Metabase.Data; +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, + 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 + ); + } + + public Task GetAllPhotovoltaicDataAsync( + PhotovoltaicDataPropositionInput? where, + string? locale, + uint? first, + string? after, + uint? 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, + 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/Requests/DataQueries.cs b/backend/src/GraphQl/Requests/DataQueries.cs new file mode 100644 index 000000000..bdc0990c6 --- /dev/null +++ b/backend/src/GraphQl/Requests/DataQueries.cs @@ -0,0 +1,1127 @@ +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using GraphQL; +using HotChocolate; +using HotChocolate.Resolvers; +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 Microsoft.Extensions.Logging; +using Microsoft.EntityFrameworkCore; +using System.Collections.Generic; +using System.Text; +using System.Collections.Immutable; +using System.Linq; +using HotChocolate.Types.Pagination; +using Metabase.Extensions; +using System.Text.Json.Serialization; + +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( + ApplicationDbContext databaseContext, + QueryingDatabases queryingDatabases, + GraphQlRequestHelper graphQlRequestHelper, + AppSettings appSettings, + 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 static bool IsIgsdbDatabase(Database database) + { + return database.Id == new Guid(DataConstants.IgsdbDatabaseUuid); + } + + 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 Convert.ToBase64String( + Encoding.UTF8.GetBytes( + JsonSerializer.Serialize(cursor, JsonSerializerSettings.Compact) + ) + ); + } + + private static CompoundCursor? DeserializeCompoundCursor(string? cursors) + { + if (cursors is null) + { + return null; + } + return JsonSerializer.Deserialize( + Encoding.UTF8.GetString( + Convert.FromBase64String(cursors) + ), + JsonSerializerSettings.Compact + ); + } + + private enum PaginationDirection + { + FORWARD, + BACKWARD + } + + public async Task GetAllDataAsync( + uint? first, + string? after, + uint? last, + string? before, + Func, uint, ConnectionPageInfo, TDataConnection> createDataConnection, + Func createDataEdge, + Func> getAllDataAsync, + CancellationToken cancellationToken + ) + where TDataConnection : DataConnectionBase + where TDataEdge : DataEdgeBase + { + 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 + var connections = await Task.WhenAll( + rotatedDatabases.Select((database) => + getAllDataAsync( + database, + first is null ? null : first + 1, + compoundAfter?.Cursors.GetValueOrDefault(database.Id)?.Before, + last is null ? null : last + 1, + 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) + var cappedFirst = (int)Math.Min(first ?? GraphQlConstants.MaximumPageSize, GraphQlConstants.MaximumPageSize); + var cappedLast = (int)Math.Min(last ?? GraphQlConstants.MaximumPageSize, GraphQlConstants.MaximumPageSize); + 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, (uint)totalCount, pageInfo); + } + + public async Task HasDataAsync( + Func> hasDataAsync, + CancellationToken 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, + 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, + 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, + 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, + 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, + 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, + 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, + 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, + 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, + string? locale, + uint? first, + string? after, + uint? 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, + string? locale, + uint? first, + string? after, + uint? 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, + string? locale, + uint? first, + string? after, + uint? 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, + string? locale, + uint? first, + string? after, + uint? 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, + string? locale, + uint? first, + string? after, + uint? 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, + string? locale, + uint? first, + string? after, + uint? 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, + 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, + 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, + 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, + 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, + 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, + 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, + 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; + }, + 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..5eeeb3e4c --- /dev/null +++ b/backend/src/GraphQl/Requests/GraphQlRequestHelper.cs @@ -0,0 +1,125 @@ +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) + ); + resolverContext.ReportError( + 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() + ); + return null; + } + catch (JsonException exception) + { + logger.FailedToDeserialize( + exception, + databaseLocator, + JsonSerializer.Serialize(request, JsonSerializerSettings.GraphQl), + exception.BytePositionInLine, + exception.LineNumber, + exception.Message, + exception.Path + ); + resolverContext.ReportError( + 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() + ); + return null; + } + catch (Exception exception) + { + logger.FailedToRequestOrDeserialize( + exception, + databaseLocator, + JsonSerializer.Serialize(request, JsonSerializerSettings.GraphQl) + ); + resolverContext.ReportError( + 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() + ); + 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 99% rename from backend/src/Services/QueryingDatabases.cs rename to backend/src/GraphQl/Requests/QueryingDatabases.cs index 822ff8ffe..e6294c9e9 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 { @@ -190,7 +190,6 @@ await httpClient.PostAsync( $"The status code is not {HttpStatusCode.OK} but {httpResponseMessage.StatusCode}.", null, 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 httpResponseMessage.Content diff --git a/backend/src/GraphQl/LocaleType.cs b/backend/src/GraphQl/Scalars/LocaleType.cs similarity index 98% rename from backend/src/GraphQl/LocaleType.cs rename to backend/src/GraphQl/Scalars/LocaleType.cs index 770cb0fc2..02297c5cc 100644 --- a/backend/src/GraphQl/LocaleType.cs +++ b/backend/src/GraphQl/Scalars/LocaleType.cs @@ -2,7 +2,7 @@ using HotChocolate.Types; // TODO Maybe use an enumeration as runtime type instead of string (and fallback to english when the given one does not exist).namespace Database.GraphQl -namespace Metabase.GraphQl; +namespace Metabase.GraphQl.Scalars; /// /// [BCP 47](https://tools.ietf.org/html/bcp47) diff --git a/backend/src/GraphQl/Scalars/MyUriType.cs b/backend/src/GraphQl/Scalars/MyUriType.cs new file mode 100644 index 000000000..c2ae572d0 --- /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 Metabase.GraphQl; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Types; + +// 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..972a8d191 --- /dev/null +++ b/backend/src/GraphQl/Scalars/NonNegativeIntType.cs @@ -0,0 +1,71 @@ +using System.Text.Json; +using HotChocolate.Language; +using HotChocolate.Text.Json; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Types; + +/// +/// +/// 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..75c68bb46 --- /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..3454d6b46 100644 --- a/backend/src/GraphQl/UserMethodDevelopers/RemoveUserMethodDeveloperPayload.cs +++ b/backend/src/GraphQl/UserMethodDevelopers/RemoveUserMethodDeveloperPayload.cs @@ -11,7 +11,10 @@ public RemoveUserMethodDeveloperPayload( 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/UserMethodDeveloperFilterType.cs b/backend/src/GraphQl/UserMethodDevelopers/UserMethodDeveloperFilterType.cs index fb499f743..99aa9556c 100644 --- a/backend/src/GraphQl/UserMethodDevelopers/UserMethodDeveloperFilterType.cs +++ b/backend/src/GraphQl/UserMethodDevelopers/UserMethodDeveloperFilterType.cs @@ -1,16 +1,20 @@ 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(); + base.Configure(descriptor); + // TODO Remove CreatedAt, and UpdatedAt once the base.Configure is respected. + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.UpdatedAt); descriptor.Field(x => x.Method); descriptor.Field(x => x.User); } 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/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/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/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..9009daa92 100644 --- a/backend/src/GraphQl/Users/UserDevelopedMethodEdge.cs +++ b/backend/src/GraphQl/Users/UserDevelopedMethodEdge.cs @@ -4,8 +4,12 @@ namespace Metabase.GraphQl.Users; public sealed class UserDevelopedMethodEdge( - UserMethodDeveloper association - ) - : Edge(association.MethodId) + UserMethodDeveloper association, + string cursor +) +: PaginatedEdge( + association.MethodId, + cursor +) { } \ 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..21093af32 100644 --- a/backend/src/GraphQl/Users/UserFilterType.cs +++ b/backend/src/GraphQl/Users/UserFilterType.cs @@ -5,15 +5,20 @@ 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(x => x.Id); + descriptor.Field(x => x.CreatedAt); + descriptor.Field(x => x.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.Name); // descriptor.Field(x => x.Email); // descriptor.Field(x => x.PostalAddress); // descriptor.Field(x => x.WebsiteLocator); 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..2eb025658 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; 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/UserQueries.cs b/backend/src/GraphQl/Users/UserQueries.cs index 7aaa5f2aa..4638db867 100644 --- a/backend/src/GraphQl/Users/UserQueries.cs +++ b/backend/src/GraphQl/Users/UserQueries.cs @@ -3,9 +3,11 @@ 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 +36,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..d4c84e9d3 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; 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..0aae5beee 100644 --- a/backend/src/GraphQl/Users/UserSortType.cs +++ b/backend/src/GraphQl/Users/UserSortType.cs @@ -5,15 +5,16 @@ 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.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(x => x.Name); // descriptor.Field(x => x.Email); // descriptor.Field(x => x.PostalAddress); // descriptor.Field(x => x.WebsiteLocator); diff --git a/backend/src/GraphQl/Users/UserType.cs b/backend/src/GraphQl/Users/UserType.cs index 0389e481c..693803303 100644 --- a/backend/src/GraphQl/Users/UserType.cs +++ b/backend/src/GraphQl/Users/UserType.cs @@ -24,7 +24,7 @@ namespace Metabase.GraphQl.Users; public sealed class UserType - : EntityType + : EntityType { private static async Task Authorize( IResolverContext context, @@ -136,17 +136,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>>() + .Cost(0) .Resolve(async context => new ContactInformation( await Authorize(context, user => user.PhoneNumber, Scopes.Phone), @@ -160,6 +163,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 +171,7 @@ await Authorize(context, user => user.WebsiteLocator, Scopes.Profile) descriptor .Field("hasPassword") .Type() + .Cost(0) .Resolve(context => AuthorizeAsync( context, @@ -177,6 +182,7 @@ await Authorize(context, user => user.WebsiteLocator, Scopes.Profile) .UseUserManager(); descriptor .Field("roles") + .Cost(0) .Resolve(context => AuthorizeAsync( context, @@ -187,33 +193,41 @@ await Authorize(context, user => user.WebsiteLocator, Scopes.Profile) .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 +235,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 +249,7 @@ await Authorize(context, user => user.WebsiteLocator, Scopes.Profile) .Field(t => t.RepresentedInstitutions) .Type>>() .UseFiltering() + .UseSorting() .Resolve(context => new UserRepresentedInstitutionConnection( context.Parent(), @@ -243,6 +261,7 @@ await Authorize(context, user => user.WebsiteLocator, Scopes.Profile) .Type>() .Authorize(AuthorizationPolicies.WriteScopePolicy) .UseFiltering() + .UseSorting() .Resolve(context => new PendingUserRepresentedInstitutionConnection( context.Parent(), @@ -253,6 +272,7 @@ await Authorize(context, user => user.WebsiteLocator, Scopes.Profile) .Field(t => t.GnuPgKeyFingerprints) .Type>>() .UseFiltering() + .UseSorting() .Resolve(context => new UserGnuPgKeyFingerprintConnection( context.Parent(), @@ -262,6 +282,7 @@ await Authorize(context, user => user.WebsiteLocator, Scopes.Profile) descriptor .Field("has" + nameof(GnuPgKeyFingerprint)) .UseFiltering() + .UseSorting() .ResolveWith(x => UserResolvers.HasGnuPgKeyFingerprintsAsync(default!, default!, default!, default!)); } 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..73681a48c 100644 --- a/backend/src/Metabase.csproj +++ b/backend/src/Metabase.csproj @@ -13,34 +13,35 @@ - - + - - - - - - - - - + + + + + + + + + + - - - - - - - + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - + + + @@ -48,25 +49,25 @@ - - - - - - - - - - - - + + + + + + + + + + + + - + 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/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/ApplicationDbContextModelSnapshot.cs b/backend/src/Migrations/ApplicationDbContextModelSnapshot.cs index 421851491..92a16a3f5 100644 --- a/backend/src/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/backend/src/Migrations/ApplicationDbContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using System.Text.Json; using Metabase.Data; @@ -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() @@ -88,12 +98,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 +135,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 +166,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 +200,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"); @@ -156,6 +230,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid") .HasDefaultValueSql("gen_random_uuid()"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("Description") .IsRequired() .HasColumnType("text"); @@ -177,6 +256,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() @@ -197,6 +281,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid") .HasDefaultValueSql("gen_random_uuid()"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("Description") .IsRequired() .HasColumnType("text"); @@ -212,6 +301,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"); @@ -242,8 +336,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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 +351,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"); @@ -286,6 +387,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Abbreviation") .HasColumnType("text"); + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("now()"); + b.Property("Description") .IsRequired() .HasColumnType("text"); @@ -306,6 +412,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() @@ -327,9 +438,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 +472,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"); @@ -375,6 +518,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 +534,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"); @@ -406,7 +559,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); b.Property("ApplicationType") .HasMaxLength(50) @@ -432,6 +586,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 +621,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() @@ -482,7 +646,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); b.Property("ApplicationId") .HasColumnType("uuid"); @@ -492,6 +657,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 +683,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() @@ -530,13 +705,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .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"); @@ -559,6 +738,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Resources") .HasColumnType("text"); + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + b.Property("Version") .IsConcurrencyToken() .ValueGeneratedOnAddOrUpdate() @@ -577,7 +759,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); b.Property("ApplicationId") .HasColumnType("uuid"); @@ -590,6 +773,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 +809,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() @@ -694,7 +887,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid"); + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); b.Property("AccessFailedCount") .HasColumnType("integer"); @@ -703,6 +897,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 +945,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)"); @@ -824,9 +1028,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 a17340535..f55e2c020 100644 --- a/backend/src/Migrations/migrate.sql +++ b/backend/src/Migrations/migrate.sql @@ -2293,3 +2293,301 @@ BEGIN 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; + 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/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/Program.cs b/backend/src/Program.cs index c569a0428..e515aeffd 100644 --- a/backend/src/Program.cs +++ b/backend/src/Program.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; +using HotChocolate; using Metabase.Data; using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; @@ -90,9 +91,8 @@ string[] commandLineArguments } await SeedDatabase(scope.ServiceProvider); } - - await application.RunAsync(); - return 0; + // dotnet run -- schema export --output ./schema.graphql + return await application.RunWithGraphQLCommandsAsync(commandLineArguments); } catch (Exception exception) when (exception is not HostAbortedException && exception.Source != "Microsoft.EntityFrameworkCore.Design") // see https://github.com/dotnet/efcore/issues/29923 { diff --git a/backend/src/Services/EmailSender.cs b/backend/src/Services/EmailSender.cs index b0e2c00b5..f0c1cc692 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}" + $"metabase@{appSettings.Host}" ) ); message.To.Add( diff --git a/backend/src/Startup.cs b/backend/src/Startup.cs index c057cd723..e18bcd9db 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; @@ -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 | @@ -283,7 +286,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) @@ -315,11 +320,11 @@ public void Configure(WebApplication app) app.UseStaticFiles(); app.UseCookiePolicy(); // [SameSite cookies](https://learn.microsoft.com/en-us/aspnet/core/security/samesite) app.UseRouting(); - // TODO Do we really want this? See https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization?view=aspnetcore-5.0 + // [Localization](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization) app.UseRequestLocalization(_ => { - _.AddSupportedCultures("en-US", "de-DE"); - _.AddSupportedUICultures("en-US", "de-DE"); + _.AddSupportedCultures("en-US"); + _.AddSupportedUICultures("en-US"); _.SetDefaultCulture("en-US"); }); app.UseCors(); @@ -342,25 +347,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/test/AuditableTests.cs b/backend/test/AuditableTests.cs new file mode 100644 index 000000000..3e3ae745e --- /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))); + Assert.That(entity.UpdatedAt, Is.EqualTo(startInstant.WithOffset(Offset.Zero))); + }); + // Act + var duration = Duration.FromHours(1); + fakeClock.Advance(duration); + var updatedInstant = startInstant.Plus(duration); + entity.Update("Name", "Description", null, null, null, [], [], []); + await context.SaveChangesAsync(); + // Assert + Assert.Multiple(() => + { + Assert.That(entity.CreatedAt, Is.EqualTo(startInstant.WithOffset(Offset.Zero)), "CreatedAt should not change on update."); + Assert.That(entity.UpdatedAt, Is.EqualTo(updatedInstant.WithOffset(Offset.Zero)), "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/GraphQl/__snapshots__/GraphQlSchemaTests.IsUnchanged.snap b/backend/test/Integration/GraphQl/__snapshots__/GraphQlSchemaTests.IsUnchanged.snap index dd06ce132..77b33725d 100644 --- a/backend/test/Integration/GraphQl/__snapshots__/GraphQlSchemaTests.IsUnchanged.snap +++ b/backend/test/Integration/GraphQl/__snapshots__/GraphQlSchemaTests.IsUnchanged.snap @@ -16,11 +16,15 @@ interface Approval { 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 @@ -179,7 +183,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 +208,14 @@ type CalorimetricData implements Data { } type CalorimetricDataConnection { - edges: [CalorimetricDataEdge!]! - pageInfo: PageInfo! + edges: [CalorimetricDataEdge!]! @cost(weight: "0") + pageInfo: PageInfo! @cost(weight: "0") totalCount: NonNegativeInt! } type CalorimetricDataEdge { cursor: String! - node: CalorimetricData! + node: CalorimetricData! @cost(weight: "0") } type ChangeInstitutionRepresentativeRoleError implements UserError { @@ -259,101 +263,103 @@ type CielabColor { type Component implements Node { abbreviation: String - assembledOf(where: ComponentAssembledOfFilterInput @cost(weight: "10")): ComponentAssembledOfConnection! @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 @authorize(policy: "WriteScope") @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: NonNegativeInt! } 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: NonNegativeInt! } 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: NonNegativeInt! } 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: NonNegativeInt! } 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: NonNegativeInt! } 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") + edges: [ComponentVariantOfEdge!]! @cost(weight: "0") + isAuthorizedToAddEdge: Boolean! @cost(weight: "1") + totalCount: NonNegativeInt! } type ComponentVariantOfEdge { - isAuthorizedToRemoveEdge: Boolean! @cost(weight: "10") - node: Component! @cost(weight: "10") + isAuthorizedToRemoveEdge: Boolean! @cost(weight: "1") + node: Component! @cost(weight: "0") } "A connection to a list of items." @@ -533,7 +539,7 @@ type CreateOpenIdConnectApplicationPayload { } type CrossDatabaseDataReference { - database: Institution @cost(weight: "10") + database: Database @cost(weight: "10") databaseId: Uuid! dataId: Uuid! dataKind: DataKind! @@ -553,31 +559,33 @@ type DataApproval implements Approval { } type DataConnection { - edges: [DataEdge!]! - pageInfo: PageInfo! + edges: [DataEdge!]! @cost(weight: "0") + pageInfo: PageInfo! @cost(weight: "0") totalCount: NonNegativeInt! } type DataEdge { cursor: String! - node: Data! + node: Data! @cost(weight: "0") } type DataFormat implements Node { + createdAt: DateTime! description: String! extension: String id: ID! - isAuthorizedToUpdateNode: Boolean! @cost(weight: "10") + isAuthorizedToUpdateNode: Boolean! @cost(weight: "1") manager: DataFormatManagerEdge! @cost(weight: "10") mediaType: String! name: String! - reference: Reference @cost(weight: "10") + reference: Reference @cost(weight: "0") schemaLocator: Url - uuid: Uuid! @cost(weight: "10") + updatedAt: DateTime! + uuid: Uuid! } type DataFormatManagerEdge { - node: Institution! @cost(weight: "10") + node: Institution! @cost(weight: "0") } "A connection to a list of items." @@ -606,6 +614,7 @@ type Database implements Node { 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") + createdAt: DateTime! data(id: Uuid! kind: DataKind! locale: Locale): Data @cost(weight: "10") description: String! geometricData(id: Uuid! locale: Locale): GeometricData @cost(weight: "10") @@ -618,21 +627,22 @@ type Database implements Node { 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") - isAuthorizedToVerifyNode: Boolean! @cost(weight: "10") + isAuthorizedToUpdateNode: Boolean! @cost(weight: "1") + isAuthorizedToVerifyNode: Boolean! @cost(weight: "1") lifeCycleData(id: Uuid! locale: Locale): LifeCycleData @cost(weight: "10") locator: Url! 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") + updatedAt: DateTime! + uuid: Uuid! verificationCode: String! verificationState: DatabaseVerificationState! } type DatabaseOperatorEdge { - node: Institution! @cost(weight: "10") + node: Institution! @cost(weight: "0") } "A connection to a list of items." @@ -712,7 +722,7 @@ type DeleteUserPayload { type DescriptionOrReference { description: String - reference: Reference @cost(weight: "10") + reference: Reference @cost(weight: "0") } type DisableUserTwoFactorAuthenticationError implements UserError { @@ -799,7 +809,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 +833,14 @@ type GeometricData implements Data { } type GeometricDataConnection { - edges: [GeometricDataEdge!]! - pageInfo: PageInfo! + edges: [GeometricDataEdge!]! @cost(weight: "0") + pageInfo: PageInfo! @cost(weight: "0") totalCount: NonNegativeInt! } type GeometricDataEdge { cursor: String! - node: GeometricData! + node: GeometricData! @cost(weight: "0") } type GetHttpsResource { @@ -866,18 +876,19 @@ 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") + node: Institution! @cost(weight: "0") } type GnuPgKeyFingerprintUserEdge { - node: User! @cost(weight: "10") + node: User! @cost(weight: "0") } "A connection to a list of items." @@ -898,7 +909,7 @@ type GnuPgKeyFingerprintsEdge { node: GnuPgKeyFingerprint! } -type HygrothermalData implements Data { +type HygrothermalData implements Node & Data { appliedMethod: AppliedMethod! approvals: [DataApproval!]! component: Component @cost(weight: "10") @@ -921,60 +932,64 @@ type HygrothermalData implements Data { } type HygrothermalDataConnection { - edges: [HygrothermalDataEdge!]! - pageInfo: PageInfo! + edges: [HygrothermalDataEdge!]! @cost(weight: "0") + pageInfo: PageInfo! @cost(weight: "0") totalCount: NonNegativeInt! } 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 @authorize(policy: "ManageInstitutionRepresentativeScope") @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! + 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: NonNegativeInt! } type InstitutionGnuPgKeyFingerprintEdge { @@ -982,68 +997,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: NonNegativeInt! } type InstitutionOperatedDatabaseEdge { @@ -1051,26 +1076,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: NonNegativeInt! } 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: NonNegativeInt! } 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 +1117,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 +1140,14 @@ type LifeCycleData implements Data { } type LifeCycleDataConnection { - edges: [LifeCycleDataEdge!]! - pageInfo: PageInfo! + edges: [LifeCycleDataEdge!]! @cost(weight: "0") + pageInfo: PageInfo! @cost(weight: "0") totalCount: NonNegativeInt! } type LifeCycleDataEdge { cursor: String! - node: LifeCycleData! + node: LifeCycleData! @cost(weight: "0") } type LoginUserError implements UserError { @@ -1178,35 +1203,37 @@ 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 @authorize(policy: "WriteScope") @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") + isAuthorizedToAddInstitutionEdge: Boolean! @cost(weight: "1") + isAuthorizedToAddUserEdge: Boolean! @cost(weight: "1") totalCount: NonNegativeInt! @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." @@ -1333,90 +1360,131 @@ 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") + 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") -} - -type OpenIdConnectApplicationAuthorizationConnection { - edges: [OpenIdConnectApplicationAuthorizationEdge!]! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + isAuthorizedToManageNode: Boolean! @cost(weight: "1") + owner: OpenIdConnectApplicationOwnerEdge! @cost(weight: "0") + postLogoutRedirectUri: Url + redirectUri: Url + requirements: [OpenIdConnectRequirement!]! @cost(weight: "0") + responseTypes: [OpenIdConnectResponseType!]! @cost(weight: "0") + scopes: [OpenIdConnectScope!]! @cost(weight: "0") + tokens: OpenIdConnectApplicationIssuedTokenConnection! @cost(weight: "10") + updatedAt: DateTime! + uuid: Uuid! +} + +type OpenIdConnectApplicationGrantedAuthorizationConnection { + edges: [OpenIdConnectApplicationGrantedAuthorizationEdge!]! @cost(weight: "0") + totalCount: NonNegativeInt! } -type OpenIdConnectApplicationAuthorizationEdge { +type OpenIdConnectApplicationGrantedAuthorizationEdge { node: OpenIdConnectAuthorization! } +type OpenIdConnectApplicationIssuedTokenConnection { + edges: [OpenIdConnectApplicationIssuedTokenEdge!]! @cost(weight: "0") + totalCount: NonNegativeInt! +} + +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 + 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") + 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: NonNegativeInt! } -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 + createdAt: DateTime! + expiredAt: LocalDateTime id: ID! - isAuthorizedToRevokeNode: Boolean! @cost(weight: "10") - redemptionDate: DateTime + isAuthorizedToRevokeNode: Boolean! @cost(weight: "1") + redeemedAt: LocalDateTime status: String subject: String type: String - uuid: Uuid! @cost(weight: "10") + updatedAt: DateTime! + uuid: Uuid! } type OpenIdConnectTokenApplicationEdge { @@ -1427,7 +1495,25 @@ 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!]! @@ -1460,14 +1546,14 @@ type OpticalData implements Data { } type OpticalDataConnection { - edges: [OpticalDataEdge!]! - pageInfo: PageInfo! + edges: [OpticalDataEdge!]! @cost(weight: "0") + pageInfo: PageInfo! @cost(weight: "0") totalCount: NonNegativeInt! } type OpticalDataEdge { cursor: String! - node: OpticalData! + node: OpticalData! @cost(weight: "0") } "Information about pagination in a connection." @@ -1483,9 +1569,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: NonNegativeInt! } "A connection to a list of items." @@ -1507,21 +1593,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: NonNegativeInt! } "A connection to a list of items." @@ -1544,24 +1632,25 @@ type PendingInstitutionsEdge { type PendingMethodDeveloperConnection { edges: [MethodDeveloperEdge!]! @cost(weight: "10") - isAuthorizedToAddInstitutionEdge: Boolean! @cost(weight: "10") - isAuthorizedToAddUserEdge: Boolean! @cost(weight: "10") + isAuthorizedToAddInstitutionEdge: Boolean! @cost(weight: "1") + isAuthorizedToAddUserEdge: Boolean! @cost(weight: "1") totalCount: NonNegativeInt! @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: NonNegativeInt! } -type PhotovoltaicData implements Data { +type PhotovoltaicData implements Node & Data { appliedMethod: AppliedMethod! approvals: [DataApproval!]! component: Component @cost(weight: "10") @@ -1584,14 +1673,14 @@ type PhotovoltaicData implements Data { } type PhotovoltaicDataConnection { - edges: [PhotovoltaicDataEdge!]! - pageInfo: PageInfo! + edges: [PhotovoltaicDataEdge!]! @cost(weight: "0") + pageInfo: PageInfo! @cost(weight: "0") totalCount: NonNegativeInt! } type PhotovoltaicDataEdge { cursor: String! - node: PhotovoltaicData! + node: PhotovoltaicData! @cost(weight: "0") } type PrimeSurfaceOrDirection { @@ -1616,35 +1705,53 @@ type Publication { } type Query { + allCalorimetricData(after: String before: String first: NonNegativeInt last: NonNegativeInt locale: String where: CalorimetricDataPropositionInput): CalorimetricDataConnection! @cost(weight: "10") + allGeometricData(after: String before: String first: NonNegativeInt last: NonNegativeInt locale: String where: GeometricDataPropositionInput): GeometricDataConnection! @cost(weight: "10") + allHygrothermalData(after: String before: String first: NonNegativeInt last: NonNegativeInt locale: String where: HygrothermalDataPropositionInput): HygrothermalDataConnection! @cost(weight: "10") + allLifeCycleData(after: String before: String first: NonNegativeInt last: NonNegativeInt locale: String where: LifeCycleDataPropositionInput): LifeCycleDataConnection! @cost(weight: "10") + allOpticalData(after: String before: String first: NonNegativeInt last: NonNegativeInt locale: String where: OpticalDataPropositionInput): OpticalDataConnection! @cost(weight: "10") + allPhotovoltaicData(after: String before: String first: NonNegativeInt last: NonNegativeInt locale: String where: PhotovoltaicDataPropositionInput): PhotovoltaicDataConnection! @cost(weight: "10") + calorimetricData(databaseId: Uuid! id: Uuid! locale: String): 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: 2147483646, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @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 @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") + 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: 2147483646, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @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: String): GeometricData @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") + 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: 100, slicingArguments: ["first", "last"], slicingArgumentDefaultValue: 100, sizedFields: ["edges", "nodes"], requireOneSlicingArgument: false) @cost(weight: "10") + hasCalorimetricData(locale: String where: CalorimetricDataPropositionInput): Boolean! @cost(weight: "10") + hasGeometricData(locale: String where: GeometricDataPropositionInput): Boolean! @cost(weight: "10") + hasHygrothermalData(locale: String where: HygrothermalDataPropositionInput): Boolean! @cost(weight: "10") + hasLifeCycleData(locale: String where: LifeCycleDataPropositionInput): Boolean! @cost(weight: "10") + hasOpticalData(locale: String where: OpticalDataPropositionInput): Boolean! @cost(weight: "10") + hasPhotovoltaicData(locale: String where: PhotovoltaicDataPropositionInput): Boolean! @cost(weight: "10") + hygrothermalData(databaseId: Uuid! id: Uuid! locale: String): 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: 2147483646, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @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: String): 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: 2147483646, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @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 @authorize(policy: "ManageOpenIdConnectScope") @cost(weight: "10") - openIdConnectApplications: [OpenIdConnectApplication!]! @authorize(policy: "ManageOpenIdConnectScope") @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 @authorize(policy: "ManageOpenIdConnectScope") @listSize(assumedSize: 100, slicingArguments: ["first", "last"], slicingArgumentDefaultValue: 100, sizedFields: ["edges", "nodes"], requireOneSlicingArgument: false) @cost(weight: "10") openIdConnectAuthorization(id: Uuid!): OpenIdConnectAuthorization @authorize(policy: "ManageOpenIdConnectScope") @cost(weight: "10") - openIdConnectAuthorizations: [OpenIdConnectAuthorization!]! @authorize(policy: "ManageOpenIdConnectScope") @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 @authorize(policy: "ManageOpenIdConnectScope") @listSize(assumedSize: 100, slicingArguments: ["first", "last"], slicingArgumentDefaultValue: 100, sizedFields: ["edges", "nodes"], requireOneSlicingArgument: false) @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") + 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 @authorize(policy: "ManageOpenIdConnectScope") @listSize(assumedSize: 100, slicingArguments: ["first", "last"], slicingArgumentDefaultValue: 100, sizedFields: ["edges", "nodes"], requireOneSlicingArgument: false) @cost(weight: "10") + opticalData(databaseId: Uuid! id: Uuid! locale: String): 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 @authorize(policy: "ManageDatabaseScope") @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 @authorize(policy: "WriteScope") @authorize(policy: "VerifyScope") @listSize(assumedSize: 100, slicingArguments: ["first", "last"], slicingArgumentDefaultValue: 100, sizedFields: ["edges", "nodes"], requireOneSlicingArgument: false) @cost(weight: "10") + photovoltaicData(databaseId: Uuid! id: Uuid! locale: String): 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: 2147483646, slicingArguments: [ "first", "last" ], slicingArgumentDefaultValue: 100, sizedFields: [ "edges", "nodes" ], requireOneSlicingArgument: false) @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") } type RegisterUserError implements UserError { @@ -2017,39 +2124,41 @@ 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") + 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 @authorize(policy: "WriteScope") @cost(weight: "10") + pendingRepresentedInstitutions(order: [UserRepresentedInstitutionSortInput!] @cost(weight: "10") where: UserRepresentedInstitutionFilterInput @cost(weight: "10")): PendingUserRepresentedInstitutionConnection @authorize(policy: "WriteScope") @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! + node: Method! @cost(weight: "0") } type UserGnuPgKeyFingerprintConnection { - edges: [UserGnuPgKeyFingerprintEdge!]! @cost(weight: "10") - totalCount: NonNegativeInt! @cost(weight: "10") + edges: [UserGnuPgKeyFingerprintEdge!]! @cost(weight: "0") + totalCount: NonNegativeInt! } type UserGnuPgKeyFingerprintEdge { @@ -2057,18 +2166,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: NonNegativeInt! } type UserRepresentedInstitutionEdge { - node: Institution! @cost(weight: "10") + isAuthorizedToRemoveEdge: Boolean! @cost(weight: "1") + node: Institution! @cost(weight: "0") role: InstitutionRepresentativeRole! } @@ -2120,7 +2230,7 @@ union Stakeholder = Institution | User input AddComponentAssemblyInput { assembledComponentId: Uuid! - index: Byte + index: UnsignedByte partComponentId: Uuid! primeSurface: PrimeSurface } @@ -2177,29 +2287,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 @@ -2257,96 +2357,134 @@ input CoatedSidePropositionInput { input ComponentAssembledOfFilterInput { and: [ComponentAssembledOfFilterInput!] + createdAt: DateTimeFilterInput + index: ByteFilterInput or: [ComponentAssembledOfFilterInput!] partComponent: ComponentFilterInput - index: ByteFilterInput primeSurface: NullableOfPrimeSurfaceFilterInput + 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 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 - extras: JsonElementFilterInput - partOf: FilterInputTypeOfComponentsFilterInput - parts: FilterInputTypeOfComponentsFilterInput - partOfEdges: FilterInputTypeOfComponentAssemblysFilterInput - partEdges: FilterInputTypeOfComponentAssemblysFilterInput concretizations: FilterInputTypeOfComponentsFilterInput + createdAt: DateTimeFilterInput + description: StringFilterInput + extras: JsonElementFilterInput generalizations: FilterInputTypeOfComponentsFilterInput - variants: FilterInputTypeOfComponentsFilterInput + id: UuidFilterInput manager: InstitutionFilterInput - manufacturers: FilterInputTypeOfInstitutionsFilterInput manufacturerEdges: FilterInputTypeOfComponentManufacturersFilterInput + manufacturers: FilterInputTypeOfInstitutionsFilterInput + name: StringFilterInput + or: [ComponentFilterInput!] + partEdges: FilterInputTypeOfComponentAssemblysFilterInput + partOf: FilterInputTypeOfComponentsFilterInput + partOfEdges: FilterInputTypeOfComponentAssemblysFilterInput + parts: FilterInputTypeOfComponentsFilterInput + updatedAt: DateTimeFilterInput + variants: FilterInputTypeOfComponentsFilterInput } 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 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 + or: [ComponentPartOfFilterInput!] primeSurface: NullableOfPrimeSurfaceFilterInput + 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 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 { @@ -2382,12 +2520,12 @@ 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 } @@ -2399,11 +2537,9 @@ input ContactInformationInput { } 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") } @@ -2480,24 +2616,27 @@ input CreateOpenIdConnectApplicationInput { input DataFormatFilterInput { and: [DataFormatFilterInput!] - or: [DataFormatFilterInput!] - id: UuidFilterInput - name: StringFilterInput - extension: StringFilterInput + createdAt: DateTimeFilterInput description: StringFilterInput + extension: StringFilterInput + id: UuidFilterInput + manager: InstitutionFilterInput mediaType: StringFilterInput + name: StringFilterInput + or: [DataFormatFilterInput!] schemaLocator: UrlFilterInput - manager: InstitutionFilterInput + updatedAt: DateTimeFilterInput } 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 { @@ -2510,49 +2649,52 @@ input DataPropositionInput { input DatabaseFilterInput { and: [DatabaseFilterInput!] - or: [DatabaseFilterInput!] - id: UuidFilterInput - name: StringFilterInput + createdAt: DateTimeFilterInput description: StringFilterInput + id: UuidFilterInput locator: UrlFilterInput + name: StringFilterInput operator: InstitutionFilterInput + or: [DatabaseFilterInput!] + updatedAt: DateTimeFilterInput } input DatabaseSortInput { - id: SortEnumType @cost(weight: "10") - name: SortEnumType @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") description: SortEnumType @cost(weight: "10") + id: SortEnumType @cost(weight: "10") locator: UriSortInput @cost(weight: "10") - operator: InstitutionSortInput @cost(weight: "10") + name: SortEnumType @cost(weight: "10") + updatedAt: SortEnumType @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 +2720,8 @@ input DeleteUserInput { input DescriptionOrReferenceFilterInput { and: [DescriptionOrReferenceFilterInput!] - or: [DescriptionOrReferenceFilterInput!] description: StringFilterInput + or: [DescriptionOrReferenceFilterInput!] } input DescriptionOrReferenceInput { @@ -2591,14 +2733,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 { @@ -2613,116 +2764,109 @@ input FilesMetaInformationPropositionInput { input FilterInputTypeOfComponentAssemblysFilterInput { all: ComponentAssembledOfFilterInput @cost(weight: "10") + any: Boolean @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") + any: Boolean @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") + any: Boolean @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") + any: Boolean @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") + any: Boolean @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") + any: Boolean @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") + any: Boolean @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") + any: Boolean @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") + any: Boolean @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") + any: Boolean @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") + any: Boolean @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") + any: Boolean @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") + any: Boolean @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 +2907,25 @@ 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 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 +2936,292 @@ 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 - extras: JsonElementFilterInput - developedMethods: FilterInputTypeOfMethodsFilterInput + createdAt: DateTimeFilterInput + description: StringFilterInput developedMethodEdges: FilterInputTypeOfInstitutionMethodDevelopersFilterInput - managedMethods: FilterInputTypeOfMethodsFilterInput + developedMethods: FilterInputTypeOfMethodsFilterInput + extras: JsonElementFilterInput + gnuPgKeyFingerprints: FilterInputTypeOfGnuPgKeyFingerprintsFilterInput + id: UuidFilterInput managedDataFormats: FilterInputTypeOfDataFormatsFilterInput - manufacturedComponents: FilterInputTypeOfComponentsFilterInput + managedInstitutions: FilterInputTypeOfInstitutionsFilterInput + managedMethods: FilterInputTypeOfMethodsFilterInput + manager: InstitutionFilterInput manufacturedComponentEdges: FilterInputTypeOfComponentManufacturersFilterInput + manufacturedComponents: FilterInputTypeOfComponentsFilterInput + name: StringFilterInput operatedDatabases: FilterInputTypeOfDatabasesFilterInput - manager: InstitutionFilterInput - managedInstitutions: FilterInputTypeOfInstitutionsFilterInput - representatives: FilterInputTypeOfUsersFilterInput + or: [InstitutionFilterInput!] representativeEdges: FilterInputTypeOfInstitutionRepresentativesFilterInput - gnuPgKeyFingerprints: FilterInputTypeOfGnuPgKeyFingerprintsFilterInput + representatives: FilterInputTypeOfUsersFilterInput + 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: FilterInputTypeOfComponentsFilterInput + createdAt: DateTimeFilterInput + description: StringFilterInput extras: JsonElementFilterInput + generalizations: FilterInputTypeOfComponentsFilterInput + id: UuidFilterInput + manufacturerEdges: FilterInputTypeOfComponentManufacturersFilterInput + manufacturers: FilterInputTypeOfInstitutionsFilterInput + name: StringFilterInput + or: [InstitutionManagedComponentFilterInput!] + partEdges: FilterInputTypeOfComponentAssemblysFilterInput partOf: FilterInputTypeOfComponentsFilterInput - parts: FilterInputTypeOfComponentsFilterInput partOfEdges: FilterInputTypeOfComponentAssemblysFilterInput - partEdges: FilterInputTypeOfComponentAssemblysFilterInput - concretizations: FilterInputTypeOfComponentsFilterInput - generalizations: FilterInputTypeOfComponentsFilterInput + parts: FilterInputTypeOfComponentsFilterInput + updatedAt: DateTimeFilterInput variants: FilterInputTypeOfComponentsFilterInput - manufacturers: FilterInputTypeOfInstitutionsFilterInput - manufacturerEdges: FilterInputTypeOfComponentManufacturersFilterInput +} + +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 { - and: [InstitutionManagedInstitutionFilterInput!] - or: [InstitutionManagedInstitutionFilterInput!] - id: UuidFilterInput - name: StringFilterInput abbreviation: StringFilterInput - description: StringFilterInput + and: [InstitutionManagedInstitutionFilterInput!] contact: ContactInformationFilterInput - state: InstitutionStateFilterInput - extras: JsonElementFilterInput - developedMethods: FilterInputTypeOfMethodsFilterInput + createdAt: DateTimeFilterInput + description: StringFilterInput developedMethodEdges: FilterInputTypeOfInstitutionMethodDevelopersFilterInput - managedMethods: FilterInputTypeOfMethodsFilterInput + developedMethods: FilterInputTypeOfMethodsFilterInput + extras: JsonElementFilterInput + gnuPgKeyFingerprints: FilterInputTypeOfGnuPgKeyFingerprintsFilterInput + id: UuidFilterInput managedDataFormats: FilterInputTypeOfDataFormatsFilterInput - manufacturedComponents: FilterInputTypeOfComponentsFilterInput + managedInstitutions: FilterInputTypeOfInstitutionsFilterInput + managedMethods: FilterInputTypeOfMethodsFilterInput manufacturedComponentEdges: FilterInputTypeOfComponentManufacturersFilterInput + manufacturedComponents: FilterInputTypeOfComponentsFilterInput + name: StringFilterInput operatedDatabases: FilterInputTypeOfDatabasesFilterInput - managedInstitutions: FilterInputTypeOfInstitutionsFilterInput - representatives: FilterInputTypeOfUsersFilterInput + or: [InstitutionManagedInstitutionFilterInput!] representativeEdges: FilterInputTypeOfInstitutionRepresentativesFilterInput - gnuPgKeyFingerprints: FilterInputTypeOfGnuPgKeyFingerprintsFilterInput + representatives: FilterInputTypeOfUsersFilterInput + state: InstitutionStateFilterInput + 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 + createdAt: DateTimeFilterInput + description: StringFilterInput + id: UuidFilterInput institutionDeveloperEdges: FilterInputTypeOfInstitutionMethodDevelopersFilterInput - userDevelopers: FilterInputTypeOfUsersFilterInput + institutionDevelopers: FilterInputTypeOfInstitutionsFilterInput + name: StringFilterInput + or: [InstitutionManagedMethodFilterInput!] + updatedAt: DateTimeFilterInput userDeveloperEdges: FilterInputTypeOfUserMethodDevelopersFilterInput + userDevelopers: FilterInputTypeOfUsersFilterInput +} + +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 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 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 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 +3230,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,54 +3244,48 @@ 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 { @@ -3083,46 +3304,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 + createdAt: DateTimeFilterInput + description: StringFilterInput + id: UuidFilterInput institutionDeveloperEdges: FilterInputTypeOfInstitutionMethodDevelopersFilterInput - userDevelopers: FilterInputTypeOfUsersFilterInput - userDeveloperEdges: FilterInputTypeOfUserMethodDevelopersFilterInput + institutionDevelopers: FilterInputTypeOfInstitutionsFilterInput manager: InstitutionFilterInput + name: StringFilterInput + or: [MethodFilterInput!] + updatedAt: DateTimeFilterInput + userDeveloperEdges: FilterInputTypeOfUserMethodDevelopersFilterInput + userDevelopers: FilterInputTypeOfUsersFilterInput } input MethodParameterInput { @@ -3131,11 +3366,12 @@ input MethodParameterInput { } 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." @@ -3146,8 +3382,8 @@ input MethodSourceInput { input NullableOfPrimeSurfaceFilterInput { equalTo: PrimeSurface @cost(weight: "10") - notEqualTo: PrimeSurface @cost(weight: "10") in: [PrimeSurface] @cost(weight: "10") + notEqualTo: PrimeSurface @cost(weight: "10") notIn: [PrimeSurface] @cost(weight: "10") } @@ -3157,41 +3393,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,35 +3400,73 @@ 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 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 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 { @@ -3379,16 +3618,16 @@ input SetUserPhoneNumberInput { 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 +3643,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 } @@ -3512,97 +3736,125 @@ 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!] - id: UuidFilterInput - developedMethods: FilterInputTypeOfMethodsFilterInput + createdAt: DateTimeFilterInput developedMethodEdges: FilterInputTypeOfUserMethodDevelopersFilterInput - representedInstitutions: FilterInputTypeOfInstitutionsFilterInput + developedMethods: FilterInputTypeOfMethodsFilterInput + id: UuidFilterInput + "Full name" + name: StringFilterInput + or: [UserFilterInput!] representedInstitutionEdges: FilterInputTypeOfInstitutionRepresentativesFilterInput + representedInstitutions: FilterInputTypeOfInstitutionsFilterInput + 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 UserMethodDeveloperSortInput { - method: MethodSortInput @cost(weight: "10") - user: UserSortInput @cost(weight: "10") +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 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 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") } @@ -3938,17 +4190,6 @@ enum InstitutionState { VERIFIED } -enum IsoDayOfWeek { - NONE - MONDAY - TUESDAY - WEDNESDAY - THURSDAY - FRIDAY - SATURDAY - SUNDAY -} - enum JsonValueKind { UNDEFINED OBJECT @@ -4334,62 +4575,56 @@ enum VerifyInstitutionErrorCode { } "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 +directive @authorize("Defines when the authorize directive shall be applied. By default the authorize directive is applied before the resolver is executed." 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 "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 +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 - -"The `Byte` scalar type represents non-fractional whole numeric values. Byte can represent values between 0 and 255." -scalar Byte +scalar Any @specifiedBy(url: "https://scalars.graphql.org/chillicream/any.html") -"The `DateTime` scalar represents an ISO-8601 compliant date time type." -scalar DateTime @specifiedBy(url: "https:\/\/www.graphql-scalars.com\/date-time") +"The `Byte` scalar type represents a signed 8-bit integer." +scalar Byte @specifiedBy(url: "https://scalars.graphql.org/chillicream/byte.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 `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") -Example: `Europe/Zurich` -""" -scalar DateTimeZone +"The `Decimal` scalar type represents a decimal floating-point number with high precision." +scalar Decimal @specifiedBy(url: "https://scalars.graphql.org/chillicream/decimal.html") -"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") -""" -Represents a fixed (and calendar-independent) length of time. +"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") -Allowed patterns: -- `-D:hh:mm:ss.sssssssss` +"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") -Examples: -- `-1:20:00:00.999999999` -""" -scalar Duration +"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") "BCP 47 compliant Language Tag string" scalar Locale -"The `Long` scalar type represents non-fractional signed whole 64-bit numeric values. Long can represent values between -(2^63) and 2^63 - 1." -scalar Long +"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." -scalar Short +"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") \ No newline at end of file diff --git a/backend/test/Metabase.Tests.csproj b/backend/test/Metabase.Tests.csproj index 6f8bdd3dd..8a49335e2 100644 --- a/backend/test/Metabase.Tests.csproj +++ b/backend/test/Metabase.Tests.csproj @@ -12,18 +12,20 @@ - - - + + + + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/frontend/codegen.ts b/frontend/codegen.ts index 9e23c42de..bd13804f5 100644 --- a/frontend/codegen.ts +++ b/frontend/codegen.ts @@ -40,17 +40,21 @@ const config: CodegenConfig = { input: "string", output: "string", }, - Any: "unknown", + Any: "object", Byte: "number", DateTime: "string", DateTimeZone: "string", Decimal: "string", Duration: "string", + LocalDate: "string", + LocalDateTime: "string", + LocalTime: "string", Locale: "string", Long: "number", NonNegativeInt: "number", Short: "number", TimeSpan: "string", + UnsignedByte: "string", Upload: "unknown", Url: "string", Uuid: "string", @@ -69,6 +73,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..1f393d0e2 --- /dev/null +++ b/frontend/components/ActiveFilterAndSortBar.tsx @@ -0,0 +1,128 @@ +import { Tag, Space, Typography, Flex } from "antd"; +import { + ObjectFilterState, + getFilterOperatorLabel, + createFilterStateReducer, + FilterDefinition, + FilterStateReducerContext, +} from "../lib/filter"; +import { formatSortDirection, SortState, SortDefinition } from "../lib/sort"; +import DeleteButton from "./DeleteButton"; +import { Key } from "react"; +import { getLabel } from "../lib/string"; + +const stringifySort = ( + sort: SortState, + definitions: readonly SortDefinition[], +) => `${String(definitions[sort.index].field)}|${sort.direction}`; + +const renderSort = ( + sort: SortState, + definitions: readonly SortDefinition[], +) => ( + <> + {getLabel(definitions[sort.index], "none-upper")} ( + {formatSortDirection(sort.direction)}) + +); + +const stringifyFilter = ( + filter: ObjectFilterState, + definitions: readonly FilterDefinition[], +): Key => + createFilterStateReducer( + (scalar) => + `${getFilterOperatorLabel(scalar.operator)}|${JSON.stringify(scalar.value)}`, + (list, stringify) => + `${getFilterOperatorLabel(list.operator)}|${stringify(list.value)}`, + (object, context, stringify) => + `${String(context[object.index].field)}|${stringify(object.value)}`, + )(filter, definitions as FilterStateReducerContext); + +const renderFilter = ( + value: ObjectFilterState, + definitions: readonly FilterDefinition[], +) => + createFilterStateReducer( + (scalar) => ( + <> + {getFilterOperatorLabel(scalar.operator)}{" "} + {Array.isArray(scalar.value) + ? `{${scalar.value.join(", ")}}` + : scalar.value} + + ), + (list, render) => ( + <> + {getFilterOperatorLabel(list.operator)} {render(list.value)} + + ), + (object, context, render) => ( + <> + {getLabel(context[object.index], "none-upper")} {render(object.value)} + + ), + )(value, definitions as FilterStateReducerContext); + +export default function ActiveFilterAndSortBar({ + values, + filterDefinitions, + sortDefinitions, + onRemoveFilter, + onRemoveSort, + onRemoveAll, +}: { + values: { + filters: readonly ObjectFilterState[]; + sorts: readonly SortState[]; + }; + filterDefinitions: readonly FilterDefinition[]; + sortDefinitions: readonly SortDefinition[]; + onRemoveFilter: (index: number) => void; + onRemoveSort: (index: number) => void; + onRemoveAll: () => void; +}) { + return ( + + + {values.filters.length > 0 && ( + <> + Filtered by + {values.filters.map((filter, index: number) => ( + + } + 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) && ( + + )} + + ); +} diff --git a/frontend/components/Availability.tsx b/frontend/components/Availability.tsx deleted file mode 100644 index 76cd13330..000000000 --- a/frontend/components/Availability.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { OpenEndedDateTimeRange } from "../__generated__/graphql"; -import OpenEndedDateTimeRangeX from "./OpenEndedDateTimeRangeX"; - -export default function Availability({ - range, -}: { - range: OpenEndedDateTimeRange; -}) { - return ( -
- Available -
- ); -} diff --git a/frontend/components/CielabColorViewer.tsx b/frontend/components/CielabColorViewer.tsx new file mode 100644 index 000000000..272d5f4ff --- /dev/null +++ b/frontend/components/CielabColorViewer.tsx @@ -0,0 +1,10 @@ +import Copyable from "./Copyable"; +import { CielabColor } from "../__generated__/graphql"; + +export default function CielabColorViewer({ value }: { value: CielabColor }) { + return ( + + L*={value.lStar}, a*={value.aStar}, b*={value.bStar} + + ); +} diff --git a/frontend/components/CodeViewer.tsx b/frontend/components/CodeViewer.tsx new file mode 100644 index 000000000..88d94bb4e --- /dev/null +++ b/frontend/components/CodeViewer.tsx @@ -0,0 +1,32 @@ +import Copyable from "./Copyable"; +import CopyableBlock from "./CopyableBlock"; + +export default function CodeViewer({ + code, + inline = false, +}: { + code: string; + inline?: boolean; +}) { + if (inline) { + return ( + + {code} + + ); + } else { + return ( + +
+          
+            {code}
+          
+        
+
+ ); + } +} diff --git a/frontend/components/ContactInformation.tsx b/frontend/components/ContactInformation.tsx index a4b3d34a1..7ceeaa3c4 100644 --- a/frontend/components/ContactInformation.tsx +++ b/frontend/components/ContactInformation.tsx @@ -5,7 +5,7 @@ import { GlobalOutlined, EnvironmentOutlined, } from "@ant-design/icons"; -import { ContactInformationPartialFragment } from "../queries/institutions.generated"; +import { ContactInformationPartialFragment } from "../queries/common.generated"; export default function ContactInformation({ contact, diff --git a/frontend/components/CopyButton.tsx b/frontend/components/CopyButton.tsx new file mode 100644 index 000000000..3cbc231cf --- /dev/null +++ b/frontend/components/CopyButton.tsx @@ -0,0 +1,73 @@ +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 index a81a1bb7b..f72dc9778 100644 --- a/frontend/components/Copyable.tsx +++ b/frontend/components/Copyable.tsx @@ -1,31 +1,26 @@ -import { Button, Space } from "antd"; -import { CopyOutlined, CheckOutlined } from "@ant-design/icons"; -import { ReactNode, useState } from "react"; +import { Space } from "antd"; +import { ReactNode } from "react"; +import CopyButton from "./CopyButton"; export default function Copyable({ text, + onlyIcon, + color, children, }: { text: string; + onlyIcon?: boolean; + color?: "white"; children?: ReactNode; }) { - const [copied, setCopied] = useState(false); - return ( - - {children == null ? {text} : children} - - + + + {children == null ? text : children} + text} onlyIcon={onlyIcon} color={color}> + Copy + + + ); } diff --git a/frontend/components/DeleteButton.tsx b/frontend/components/DeleteButton.tsx new file mode 100644 index 000000000..0a923d81c --- /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 { + title?: React.ReactNode; + kind?: "delete" | "remove"; + type?: "primary" | "text" | "default" | "icon"; + deleting?: boolean; + style?: CSSProperties; + onClick?: (e?: React.MouseEvent) => void; + // This allows Popconfirm to inject its internal event handlers + [key: string]: any; +} + +const DeleteButton = forwardRef( + ( + { + title, + kind = "delete", + type = "primary", + deleting = false, + style, + onClick, + ...rest + }, + ref, + ) => { + const theTitle = title ?? capitalize(kind); + + const commonProps = { + ...rest, // contains Popconfirm's events + ref, // allows Popconfirm to measure position + danger: true, + loading: deleting, + style, + onClick, + }; + + switch (type) { + case "icon": + return ( + + + ); + } + }, +); + +export default DeleteButton; diff --git a/frontend/components/EditButton.tsx b/frontend/components/EditButton.tsx new file mode 100644 index 000000000..37a6111ef --- /dev/null +++ b/frontend/components/EditButton.tsx @@ -0,0 +1,30 @@ +import { EditOutlined } from "@ant-design/icons"; +import { Button, Tooltip } from "antd"; + +export default function EditButton({ + type = "default", + onClick, +}: { + type?: "text" | "default" | "icon"; + onClick?: (e?: React.MouseEvent) => void; +}) { + switch (type) { + case "icon": + return ( + + + ); + } +} diff --git a/frontend/components/EnumTag.tsx b/frontend/components/EnumTag.tsx new file mode 100644 index 000000000..ea6b35e4a --- /dev/null +++ b/frontend/components/EnumTag.tsx @@ -0,0 +1,16 @@ +import { Tag, TagProps } from "antd"; + +export default function EnumTag(props: TagProps) { + return ( + + {props.children} + + ); +} diff --git a/frontend/components/Float.tsx b/frontend/components/Float.tsx new file mode 100644 index 000000000..ab72ce580 --- /dev/null +++ b/frontend/components/Float.tsx @@ -0,0 +1,14 @@ +import Copyable from "./Copyable"; +import { Scalars } from "../__generated__/graphql"; + +export default function Float({ + value, +}: { + value: Scalars["Float"]["output"]; +}) { + return ( + + {value} + + ); +} 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/Highlight.tsx b/frontend/components/Highlight.tsx index b80fe130e..62a1b23e2 100644 --- a/frontend/components/Highlight.tsx +++ b/frontend/components/Highlight.tsx @@ -4,9 +4,9 @@ import Highlighter from "react-highlight-words"; interface HighlightProps { text: string | null | undefined; snippet: string | null | undefined; -}; +} -export const Highlight = forwardRef( +const Highlight = forwardRef( ({ text, snippet }, ref) => ( ( /> ), ); + +export default Highlight; 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/IdentifierItem.tsx b/frontend/components/IdentifierItem.tsx new file mode 100644 index 000000000..45ca2e9f8 --- /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 = () => ( + + + +); + +export const ArXivIcon = (props: Partial) => ( + +); +export 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..0063081c6 --- /dev/null +++ b/frontend/components/InlineList.tsx @@ -0,0 +1,22 @@ +export default function InlineList({ + items, + renderItem, +}: { + items: readonly TItem[]; + renderItem: (item: TItem, index: number) => React.ReactNode; +}) { + return ( + <> + + + {items.map((item, index) => renderItem(item, index))} + + + ); +} diff --git a/frontend/components/JsonViewer.tsx b/frontend/components/JsonViewer.tsx index 6f51f4469..ce8559e94 100644 --- a/frontend/components/JsonViewer.tsx +++ b/frontend/components/JsonViewer.tsx @@ -1,19 +1,37 @@ +import { Tooltip } from "antd"; +import Copyable from "./Copyable"; import CopyableBlock from "./CopyableBlock"; -export default function JsonViewer({ jsonData }: { jsonData: any }) { - const jsonString = JSON.stringify(jsonData, null, 2); - - return ( - -
-        
-          {jsonString}
-        
-      
-
- ); +export default function JsonViewer({ + data, + inline = false, +}: { + data: object; + inline?: boolean; +}) { + 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..71041df52 --- /dev/null +++ b/frontend/components/JumpToId.tsx @@ -0,0 +1,47 @@ +import { 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"; + +export type JumpToIdProps = { + query?: PaginatedSelectProps["query"]; + route: (id: Scalars["Uuid"]["output"]) => Route; +}; + +export default function JumpToId({ query, route }: JumpToIdProps) { + const router = useRouter(); + const [id, setId] = useState(""); + + const handleJump = () => { + if (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..8b20d326c 100644 --- a/frontend/components/Layout.tsx +++ b/frontend/components/Layout.tsx @@ -2,7 +2,7 @@ import Head from "next/head"; import { ReactNode, useEffect } from "react"; import Footer from "./Footer"; import NavBar from "./NavBar"; -import { Layout as AntLayout, App, Typography } from "antd"; +import { Layout as AntLayout, App, Flex, Typography } from "antd"; import paths from "../paths"; import { useCookies } from "react-cookie"; @@ -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", }, ], @@ -75,7 +75,7 @@ const navItems = [ interface LayoutProps { children?: ReactNode; -}; +} const cookieConsentName = "consent"; const cookieConsentValue = "yes"; @@ -93,7 +93,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. @@ -115,13 +115,30 @@ export default function Layout({ children }: LayoutProps) { - + + + - - {children} + + +
{children}
+
-
+ +
+ ); diff --git a/frontend/components/LazyTabs.tsx b/frontend/components/LazyTabs.tsx new file mode 100644 index 000000000..e9b4eac29 --- /dev/null +++ b/frontend/components/LazyTabs.tsx @@ -0,0 +1,77 @@ +import React, { useState, useMemo } from "react"; +import { GetProp, Tabs, TabsProps } from "antd"; +import TabLabel from "./TabLabel"; + +type TabItem = GetProp[number]; + +type TabItemWithCount = TabItem & { + count?: number; +}; + +export type LazyTabsProps = Omit & { + items?: TabItemWithCount[]; +}; + +/** + * Internal wrapper that tracks if a tab has ever been "active". + * Once initialized, it stays mounted. + */ +const LazyWrapper: React.FC<{ active: boolean; children: React.ReactNode }> = ({ + active, + children, +}) => { + const [initialized, setInitialized] = useState(false); + + if (active && !initialized) { + setInitialized(true); + } + if (!initialized) return null; + return <>{children}; +}; + +/** + * Reusable LazyTabs Component + * Extends standard Ant Design TabsProps + */ +export default function LazyTabs({ items, onChange, ...props }: LazyTabsProps) { + const [activeKey, setActiveKey] = useState(() => { + return ( + props.activeKey || + props.defaultActiveKey || + (items?.filter((item) => item.count && item.count > 0)?.[0] + ?.key as string) || + (items?.[0].key as string) + ); + }); + + const handleTabChange = (key: string) => { + setActiveKey(key); + onChange?.(key); + }; + + const lazyItems = useMemo(() => { + return items?.map((item) => ({ + ...item, + label: + item.count === undefined ? ( + item.label + ) : ( + + ), + children: ( + + {item.children} + + ), + })); + }, [items, activeKey]); + + return ( + + ); +} diff --git a/frontend/components/Manager.tsx b/frontend/components/Manager.tsx index 6e0ee7b8f..5408cfa8a 100644 --- a/frontend/components/Manager.tsx +++ b/frontend/components/Manager.tsx @@ -1,7 +1,6 @@ -import Link from "next/link"; import { Scalars } from "../__generated__/graphql"; import paths from "../paths"; -import { Tooltip } from "antd"; +import EntityLink from "./entities/EntityLink"; export default function Manager({ data, @@ -12,11 +11,8 @@ export default function Manager({ }; }) { return ( -
- Managed by{" "} - - {data.name} - -
+ <> + Managed by + ); } diff --git a/frontend/components/NavBar.tsx b/frontend/components/NavBar.tsx index 77d23085a..160a3af9c 100644 --- a/frontend/components/NavBar.tsx +++ b/frontend/components/NavBar.tsx @@ -2,11 +2,79 @@ import { useQuery } from "@apollo/client/react"; import Link from "next/link"; import { useRouter } from "next/router"; import { Menu, Button, Spin } from "antd"; -import { CurrentUserDocument } from "../queries/currentUser.generated"; +import { + CurrentUserDocument, + CurrentUserPartialFragment, +} from "../queries/currentUser.generated"; import paths from "../paths"; import { extractAntiforgeryTokenFromCookie } from "../lib/apollo"; import { UserOutlined, LoadingOutlined } from "@ant-design/icons"; import type { Route } from "next"; +import { isTruthy } from "../lib/array"; +import { CSSProperties, useMemo } from "react"; + +const userLoadingItem = { + key: paths.openIdConnect, + style: { marginLeft: "auto" }, + label: ( + } /> + ), +}; + +const loginOrRegisterItems = [ + { + key: paths.openIdConnectClientLogin, + style: { marginLeft: "auto" }, + label: Login, + }, + { + key: paths.userRegister, + label: Register, + }, +]; + +const userItems = (currentUser: CurrentUserPartialFragment) => + [ + currentUser?.isAuthorizedToManageOpenIdConnect && { + key: paths.openIdConnect, + label: OpenId Connect, + }, + { + key: paths.me.manage.home, + label: currentUser.name, + icon: , + style: { marginLeft: "auto" }, + children: [ + { + key: paths.user(currentUser.uuid), + label: Profile, + }, + { + key: paths.me.manage.profile, + label: Account, + }, + { + key: paths.openIdConnectClientLogout, + label: ( +
+ + + + ), + }, + ], + }, + ].filter(isTruthy); type NavItemProps = | { @@ -18,89 +86,51 @@ type NavItemProps = interface NavBarProps { items: NavItemProps[]; + style?: CSSProperties; } -export default function NavBar({ items }: NavBarProps) { +export default function NavBar({ items, style }: NavBarProps) { const router = useRouter(); const { loading, data } = useQuery(CurrentUserDocument); const currentUser = data?.currentUser; + const mainItems = useMemo( + () => + items.map((item) => + item.subitems === null + ? { + key: item.path, + label: {item.label}, + } + : { + key: item.label, + label: item.label, + children: item.subitems.map((subitem) => ({ + key: subitem.path, + label: {subitem.label}, + })), + }, + ), + [items], + ); + + const userOrLoginItems = useMemo( + () => + loading + ? [userLoadingItem] + : currentUser + ? userItems(currentUser) + : loginOrRegisterItems, + [loading, userLoadingItem, currentUser, loginOrRegisterItems], + ); + return ( - <> - - {items.map((item) => - item.subitems === null ? ( - - {item.label} - - ) : ( - // TODO find a better key - - {item.subitems.map((subitem) => ( - - {subitem.label} - - ))} - - ), - )} - {loading ? ( - - } - /> - - ) : currentUser ? ( - <> - {currentUser?.isAuthorizedToManageOpenIdConnect && ( - - OpenId Connect - - )} - } - style={{ marginLeft: "auto" }} - > - - Profile - - - Account - - -
- - - -
-
- - ) : ( - <> - - Login - - - Register - - - )} -
- + ); } diff --git a/frontend/components/OpenEndedDateTimeRangeX.tsx b/frontend/components/OpenEndedDateTimeRangeX.tsx index d26be9181..f2239975d 100644 --- a/frontend/components/OpenEndedDateTimeRangeX.tsx +++ b/frontend/components/OpenEndedDateTimeRangeX.tsx @@ -1,6 +1,7 @@ import { Typography } from "antd"; import dayjs from "dayjs"; import { OpenEndedDateTimeRange } from "../__generated__/graphql"; +import { isTruthy } from "../lib/array"; interface OpenEndedDateTimeRangeProps { range: OpenEndedDateTimeRange; @@ -11,12 +12,13 @@ export default function OpenEndedDateTimeRangeX({ }: OpenEndedDateTimeRangeProps) { return ( - from{" "} - {range.from == null - ? "beginning of time" - : dayjs(range.from).format("DD/MM/YYYY")}{" "} - to{" "} - {range.to == null ? "end of time" : dayjs(range.to).format("DD/MM/YYYY")} + {[ + range.from == null && range.to == null && "unrestricted", + range.from != null && `from ${dayjs(range.from).format("DD/MM/YYYY")}`, + range.to != null && `to ${dayjs(range.to).format("DD/MM/YYYY")}`, + ] + .filter(isTruthy) + .join(" ")} ); } diff --git a/frontend/components/PageHeader.tsx b/frontend/components/PageHeader.tsx deleted file mode 100644 index 935207813..000000000 --- a/frontend/components/PageHeader.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Breadcrumb, Button, Space, Typography } from "antd"; -import { ArrowLeftOutlined } from "@ant-design/icons"; -import { Scalars } from "../__generated__/graphql"; -import Copyable from "./Copyable"; -import Id from "./Id"; - -const { Title, Text } = Typography; - -interface Props { - id?: Scalars["Uuid"]["output"]; - title: string; - subTitle?: string; - tags?: React.ReactNode[]; - onBack?: () => void; - extra?: React.ReactNode; - breadcrumb?: { title: string; href?: string }[]; - children?: React.ReactNode; -} - -export default function PageHeader({ - id, - title, - subTitle, - tags, - onBack, - extra, - breadcrumb, - children, -}: Props) { - return ( - <> - {breadcrumb && ( - ({ - title: item.title, - href: item.href, - }))} - style={{ marginBottom: 12 }} - /> - )} - -
- {onBack && ( -
- - {children &&
{children}
} - - ); -} diff --git a/frontend/components/PaginatedIdSelect.tsx b/frontend/components/PaginatedIdSelect.tsx new file mode 100644 index 000000000..4b4c34621 --- /dev/null +++ b/frontend/components/PaginatedIdSelect.tsx @@ -0,0 +1,162 @@ +import React, { useState } from "react"; +import { Select, Space, Spin } from "antd"; +import { useQuery } from "@apollo/client/react"; +import { useDebounce } from "../lib/hooks/useDebounce"; +import { Scalars, SortEnumType } from "../__generated__/graphql"; +import { TypedDocumentNode } from "@apollo/client"; +import { isUuid, notEmpty } from "../lib/string"; +import { isTruthy } from "../lib/array"; +import Id from "./Id"; + +// Inspired by https://ant.design/components/select/#components-select-demo-select-users + +interface PageInfo { + endCursor: string | null; + hasNextPage: boolean; +} + +interface Item { + uuid: Scalars["Uuid"]["output"]; + name: string; +} + +interface ItemsData { + connection: { + edges: { node: Item }[]; + pageInfo: PageInfo; + }; +} + +interface ItemsVariables { + first: number; + after?: string | null; + where?: { or?: { id?: { equalTo: any }; name?: { contains: any } }[] }; + order?: { name?: SortEnumType }; +} + +interface BaseProps { + query: TypedDocumentNode; + value?: string; + style?: React.CSSProperties; + onChange?: (value: string) => void; +} + +interface SingleProps extends BaseProps { + mode?: undefined; +} + +interface MultiProps extends BaseProps { + mode: "multiple" | "tags"; +} + +export type PaginatedSelectProps = SingleProps | MultiProps; + +const order = { + name: SortEnumType.Asc, +}; + +export default function PaginatedIdSelect({ + query, + mode, + value, + style = { width: "100%" }, + onChange, +}: PaginatedSelectProps) { + const [search, setSearch] = useState(""); + const pageSize = 10; + + const filters = [ + isUuid(search) && { id: { equalTo: search } }, + notEmpty(search) && { name: { contains: search } }, + ].filter(isTruthy); + const where = filters.length > 0 ? { or: filters } : undefined; + + const { data, loading, fetchMore, refetch } = useQuery< + ItemsData, + ItemsVariables + >(query, { + variables: { + first: pageSize, + after: null, + where: where, + order: order, + }, + }); + + const { edges, pageInfo } = data?.connection || { + edges: [], + pageInfo: { hasNextPage: false, endCursor: null }, + }; + + const handleSearch = useDebounce((value: string) => { + setSearch(value); + refetch({ + first: pageSize, + after: null, + where: where, + order: order, + }); + }, 500); + + const handleScroll = (e: React.UIEvent) => { + const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; + const isBottom = scrollHeight - scrollTop <= clientHeight + 10; + + if (isBottom && !loading && pageInfo?.hasNextPage) { + fetchMore({ + variables: { + first: pageSize, + after: pageInfo.endCursor, + where: where, + order: order, + }, + }); + } + }; + + return ( + + + ); +} diff --git a/frontend/components/QueryToolbar.tsx b/frontend/components/QueryToolbar.tsx new file mode 100644 index 000000000..0ca5f6f24 --- /dev/null +++ b/frontend/components/QueryToolbar.tsx @@ -0,0 +1,47 @@ +import { Button, Space } from "antd"; +import { RocketOutlined } from "@ant-design/icons"; +import { print } from "graphql"; +import { TypedDocumentNode } from "@apollo/client"; +import CopyButton from "./CopyButton"; + +export default function QueryToolbar({ + query, + variables, +}: { + query: TypedDocumentNode; + variables?: TVariables | null; +}) { + const openInNitro = () => { + const nitroUrl = new URL("/graphql/", window.location.origin); + // const minifiedQuery = print(query) + // .replace(/#.*$/gm, "") + // .replace(/\s+/g, " ") + // .trim(); + // nitroUrl.searchParams.set("query", minifiedQuery); + // if (variables != null) { + // nitroUrl.searchParams.set("variables", JSON.stringify(variables)); + // } + window.open(nitroUrl.toString(), "_blank"); + }; + + return ( + + GraphQL + print(query)}> + Copy Query + + {variables && ( + JSON.stringify(variables)} + > + Copy Variables + + )} + + + ); +} diff --git a/frontend/components/Reference.tsx b/frontend/components/Reference.tsx index 87d460ddd..1cf20ffff 100644 --- a/frontend/components/Reference.tsx +++ b/frontend/components/Reference.tsx @@ -1,51 +1,124 @@ -import { Descriptions, Typography } from "antd"; -import { Publication, Standard } from "../__generated__/graphql"; +import { Space, Typography } from "antd"; +import { + GlobalOutlined, + BookOutlined, + SafetyCertificateOutlined, +} from "@ant-design/icons"; +import { Iconize } from "./Iconize"; +import { isTruthy } from "../lib/array"; +import { ReactNode } from "react"; +import { Fragment } from "react/jsx-runtime"; +import IdentifierItem from "./IdentifierItem"; +import { ReferencePartialFragment } from "../queries/common.generated"; -interface ReferenceProps { - reference?: Publication | Standard | null; +const joinWithCopyableSpace = ( + nodes: ReactNode[], + separator: string = " ", +): ReactNode => { + return nodes.reduce((acc, curr, index) => { + if (index === 0) return [curr]; + return [ + ...(acc as ReactNode[]), + {separator}, + curr, + ]; + }, [] as ReactNode[]); }; -export function Reference({ reference }: ReferenceProps) { - return reference == null ? ( - None - ) : ( - - <> - {reference?.title} - - {reference?.abstract} - - - {reference?.section} - - - {reference.__typename === "Standard" && ( - <> - {`${reference.numeration.prefix} ${reference.numeration.mainNumber} ${reference.numeration.suffix}`} - {reference.year} - - - {reference.locator} - - - - {reference.standardizers.join(", ")} - - - )} - {reference.__typename === "Publication" && ( - <> - {reference.arXiv} - {reference.doi} - {reference.urn} - - {reference.webAddress} - - - {reference.authors?.join(", ")} - - +interface ReferenceProps { + data: ReferencePartialFragment; +} + +export function Reference({ data }: ReferenceProps) { + const Icon = + data.__typename === "Standard" ? SafetyCertificateOutlined : BookOutlined; + + return ( +
+ }>{data.__typename}{" "} + + {data.__typename === "Publication" && + joinWithCopyableSpace( + [ + data.authors && data.authors.length > 0 && ( + + {data.authors.join(", ")} + + ), + data.title && ( + + {data.title}, + + ), + data.section && ( + + Section{" "} + {data.section}. + + ), + (data.arXiv || data.doi || data.urn || data.webAddress) && ( + + {(["arXiv", "doi", "urn", "webAddress"] as const) + .map( + (key) => + data[key] && ( + + ), + ) + .filter(isTruthy)} + + ), + ].filter(isTruthy), + )} + {data.__typename === "Standard" && + joinWithCopyableSpace( + [ + data.standardizers && data.standardizers.length > 0 && ( + + {data.standardizers?.join(", ")} + + ), + + {data.numeration.prefix ?? ""} + {data.numeration.mainNumber} + {data.numeration.suffix ? `-${data.numeration.suffix}` : ""} + , + data.year && ({data.year})., + data.title && ( + + {data.title}. + + ), + data.section && ( + + Section{" "} + {data.section}. + + ), + data.locator && ( + + }>Web + + ), + ].filter(isTruthy), + )} + + {data.abstract && ( + + {data.abstract} + )} - +
); } diff --git a/frontend/components/ReferenceForm.tsx b/frontend/components/ReferenceSubform.tsx similarity index 82% rename from frontend/components/ReferenceForm.tsx rename to frontend/components/ReferenceSubform.tsx index af489bcb8..4f3bbc551 100644 --- a/frontend/components/ReferenceForm.tsx +++ b/frontend/components/ReferenceSubform.tsx @@ -7,13 +7,12 @@ import { Space, Button, FormInstance, + Flex, } from "antd"; import { useState } from "react"; import { Standardizer, Standard, Publication } from "../__generated__/graphql"; - -const tailLayout = { - wrapperCol: { offset: 8, span: 16 }, -}; +import DeleteButton from "./DeleteButton"; +import TextArea from "antd/es/input/TextArea"; enum ReferenceKind { None = "None", @@ -27,15 +26,14 @@ function referenceToKind( switch (reference?.__typename) { case null: return ReferenceKind.None; + case undefined: + return ReferenceKind.None; case "Standard": return ReferenceKind.Standard; case "Publication": return ReferenceKind.Publication; default: - // TODO Why does this not work? For a working example see https://www.typescriptlang.org/docs/handbook/2/narrowing.html#exhaustiveness-checking - // const _exhaustiveCheck: never = reference; - // return _exhaustiveCheck; - return ReferenceKind.None; + return assertNever(reference); } } @@ -54,23 +52,18 @@ function removeTypenames( return referenceWithoutTypename; } -// interface HasStandardAndPublication { -// standard: CreateStandardInput | null | undefined; -// publication: CreatePublicationInput | null | undefined; -// } - -export type ReferenceFormProps = { +export type ReferenceSubformProps = { form: FormInstance; initialValue?: Standard | Publication | null; namespace: string[]; }; -// TODO Why does the following not work? export function ReferenceForm({form}: ReferenceFormProps) { -export function ReferenceForm({ +// TODO Harden types: export function ReferenceForm({form}: ReferenceFormProps) { +export function ReferenceSubform({ form, initialValue, namespace, -}: ReferenceFormProps) { +}: ReferenceSubformProps) { const initialKind = referenceToKind(initialValue); const initialReference = removeTypenames(initialValue); const [selectedReferenceOption, setSelectedReferenceOption] = @@ -143,7 +136,7 @@ export function ReferenceForm({ : null } > - +