From 49d859781a8eb8d3c706e64e7ec13e1ce490a245 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Mon, 25 Nov 2024 23:53:23 -0300 Subject: [PATCH 01/68] optimize /new/model page --- packages/hub/src/app/new/model/NewModel.tsx | 42 +++------------- packages/hub/src/app/new/model/page.tsx | 55 ++++++++++++++++++++- 2 files changed, 61 insertions(+), 36 deletions(-) diff --git a/packages/hub/src/app/new/model/NewModel.tsx b/packages/hub/src/app/new/model/NewModel.tsx index ec22897eba..2c01d4469b 100644 --- a/packages/hub/src/app/new/model/NewModel.tsx +++ b/packages/hub/src/app/new/model/NewModel.tsx @@ -1,9 +1,7 @@ "use client"; -import { useSession } from "next-auth/react"; -import { useRouter, useSearchParams } from "next/navigation"; -import { FC, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { FC, useState } from "react"; import { FormProvider } from "react-hook-form"; -import { useLazyLoadQuery } from "react-relay"; import { graphql } from "relay-runtime"; import { generateSeed } from "@quri/squiggle-lang"; @@ -14,10 +12,9 @@ import { SelectGroup, SelectGroupOption } from "@/components/SelectGroup"; import { H1 } from "@/components/ui/Headers"; import { SlugFormField } from "@/components/ui/SlugFormField"; import { useMutationForm } from "@/hooks/useMutationForm"; -import { modelRoute, newModelRoute } from "@/routes"; +import { modelRoute } from "@/routes"; import { NewModelMutation } from "@/__generated__/NewModelMutation.graphql"; -import { NewModelPageQuery } from "@/__generated__/NewModelPageQuery.graphql"; const defaultCode = `/* Describe your code here @@ -32,35 +29,12 @@ type FormShape = { isPrivate: boolean; }; -export const NewModel: FC = () => { - useSession({ required: true }); - - const searchParams = useSearchParams(); - - const { group: initialGroup } = useLazyLoadQuery( - graphql` - query NewModelPageQuery($groupSlug: String!, $groupSlugIsSet: Boolean!) { - group(slug: $groupSlug) @include(if: $groupSlugIsSet) { - ... on Group { - id - slug - myMembership { - id - } - } - } - } - `, - { - groupSlug: searchParams.get("group") ?? "", - groupSlugIsSet: Boolean(searchParams.get("group")), - } - ); +export const NewModel: FC<{ initialGroup: SelectGroupOption | null }> = ({ + initialGroup, +}) => { + const [group] = useState(initialGroup); const router = useRouter(); - useEffect(() => { - router.replace(newModelRoute()); // clean up group=... param - }, [router]); const { form, onSubmit, inFlight } = useMutationForm< FormShape, @@ -70,7 +44,7 @@ export const NewModel: FC = () => { mode: "onChange", defaultValues: { // don't pass `slug: ""` here, it will lead to form reset if a user started to type in a value before JS finished loading - group: initialGroup?.myMembership ? initialGroup : null, + group, isPrivate: false, }, mutation: graphql` diff --git a/packages/hub/src/app/new/model/page.tsx b/packages/hub/src/app/new/model/page.tsx index c939ed570c..ac979abdad 100644 --- a/packages/hub/src/app/new/model/page.tsx +++ b/packages/hub/src/app/new/model/page.tsx @@ -1,13 +1,64 @@ import { Metadata } from "next"; +import { fetchQuery, graphql } from "relay-runtime"; +import { z } from "zod"; import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; +import { SelectGroupOption } from "@/components/SelectGroup"; +import { getCurrentEnvironment } from "@/relay/environment"; +import { getSessionUserOrRedirect } from "@/server/helpers"; import { NewModel } from "./NewModel"; -export default async function OuterNewModelPage() { +import { pageNewModelQuery } from "@/__generated__/pageNewModelQuery.graphql"; + +export default async function OuterNewModelPage({ + searchParams, +}: { + searchParams: Promise<{ + [key: string]: string | string[] | undefined; + }>; +}) { + await getSessionUserOrRedirect(); + + const groupSlug = z + .string() + .optional() + .parse((await searchParams)["group"]); + + const environment = getCurrentEnvironment(); + + let group: SelectGroupOption | null = null; + + if (groupSlug) { + const result = await fetchQuery( + environment, + graphql` + query pageNewModelQuery($groupSlug: String!) { + group(slug: $groupSlug) { + ... on Group { + id + slug + myMembership { + id + } + } + } + } + `, + { groupSlug } + ).toPromise(); + + if (result?.group?.id && result?.group?.slug) { + group = { + id: result.group.id, + slug: result.group.slug, + }; + } + } + return ( - + ); } From fa22fa6467f019466b44c36f535fe63e096d0dfb Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 26 Nov 2024 11:49:54 -0300 Subject: [PATCH 02/68] prefetch "New Model" link --- packages/hub/src/components/layout/RootLayout/PageMenu.tsx | 1 + .../hub/src/components/layout/RootLayout/PageMenuLink.tsx | 4 ++++ packages/hub/src/components/ui/DropdownMenuNextLinkItem.tsx | 4 +++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/hub/src/components/layout/RootLayout/PageMenu.tsx b/packages/hub/src/components/layout/RootLayout/PageMenu.tsx index 25866b5c7a..65d0e21b78 100644 --- a/packages/hub/src/components/layout/RootLayout/PageMenu.tsx +++ b/packages/hub/src/components/layout/RootLayout/PageMenu.tsx @@ -64,6 +64,7 @@ const NewModelMenuLink: FC = (props) => { href={newModelRoute()} icon={PlusIcon} title="New Model" + prefetch /> ); }; diff --git a/packages/hub/src/components/layout/RootLayout/PageMenuLink.tsx b/packages/hub/src/components/layout/RootLayout/PageMenuLink.tsx index be25ffea90..a28e4cff8f 100644 --- a/packages/hub/src/components/layout/RootLayout/PageMenuLink.tsx +++ b/packages/hub/src/components/layout/RootLayout/PageMenuLink.tsx @@ -21,6 +21,7 @@ type Props = { href: string; icon?: FC; external?: boolean; + prefetch?: boolean; } & MenuLinkModeProps; export const PageMenuLink: FC = ({ @@ -30,6 +31,7 @@ export const PageMenuLink: FC = ({ icon, href, external, + prefetch, }) => { const Icon = icon; return mode === "desktop" ? ( @@ -37,6 +39,7 @@ export const PageMenuLink: FC = ({ className="select-none rounded-md px-2 py-1 text-sm text-white hover:bg-slate-700" href={href} target={external ? "_blank" : undefined} + prefetch={prefetch} > {Icon && } {title} @@ -47,6 +50,7 @@ export const PageMenuLink: FC = ({ icon={icon ?? EmptyIcon} title={title} close={close} + prefetch={prefetch} /> ); }; diff --git a/packages/hub/src/components/ui/DropdownMenuNextLinkItem.tsx b/packages/hub/src/components/ui/DropdownMenuNextLinkItem.tsx index 1159677967..84ba1a3c1c 100644 --- a/packages/hub/src/components/ui/DropdownMenuNextLinkItem.tsx +++ b/packages/hub/src/components/ui/DropdownMenuNextLinkItem.tsx @@ -9,6 +9,7 @@ type Props = { title: string; icon?: FC; close: () => void; + prefetch?: boolean; }; export const DropdownMenuNextLinkItem: FC = ({ @@ -16,9 +17,10 @@ export const DropdownMenuNextLinkItem: FC = ({ href, icon, close, + prefetch, }) => { return ( - + ); From e6a68a4acad4daad4788a51090eb81954ba48fde Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 26 Nov 2024 12:09:31 -0300 Subject: [PATCH 03/68] remove the global loading.tsx --- packages/hub/src/app/loading.tsx | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 packages/hub/src/app/loading.tsx diff --git a/packages/hub/src/app/loading.tsx b/packages/hub/src/app/loading.tsx deleted file mode 100644 index 8f9c8feb4e..0000000000 --- a/packages/hub/src/app/loading.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import Skeleton from "react-loading-skeleton"; - -import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; - -export default function Loading() { - return ( - - - - ); -} From 30bdb9fc7ec7c7087dc24d539499d59eee9298c5 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 26 Nov 2024 16:13:50 -0300 Subject: [PATCH 04/68] remove useSession; use server-side auth instead --- packages/hub/schema.graphql | 1 + packages/hub/src/app/RootLayout.tsx | 58 ------------------ packages/hub/src/app/ai/analytics/layout.tsx | 9 ++- .../invite-link/AcceptGroupInvitePage.tsx | 3 - .../app/groups/[slug]/invite-link/page.tsx | 11 +++- packages/hub/src/app/layout.tsx | 8 +-- .../[slug]/EditSquiggleSnippetModel.tsx | 5 +- .../src/app/new/definition/NewDefinition.tsx | 3 - packages/hub/src/app/new/definition/page.tsx | 9 ++- packages/hub/src/app/new/group/NewGroup.tsx | 3 - packages/hub/src/app/new/group/page.tsx | 9 ++- .../edit/EditRelativeValuesDefinition.tsx | 3 - .../[owner]/[slug]/edit/page.tsx | 17 ++++-- .../choose-username/ChooseUsername.tsx | 13 ++-- .../src/app/settings/choose-username/page.tsx | 7 ++- .../src/app/users/[username]/UserLayout.tsx | 5 +- packages/hub/src/components/ReactRoot.tsx | 23 +++---- .../components/WithAuth/RedirectToLogin.tsx | 9 +++ .../hub/src/components/WithAuth/index.tsx | 35 +++++++++++ .../layout/RootLayout/DesktopUserControls.tsx | 9 +-- .../layout/RootLayout/PageFooter.tsx | 1 - .../RootLayout/PageFooterIfNecessary.tsx | 19 ++++++ .../components/layout/RootLayout/PageMenu.tsx | 60 ++++++++++--------- .../components/layout/RootLayout/index.tsx | 49 +++++++++++++++ .../RootLayout/useForceChooseUsername.ts | 14 ++--- packages/hub/src/graphql/types/User.ts | 4 ++ packages/hub/src/hooks/useUsername.ts | 6 -- 27 files changed, 224 insertions(+), 169 deletions(-) delete mode 100644 packages/hub/src/app/RootLayout.tsx create mode 100644 packages/hub/src/components/WithAuth/RedirectToLogin.tsx create mode 100644 packages/hub/src/components/WithAuth/index.tsx create mode 100644 packages/hub/src/components/layout/RootLayout/PageFooterIfNecessary.tsx create mode 100644 packages/hub/src/components/layout/RootLayout/index.tsx delete mode 100644 packages/hub/src/hooks/useUsername.ts diff --git a/packages/hub/schema.graphql b/packages/hub/schema.graphql index add529deeb..0f375deeca 100644 --- a/packages/hub/schema.graphql +++ b/packages/hub/schema.graphql @@ -727,6 +727,7 @@ type UpdateSquiggleSnippetResult { type User implements Node & Owner { groups(after: String, before: String, first: Int, last: Int): GroupConnection! id: ID! + isMe: Boolean! isRoot: Boolean! models(after: String, before: String, first: Int, last: Int): ModelConnection! relativeValuesDefinitions(after: String, before: String, first: Int, last: Int): RelativeValuesDefinitionConnection! diff --git a/packages/hub/src/app/RootLayout.tsx b/packages/hub/src/app/RootLayout.tsx deleted file mode 100644 index 0243156b87..0000000000 --- a/packages/hub/src/app/RootLayout.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client"; -import { Session } from "next-auth"; -import { useSession } from "next-auth/react"; -import { usePathname } from "next/navigation"; -import { FC, PropsWithChildren } from "react"; -import { useLazyLoadQuery } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { isAiRoute, isModelRoute } from "@/routes"; - -import { PageFooter } from "../components/layout/RootLayout/PageFooter"; -import { PageMenu } from "../components/layout/RootLayout/PageMenu"; -import { ReactRoot } from "../components/ReactRoot"; - -import { RootLayoutQuery } from "@/__generated__/RootLayoutQuery.graphql"; - -const InnerRootLayout: FC = ({ children }) => { - const { data: session } = useSession(); - const queryData = useLazyLoadQuery( - graphql` - query RootLayoutQuery($signedIn: Boolean!) { - ...PageMenu @arguments(signedIn: $signedIn) - } - `, - { signedIn: !!session } - ); - - const pathname = usePathname(); - - const showFooter = !isModelRoute(pathname) && !isAiRoute(pathname); - - return ( -
- -
- {children} -
- {showFooter && } -
- ); -}; - -export const RootLayout: FC< - PropsWithChildren<{ - session: Session | null; - }> -> = ({ session, children }) => { - return ( - - {children} - - ); -}; diff --git a/packages/hub/src/app/ai/analytics/layout.tsx b/packages/hub/src/app/ai/analytics/layout.tsx index 0f077a0528..f591cdca79 100644 --- a/packages/hub/src/app/ai/analytics/layout.tsx +++ b/packages/hub/src/app/ai/analytics/layout.tsx @@ -1,10 +1,13 @@ import { PropsWithChildren } from "react"; -import { checkRootUser } from "@/server/helpers"; +import { WithAuth } from "@/components/WithAuth"; import { AiAnalyticsClientLayout } from "./ClientLayout"; export default async function ({ children }: PropsWithChildren) { - await checkRootUser(); - return {children}; + return ( + + {children} + + ); } diff --git a/packages/hub/src/app/groups/[slug]/invite-link/AcceptGroupInvitePage.tsx b/packages/hub/src/app/groups/[slug]/invite-link/AcceptGroupInvitePage.tsx index 674e71345b..a5fea6fc49 100644 --- a/packages/hub/src/app/groups/[slug]/invite-link/AcceptGroupInvitePage.tsx +++ b/packages/hub/src/app/groups/[slug]/invite-link/AcceptGroupInvitePage.tsx @@ -1,5 +1,4 @@ "use client"; -import { useSession } from "next-auth/react"; import { redirect, useRouter, useSearchParams } from "next/navigation"; import { FC, useEffect } from "react"; import { graphql } from "relay-runtime"; @@ -22,8 +21,6 @@ import { AcceptGroupInvitePageQuery } from "@/__generated__/AcceptGroupInvitePag export const AcceptGroupInvitePage: FC<{ query: SerializablePreloadedQuery; }> = ({ query }) => { - useSession({ required: true }); - const [{ result }] = usePageQuery( graphql` query AcceptGroupInvitePageQuery($slug: String!) { diff --git a/packages/hub/src/app/groups/[slug]/invite-link/page.tsx b/packages/hub/src/app/groups/[slug]/invite-link/page.tsx index dcb4ac3a3e..2a36e863c5 100644 --- a/packages/hub/src/app/groups/[slug]/invite-link/page.tsx +++ b/packages/hub/src/app/groups/[slug]/invite-link/page.tsx @@ -1,3 +1,4 @@ +import { WithAuth } from "@/components/WithAuth"; import { loadPageQuery } from "@/relay/loadPageQuery"; import { AcceptGroupInvitePage } from "./AcceptGroupInvitePage"; @@ -10,7 +11,7 @@ type Props = { params: Promise<{ slug: string }>; }; -export default async function OuterAcceptGroupInvitePage({ params }: Props) { +async function InnerPage({ params }: Props) { const { slug } = await params; const query = await loadPageQuery(QueryNode, { slug, @@ -18,3 +19,11 @@ export default async function OuterAcceptGroupInvitePage({ params }: Props) { return ; } + +export default async function ({ params }: Props) { + return ( + + + + ); +} diff --git a/packages/hub/src/app/layout.tsx b/packages/hub/src/app/layout.tsx index aa416e4fab..20b0ad42fd 100644 --- a/packages/hub/src/app/layout.tsx +++ b/packages/hub/src/app/layout.tsx @@ -6,19 +6,15 @@ import { Analytics } from "@vercel/analytics/react"; import { Metadata } from "next"; import { PropsWithChildren } from "react"; -import { auth } from "@/auth"; - -import { RootLayout } from "./RootLayout"; +import { RootLayout } from "../components/layout/RootLayout"; export default async function ServerRootLayout({ children, }: PropsWithChildren) { - const session = await auth(); - return ( - {children} + {children} diff --git a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx index a150abde8f..c7171c6973 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx @@ -1,4 +1,3 @@ -import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; import { BaseSyntheticEvent, @@ -137,8 +136,6 @@ export const EditSquiggleSnippetModel: FC = ({ modelRef, forceVersionPicker, }) => { - const { data: session } = useSession(); - const model = useFragment( graphql` fragment EditSquiggleSnippetModel on Model { @@ -456,7 +453,7 @@ export const EditSquiggleSnippetModel: FC = ({ }: { importId: string; }) => ( - + ); diff --git a/packages/hub/src/app/new/definition/NewDefinition.tsx b/packages/hub/src/app/new/definition/NewDefinition.tsx index 81bba3e2f8..51edff3342 100644 --- a/packages/hub/src/app/new/definition/NewDefinition.tsx +++ b/packages/hub/src/app/new/definition/NewDefinition.tsx @@ -1,6 +1,5 @@ "use client"; -import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; import { FC } from "react"; import { graphql } from "relay-runtime"; @@ -36,8 +35,6 @@ const Mutation = graphql` `; export const NewDefinition: FC = () => { - useSession({ required: true }); - const router = useRouter(); const [runMutation] = useAsyncMutation({ diff --git a/packages/hub/src/app/new/definition/page.tsx b/packages/hub/src/app/new/definition/page.tsx index 1b3a4d656b..0f87fef358 100644 --- a/packages/hub/src/app/new/definition/page.tsx +++ b/packages/hub/src/app/new/definition/page.tsx @@ -1,14 +1,17 @@ import { Metadata } from "next"; import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; +import { WithAuth } from "@/components/WithAuth"; import { NewDefinition } from "./NewDefinition"; export default function OuterNewModelDefinitionPage() { return ( - - - + + + + + ); } diff --git a/packages/hub/src/app/new/group/NewGroup.tsx b/packages/hub/src/app/new/group/NewGroup.tsx index 7fc9541443..e285000ad9 100644 --- a/packages/hub/src/app/new/group/NewGroup.tsx +++ b/packages/hub/src/app/new/group/NewGroup.tsx @@ -1,5 +1,4 @@ "use client"; -import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; import { FC } from "react"; import { FormProvider } from "react-hook-form"; @@ -32,8 +31,6 @@ const Mutation = graphql` `; export const NewGroup: FC = () => { - useSession({ required: true }); - const router = useRouter(); type FormShape = { diff --git a/packages/hub/src/app/new/group/page.tsx b/packages/hub/src/app/new/group/page.tsx index 0d33e59c35..3035067b83 100644 --- a/packages/hub/src/app/new/group/page.tsx +++ b/packages/hub/src/app/new/group/page.tsx @@ -1,14 +1,17 @@ import { Metadata } from "next"; import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; +import { WithAuth } from "@/components/WithAuth"; import { NewGroup } from "./NewGroup"; export default function OuterNewGroupPage() { return ( - - - + + + + + ); } diff --git a/packages/hub/src/app/relative-values/[owner]/[slug]/edit/EditRelativeValuesDefinition.tsx b/packages/hub/src/app/relative-values/[owner]/[slug]/edit/EditRelativeValuesDefinition.tsx index 2be9e09720..cdd18b01e3 100644 --- a/packages/hub/src/app/relative-values/[owner]/[slug]/edit/EditRelativeValuesDefinition.tsx +++ b/packages/hub/src/app/relative-values/[owner]/[slug]/edit/EditRelativeValuesDefinition.tsx @@ -1,5 +1,4 @@ "use client"; -import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; import { FC } from "react"; import { graphql, useFragment } from "react-relay"; @@ -44,8 +43,6 @@ const Mutation = graphql` export const EditRelativeValuesDefinition: FC<{ query: SerializablePreloadedQuery; }> = ({ query }) => { - useSession({ required: true }); - const [{ relativeValuesDefinition: result }] = usePageQuery( RelativeValuesDefinitionPageQuery, query diff --git a/packages/hub/src/app/relative-values/[owner]/[slug]/edit/page.tsx b/packages/hub/src/app/relative-values/[owner]/[slug]/edit/page.tsx index d2345b1ca4..8d031f779e 100644 --- a/packages/hub/src/app/relative-values/[owner]/[slug]/edit/page.tsx +++ b/packages/hub/src/app/relative-values/[owner]/[slug]/edit/page.tsx @@ -3,15 +3,16 @@ import QueryNode, { } from "@gen/RelativeValuesDefinitionPageQuery.graphql"; import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; +import { WithAuth } from "@/components/WithAuth"; import { loadPageQuery } from "@/relay/loadPageQuery"; import { EditRelativeValuesDefinition } from "./EditRelativeValuesDefinition"; -export default async function Page({ - params, -}: { +type Props = { params: Promise<{ owner: string; slug: string }>; -}) { +}; + +async function InnerPage({ params }: Props) { const { owner, slug } = await params; const query = await loadPageQuery( QueryNode, @@ -26,3 +27,11 @@ export default async function Page({ ); } + +export default async function Page({ params }: Props) { + return ( + + + + ); +} diff --git a/packages/hub/src/app/settings/choose-username/ChooseUsername.tsx b/packages/hub/src/app/settings/choose-username/ChooseUsername.tsx index 922ee2b755..4634cb1da7 100644 --- a/packages/hub/src/app/settings/choose-username/ChooseUsername.tsx +++ b/packages/hub/src/app/settings/choose-username/ChooseUsername.tsx @@ -1,6 +1,6 @@ "use client"; import { ChooseUsernameMutation } from "@gen/ChooseUsernameMutation.graphql"; -import { useSession } from "next-auth/react"; +import { Session } from "next-auth"; import { useRouter } from "next/navigation"; import { FC } from "react"; import { FormProvider } from "react-hook-form"; @@ -11,11 +11,9 @@ import { Button } from "@quri/ui"; import { SlugFormField } from "@/components/ui/SlugFormField"; import { useMutationForm } from "@/hooks/useMutationForm"; -export const ChooseUsername: FC = () => { - const { data: session, update: updateSession } = useSession({ - required: true, - }); - +export const ChooseUsername: FC<{ session: Session | null }> = ({ + session, +}) => { const router = useRouter(); if (session?.user.username) { router.replace("/"); @@ -47,8 +45,7 @@ export const ChooseUsername: FC = () => { expectedTypename: "Me", formDataToVariables: (data) => ({ username: data.username }), onCompleted: () => { - updateSession(); - router.replace("/"); + router.refresh(); }, blockOnSuccess: true, }); diff --git a/packages/hub/src/app/settings/choose-username/page.tsx b/packages/hub/src/app/settings/choose-username/page.tsx index ac66b8e38c..cce33da869 100644 --- a/packages/hub/src/app/settings/choose-username/page.tsx +++ b/packages/hub/src/app/settings/choose-username/page.tsx @@ -1,9 +1,12 @@ import { Metadata } from "next"; +import { auth } from "@/auth"; + import { ChooseUsername } from "./ChooseUsername"; -export default function OuterChooseUsernamePage() { - return ; +export default async function OuterChooseUsernamePage() { + const session = await auth(); + return ; } export const metadata: Metadata = { diff --git a/packages/hub/src/app/users/[username]/UserLayout.tsx b/packages/hub/src/app/users/[username]/UserLayout.tsx index d110d0babb..ccadad9ff2 100644 --- a/packages/hub/src/app/users/[username]/UserLayout.tsx +++ b/packages/hub/src/app/users/[username]/UserLayout.tsx @@ -7,7 +7,6 @@ import { Button, PlusIcon, UserIcon } from "@quri/ui"; import { H1 } from "@/components/ui/Headers"; import { StyledTabLink } from "@/components/ui/StyledTabLink"; -import { useUsername } from "@/hooks/useUsername"; import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; import { usePageQuery } from "@/relay/usePageQuery"; @@ -35,6 +34,7 @@ const Query = graphql` } ... on User { username + isMe # fields for count (empty/non-empty) # TODO: implement "totalCount" field instead models(first: 1) { @@ -97,8 +97,7 @@ export const UserLayout: FC< const user = extractFromGraphqlErrorUnion(result, "User"); - const myUsername = useUsername(); - const isMe = user.username === myUsername; + const isMe = user.isMe; return (
diff --git a/packages/hub/src/components/ReactRoot.tsx b/packages/hub/src/components/ReactRoot.tsx index 6349357d53..b868ace920 100644 --- a/packages/hub/src/components/ReactRoot.tsx +++ b/packages/hub/src/components/ReactRoot.tsx @@ -1,6 +1,4 @@ "use client"; -import { Session } from "next-auth"; -import { SessionProvider } from "next-auth/react"; import { FC, PropsWithChildren } from "react"; import { RelayEnvironmentProvider } from "react-relay"; @@ -13,21 +11,16 @@ import { ExitConfirmationWrapper } from "./ExitConfirmationWrapper"; // This component is used in the app's root layout to configure all common providers and wrappers. // It's also useful when you want to mount a separate React root. One example is CodeMirror tooltips, which are mounted as separate DOM elements. -export const ReactRoot: FC> = ({ - session, - children, -}) => { +export const ReactRoot: FC = ({ children }) => { const environment = getCurrentEnvironment(); return ( - - - - - {children} - - - - + + + + {children} + + + ); }; diff --git a/packages/hub/src/components/WithAuth/RedirectToLogin.tsx b/packages/hub/src/components/WithAuth/RedirectToLogin.tsx new file mode 100644 index 0000000000..2a48e6e386 --- /dev/null +++ b/packages/hub/src/components/WithAuth/RedirectToLogin.tsx @@ -0,0 +1,9 @@ +"use client"; + +import { redirect, usePathname } from "next/navigation"; +import { FC } from "react"; + +export const RedirectToLogin: FC = () => { + const pathname = usePathname(); + redirect(`/api/auth/signin?callbackUrl=${pathname}`); +}; diff --git a/packages/hub/src/components/WithAuth/index.tsx b/packages/hub/src/components/WithAuth/index.tsx new file mode 100644 index 0000000000..536015374c --- /dev/null +++ b/packages/hub/src/components/WithAuth/index.tsx @@ -0,0 +1,35 @@ +import "server-only"; + +import { FC, PropsWithChildren } from "react"; + +import { auth } from "@/auth"; +import { isRootEmail, isSignedIn } from "@/graphql/helpers/userHelpers"; +import { prisma } from "@/prisma"; + +import { RedirectToLogin } from "./RedirectToLogin"; + +type Props = PropsWithChildren<{ + rootOnly?: boolean; +}>; + +export const WithAuth: FC = async ({ children, rootOnly = false }) => { + const session = await auth(); + if (!isSignedIn(session)) { + return ; + } + + if (rootOnly) { + const user = await prisma.user.findUniqueOrThrow({ + where: { email: session.user.email }, + }); + if (!(user.email && user.emailVerified && isRootEmail(user.email))) { + return ( +
+
Unauthorized
+
+ ); + } + } + + return <>{children}; +}; diff --git a/packages/hub/src/components/layout/RootLayout/DesktopUserControls.tsx b/packages/hub/src/components/layout/RootLayout/DesktopUserControls.tsx index e11ff595e0..f0be0b138b 100644 --- a/packages/hub/src/components/layout/RootLayout/DesktopUserControls.tsx +++ b/packages/hub/src/components/layout/RootLayout/DesktopUserControls.tsx @@ -1,15 +1,16 @@ +import { Session } from "next-auth"; import { signIn } from "next-auth/react"; import { FC } from "react"; import { Button, Dropdown, DropdownMenu } from "@quri/ui"; -import { useUsername } from "@/hooks/useUsername"; - import { DropdownWithArrow } from "./DropdownWithArrow"; import { UserControlsMenu } from "./UserControlsMenu"; -export const DesktopUserControls: FC = () => { - const username = useUsername(); +export const DesktopUserControls: FC<{ session: Session | null }> = ({ + session, +}) => { + const username = session?.user?.username; return username ? (
diff --git a/packages/hub/src/components/layout/RootLayout/PageFooter.tsx b/packages/hub/src/components/layout/RootLayout/PageFooter.tsx index 3a804ceb46..230c2d9614 100644 --- a/packages/hub/src/components/layout/RootLayout/PageFooter.tsx +++ b/packages/hub/src/components/layout/RootLayout/PageFooter.tsx @@ -1,4 +1,3 @@ -"use client"; import Image from "next/image"; import { FC } from "react"; import { FaDiscord, FaGithub, FaRss } from "react-icons/fa"; diff --git a/packages/hub/src/components/layout/RootLayout/PageFooterIfNecessary.tsx b/packages/hub/src/components/layout/RootLayout/PageFooterIfNecessary.tsx new file mode 100644 index 0000000000..59fe220686 --- /dev/null +++ b/packages/hub/src/components/layout/RootLayout/PageFooterIfNecessary.tsx @@ -0,0 +1,19 @@ +"use client"; + +import { usePathname } from "next/navigation"; +import { FC } from "react"; + +import { isAiRoute, isModelRoute } from "@/routes"; + +import { PageFooter } from "./PageFooter"; + +// This is a client component because it needs to know the current pathname. +// Alternatively, we could use route groups (https://nextjs.org/docs/app/building-your-application/routing/route-groups), +// and then the footer could be RSC. But that would complicate the `app/` tree more than it's worth. +export const PageFooterIfNecessary: FC = () => { + const pathname = usePathname(); + + const showFooter = !isModelRoute(pathname) && !isAiRoute(pathname); + + return showFooter ? : null; +}; diff --git a/packages/hub/src/components/layout/RootLayout/PageMenu.tsx b/packages/hub/src/components/layout/RootLayout/PageMenu.tsx index 65d0e21b78..7a70cf15bd 100644 --- a/packages/hub/src/components/layout/RootLayout/PageMenu.tsx +++ b/packages/hub/src/components/layout/RootLayout/PageMenu.tsx @@ -1,6 +1,8 @@ -import { signIn, useSession } from "next-auth/react"; +"use client"; +import { Session } from "next-auth"; +import { signIn } from "next-auth/react"; import { FC, useState } from "react"; -import { useFragment } from "react-relay"; +import { useFragment, useLazyLoadQuery } from "react-relay"; import { graphql } from "relay-runtime"; import { @@ -16,8 +18,6 @@ import { UserCircleIcon, } from "@quri/ui"; -import { Link } from "@/components/ui/Link"; -import { useUsername } from "@/hooks/useUsername"; import { SQUIGGLE_DOCS_URL } from "@/lib/common"; import { aboutRoute, aiRoute, newModelRoute } from "@/routes"; @@ -30,12 +30,9 @@ import { useForceChooseUsername } from "./useForceChooseUsername"; import { UserControlsMenu } from "./UserControlsMenu"; import { PageMenu$key } from "@/__generated__/PageMenu.graphql"; +import { PageMenuQuery } from "@/__generated__/PageMenuQuery.graphql"; const AboutMenuLink: FC = (props) => { - const { data: session } = useSession(); - if (session) { - return null; - } return ; }; @@ -54,10 +51,6 @@ const AiMenuLink: FC = (props) => ( ); const NewModelMenuLink: FC = (props) => { - const { data: session } = useSession(); - if (!session) { - return null; - } return ( = ({ queryRef }) => { - const { data: session } = useSession(); +const DesktopMenu: FC = ({ queryRef, session }) => { const menu = useFragment(fragment, queryRef); return (
- + {!session && } {session ? ( <> @@ -103,15 +96,15 @@ const DesktopMenu: FC = ({ queryRef }) => { ) : null} - +
); }; -const MobileMenu: FC = ({ queryRef }) => { +const MobileMenu: FC = ({ queryRef, session }) => { const menu = useFragment(fragment, queryRef); - const username = useUsername(); + const username = session?.user?.username; const [open, setOpen] = useState(false); const Icon = username ? UserCircleIcon : DotsHorizontalIcon; @@ -137,8 +130,8 @@ const MobileMenu: FC = ({ queryRef }) => {
Menu - - + {session && } + {!session && } {username ? ( <> @@ -168,20 +161,29 @@ const MobileMenu: FC = ({ queryRef }) => { ); }; -export const PageMenu: FC = ({ queryRef }) => { - useForceChooseUsername(); +export const PageMenu: FC<{ session: Session | null }> = ({ session }) => { + // TODO - if redirecting, return a custom menu; right now we render the + // confused version where "New Model" button is visible, but "Sign In" button + // is visible too + useForceChooseUsername(session); + + const queryRef = useLazyLoadQuery( + graphql` + query PageMenuQuery($signedIn: Boolean!) { + ...PageMenu @arguments(signedIn: $signedIn) + } + `, + { signedIn: !!session } + ); return ( -
- - Squiggle Hub - + <>
- +
- +
-
+ ); }; diff --git a/packages/hub/src/components/layout/RootLayout/index.tsx b/packages/hub/src/components/layout/RootLayout/index.tsx new file mode 100644 index 0000000000..1e45b37a90 --- /dev/null +++ b/packages/hub/src/components/layout/RootLayout/index.tsx @@ -0,0 +1,49 @@ +import { FC, PropsWithChildren, Suspense } from "react"; + +import { auth } from "@/auth"; +import { Link } from "@/components/ui/Link"; + +import { ReactRoot } from "../../ReactRoot"; +import { PageFooterIfNecessary } from "./PageFooterIfNecessary"; +import { PageMenu } from "./PageMenu"; + +const WrappedPageMenu: FC = async () => { + // TODO - we wait for the session, and then we do another GraphQL query in + // ``, sequentially. We could select all relevant session data + // through GraphQL, or avoid GraphQL queries altogether. + const session = await auth(); + + return ; +}; + +const InnerRootLayout: FC = ({ children }) => { + return ( +
+
+ + Squiggle Hub + + + + +
+
+ {children} +
+ +
+ ); +}; + +export const RootLayout: FC = ({ children }) => { + return ( + + {children} + + ); +}; diff --git a/packages/hub/src/components/layout/RootLayout/useForceChooseUsername.ts b/packages/hub/src/components/layout/RootLayout/useForceChooseUsername.ts index ec90ad14cc..3f9f7c47f3 100644 --- a/packages/hub/src/components/layout/RootLayout/useForceChooseUsername.ts +++ b/packages/hub/src/components/layout/RootLayout/useForceChooseUsername.ts @@ -1,17 +1,17 @@ -import { useSession } from "next-auth/react"; +import { Session } from "next-auth"; +import { usePathname, useRouter } from "next/navigation"; import { chooseUsernameRoute } from "@/routes"; -export function useForceChooseUsername() { - const { data: session } = useSession(); +export function useForceChooseUsername(session: Session | null) { + const pathname = usePathname(); + const router = useRouter(); if ( session?.user && !session?.user.username && - !window.location.href.includes(chooseUsernameRoute()) + !pathname.includes(chooseUsernameRoute()) ) { - // Next's redirect() is broken for components included from the root layout - // https://github.com/vercel/next.js/issues/42556 (it's closed but not really solved) - window.location.href = chooseUsernameRoute(); + router.push(chooseUsernameRoute()); } } diff --git a/packages/hub/src/graphql/types/User.ts b/packages/hub/src/graphql/types/User.ts index 9461ca5a8c..82db46789d 100644 --- a/packages/hub/src/graphql/types/User.ts +++ b/packages/hub/src/graphql/types/User.ts @@ -138,5 +138,9 @@ export const User = builder.prismaNode("User", { }, resolve: async (user) => isRootUser(user), }), + isMe: t.boolean({ + resolve: async (user, _, { session }) => + !!(user.email && user.email === session?.user.email), + }), }), }); diff --git a/packages/hub/src/hooks/useUsername.ts b/packages/hub/src/hooks/useUsername.ts deleted file mode 100644 index 6efb10083b..0000000000 --- a/packages/hub/src/hooks/useUsername.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { useSession } from "next-auth/react"; - -export function useUsername(): string | undefined { - const { data: session } = useSession(); - return session?.user?.username; -} From 3b1715d34a83f5d965e4cc3155ebf0b2c83e2eca Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 26 Nov 2024 20:07:51 -0300 Subject: [PATCH 05/68] fix builder --- packages/hub/src/scripts/buildRecentModelRevision/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hub/src/scripts/buildRecentModelRevision/main.ts b/packages/hub/src/scripts/buildRecentModelRevision/main.ts index bfcc6e6771..afe0793de9 100644 --- a/packages/hub/src/scripts/buildRecentModelRevision/main.ts +++ b/packages/hub/src/scripts/buildRecentModelRevision/main.ts @@ -18,7 +18,7 @@ async function runWorker( ): Promise { return new Promise((resolve, _) => { console.log("Spawning worker process for Revision ID: " + revisionId); - const worker = spawn("node", [__dirname + "/worker.js"], { + const worker = spawn("node", [__dirname + "/worker.mjs"], { stdio: ["pipe", "pipe", "pipe", "ipc"], }); From 6745146da166d5280d8bf7b632f2329fa26641a5 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 26 Nov 2024 20:15:21 -0300 Subject: [PATCH 06/68] improve some loading states --- .../src/app/models/[owner]/[slug]/loading.tsx | 11 ------- .../src/app/models/[owner]/[slug]/page.tsx | 29 ++++++++++++++++++- .../[slug]/revisions/ModelRevisionsList.tsx | 1 - .../[owner]/[slug]/revisions/layout.tsx | 12 ++++++++ .../[owner]/[slug]/revisions/loading.tsx | 5 ++++ .../models/[owner]/[slug]/revisions/page.tsx | 7 +---- .../components/layout/RootLayout/index.tsx | 1 + 7 files changed, 47 insertions(+), 19 deletions(-) delete mode 100644 packages/hub/src/app/models/[owner]/[slug]/loading.tsx create mode 100644 packages/hub/src/app/models/[owner]/[slug]/revisions/layout.tsx create mode 100644 packages/hub/src/app/models/[owner]/[slug]/revisions/loading.tsx diff --git a/packages/hub/src/app/models/[owner]/[slug]/loading.tsx b/packages/hub/src/app/models/[owner]/[slug]/loading.tsx deleted file mode 100644 index 732a9336af..0000000000 --- a/packages/hub/src/app/models/[owner]/[slug]/loading.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import Skeleton from "react-loading-skeleton"; - -import { FullLayoutWithPadding } from "@/components/layout/FullLayoutWithPadding"; - -export default function Loading() { - return ( - - - - ); -} diff --git a/packages/hub/src/app/models/[owner]/[slug]/page.tsx b/packages/hub/src/app/models/[owner]/[slug]/page.tsx index a72ec311e0..f7a4474e18 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/page.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/page.tsx @@ -1,3 +1,6 @@ +import { Suspense } from "react"; +import Skeleton from "react-loading-skeleton"; + import { loadPageQuery } from "@/relay/loadPageQuery"; import { EditModelPage } from "./EditModelPage"; @@ -10,7 +13,7 @@ type Props = { params: Promise<{ owner: string; slug: string }>; }; -export default async function Page({ params }: Props) { +async function InnerPage({ params }: Props) { const { owner, slug } = await params; const query = await loadPageQuery(QueryNode, { input: { owner, slug }, @@ -22,3 +25,27 @@ export default async function Page({ params }: Props) {
); } + +const Loading = () => { + return ( +
+
+ +
+
+ +
+
+ ); +}; + +export default async function Page({ params }: Props) { + return ( +
+ {/* Intentionally not using loading.tsx here, because this route has sub-routes where that doesn't make sense. */} + }> + + +
+ ); +} diff --git a/packages/hub/src/app/models/[owner]/[slug]/revisions/ModelRevisionsList.tsx b/packages/hub/src/app/models/[owner]/[slug]/revisions/ModelRevisionsList.tsx index 55032b646c..9eae2b4b80 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/revisions/ModelRevisionsList.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/revisions/ModelRevisionsList.tsx @@ -144,7 +144,6 @@ export const ModelRevisionsList: FC<{ return (
-
Revision history
{revisions.edges.map((edge) => ( +
Revision history
+ {children} + + ); +} diff --git a/packages/hub/src/app/models/[owner]/[slug]/revisions/loading.tsx b/packages/hub/src/app/models/[owner]/[slug]/revisions/loading.tsx new file mode 100644 index 0000000000..702e460f6c --- /dev/null +++ b/packages/hub/src/app/models/[owner]/[slug]/revisions/loading.tsx @@ -0,0 +1,5 @@ +import Skeleton from "react-loading-skeleton"; + +export default function Loading() { + return ; +} diff --git a/packages/hub/src/app/models/[owner]/[slug]/revisions/page.tsx b/packages/hub/src/app/models/[owner]/[slug]/revisions/page.tsx index 1e9c63f9d7..4a8f114767 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/revisions/page.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/revisions/page.tsx @@ -1,4 +1,3 @@ -import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; import { loadPageQuery } from "@/relay/loadPageQuery"; import { ModelRevisionsList } from "./ModelRevisionsList"; @@ -17,9 +16,5 @@ export default async function ModelPage({ input: { owner, slug }, }); - return ( - - - - ); + return ; } diff --git a/packages/hub/src/components/layout/RootLayout/index.tsx b/packages/hub/src/components/layout/RootLayout/index.tsx index 1e45b37a90..b1487f69f6 100644 --- a/packages/hub/src/components/layout/RootLayout/index.tsx +++ b/packages/hub/src/components/layout/RootLayout/index.tsx @@ -23,6 +23,7 @@ const InnerRootLayout: FC = ({ children }) => { Squiggle Hub + {/* Top menu is not essential for fetching and rendering other content, so we render it in a Suspense boundary */} From 83b99c28a918077218e59de7e38eafb8ff468af0 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 26 Nov 2024 21:04:26 -0300 Subject: [PATCH 07/68] nextjs docs --- packages/hub/docs/nextjs.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 packages/hub/docs/nextjs.md diff --git a/packages/hub/docs/nextjs.md b/packages/hub/docs/nextjs.md new file mode 100644 index 0000000000..b8d7e94423 --- /dev/null +++ b/packages/hub/docs/nextjs.md @@ -0,0 +1,11 @@ +# Notes on Next.js + +## Loading pages + +Avoid generic `loading.tsx` files. Thoughtful loading states are good, but the generic top-level loading state was harmful: + +- it doesn't match the final rendered state so it looks more like a flash of unrelated content than a skeleton +- loading state means that the previous page will disappear faster than necessary, and that's bad; in other words, the loading state is only useful when it hints where the content will appear +- in addition, the loading state means that `` prefetching won't work + +Avoid nested `loading.tsx` files. I'm not sure why but they might cause double flash of loading states, similar to this thread: https://www.reddit.com/r/nextjs/comments/17hn1a5/nested_loading_states/ From 22053aab970eb85bcf8fa581d92bad872318a90e Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 26 Nov 2024 21:29:59 -0300 Subject: [PATCH 08/68] split frontpage into sub-routes --- .../hub/src/app/(frontpage)/FrontPage.tsx | 24 ++++++++ .../{ => (frontpage)}/FrontPageModelList.tsx | 0 .../definitions/DefinitionsPage.tsx | 24 ++++++++ .../definitions}/FrontPageDefinitionList.tsx | 0 .../src/app/(frontpage)/definitions/page.tsx | 13 +++++ .../groups}/FrontPageGroupList.tsx | 0 .../src/app/(frontpage)/groups/GroupsPage.tsx | 24 ++++++++ .../hub/src/app/(frontpage)/groups/page.tsx | 13 +++++ packages/hub/src/app/(frontpage)/layout.tsx | 22 +++++++ .../hub/src/app/{ => (frontpage)}/page.tsx | 7 +-- .../variables}/FrontPageVariableList.tsx | 0 .../(frontpage)/variables/VariablesPage.tsx | 24 ++++++++ .../src/app/(frontpage)/variables/page.tsx | 13 +++++ packages/hub/src/app/FrontPage.tsx | 57 ------------------- .../hub/src/components/ui/StyledTabLink.tsx | 9 ++- packages/hub/src/routes.ts | 12 ++++ 16 files changed, 177 insertions(+), 65 deletions(-) create mode 100644 packages/hub/src/app/(frontpage)/FrontPage.tsx rename packages/hub/src/app/{ => (frontpage)}/FrontPageModelList.tsx (100%) create mode 100644 packages/hub/src/app/(frontpage)/definitions/DefinitionsPage.tsx rename packages/hub/src/app/{ => (frontpage)/definitions}/FrontPageDefinitionList.tsx (100%) create mode 100644 packages/hub/src/app/(frontpage)/definitions/page.tsx rename packages/hub/src/app/{ => (frontpage)/groups}/FrontPageGroupList.tsx (100%) create mode 100644 packages/hub/src/app/(frontpage)/groups/GroupsPage.tsx create mode 100644 packages/hub/src/app/(frontpage)/groups/page.tsx create mode 100644 packages/hub/src/app/(frontpage)/layout.tsx rename packages/hub/src/app/{ => (frontpage)}/page.tsx (64%) rename packages/hub/src/app/{ => (frontpage)/variables}/FrontPageVariableList.tsx (100%) create mode 100644 packages/hub/src/app/(frontpage)/variables/VariablesPage.tsx create mode 100644 packages/hub/src/app/(frontpage)/variables/page.tsx delete mode 100644 packages/hub/src/app/FrontPage.tsx diff --git a/packages/hub/src/app/(frontpage)/FrontPage.tsx b/packages/hub/src/app/(frontpage)/FrontPage.tsx new file mode 100644 index 0000000000..2bc375cafb --- /dev/null +++ b/packages/hub/src/app/(frontpage)/FrontPage.tsx @@ -0,0 +1,24 @@ +"use client"; +import { FC } from "react"; +import { graphql } from "relay-runtime"; + +import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; +import { usePageQuery } from "@/relay/usePageQuery"; + +import { FrontPageModelList } from "./FrontPageModelList"; + +import { FrontPageQuery } from "@/__generated__/FrontPageQuery.graphql"; + +const Query = graphql` + query FrontPageQuery { + ...FrontPageModelList + } +`; + +export const FrontPage: FC<{ + query: SerializablePreloadedQuery; +}> = ({ query }) => { + const [data] = usePageQuery(Query, query); + + return ; +}; diff --git a/packages/hub/src/app/FrontPageModelList.tsx b/packages/hub/src/app/(frontpage)/FrontPageModelList.tsx similarity index 100% rename from packages/hub/src/app/FrontPageModelList.tsx rename to packages/hub/src/app/(frontpage)/FrontPageModelList.tsx diff --git a/packages/hub/src/app/(frontpage)/definitions/DefinitionsPage.tsx b/packages/hub/src/app/(frontpage)/definitions/DefinitionsPage.tsx new file mode 100644 index 0000000000..d59a37e234 --- /dev/null +++ b/packages/hub/src/app/(frontpage)/definitions/DefinitionsPage.tsx @@ -0,0 +1,24 @@ +"use client"; +import { FC } from "react"; +import { graphql } from "relay-runtime"; + +import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; +import { usePageQuery } from "@/relay/usePageQuery"; + +import { FrontPageDefinitionList } from "./FrontPageDefinitionList"; + +import { DefinitionsPageQuery } from "@/__generated__/DefinitionsPageQuery.graphql"; + +const Query = graphql` + query DefinitionsPageQuery { + ...FrontPageDefinitionList + } +`; + +export const DefinitionsPage: FC<{ + query: SerializablePreloadedQuery; +}> = ({ query }) => { + const [data] = usePageQuery(Query, query); + + return ; +}; diff --git a/packages/hub/src/app/FrontPageDefinitionList.tsx b/packages/hub/src/app/(frontpage)/definitions/FrontPageDefinitionList.tsx similarity index 100% rename from packages/hub/src/app/FrontPageDefinitionList.tsx rename to packages/hub/src/app/(frontpage)/definitions/FrontPageDefinitionList.tsx diff --git a/packages/hub/src/app/(frontpage)/definitions/page.tsx b/packages/hub/src/app/(frontpage)/definitions/page.tsx new file mode 100644 index 0000000000..f5841c782d --- /dev/null +++ b/packages/hub/src/app/(frontpage)/definitions/page.tsx @@ -0,0 +1,13 @@ +import { loadPageQuery } from "@/relay/loadPageQuery"; + +import { DefinitionsPage } from "./DefinitionsPage"; + +import QueryNode, { + DefinitionsPageQuery, +} from "@/__generated__/DefinitionsPageQuery.graphql"; + +export default async function OuterDefinitionsPage() { + const query = await loadPageQuery(QueryNode, {}); + + return ; +} diff --git a/packages/hub/src/app/FrontPageGroupList.tsx b/packages/hub/src/app/(frontpage)/groups/FrontPageGroupList.tsx similarity index 100% rename from packages/hub/src/app/FrontPageGroupList.tsx rename to packages/hub/src/app/(frontpage)/groups/FrontPageGroupList.tsx diff --git a/packages/hub/src/app/(frontpage)/groups/GroupsPage.tsx b/packages/hub/src/app/(frontpage)/groups/GroupsPage.tsx new file mode 100644 index 0000000000..2eacc47f6a --- /dev/null +++ b/packages/hub/src/app/(frontpage)/groups/GroupsPage.tsx @@ -0,0 +1,24 @@ +"use client"; +import { FC } from "react"; +import { graphql } from "relay-runtime"; + +import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; +import { usePageQuery } from "@/relay/usePageQuery"; + +import { FrontPageGroupList } from "./FrontPageGroupList"; + +import { GroupsPageQuery } from "@/__generated__/GroupsPageQuery.graphql"; + +const Query = graphql` + query GroupsPageQuery { + ...FrontPageGroupList + } +`; + +export const GroupsPage: FC<{ + query: SerializablePreloadedQuery; +}> = ({ query }) => { + const [data] = usePageQuery(Query, query); + + return ; +}; diff --git a/packages/hub/src/app/(frontpage)/groups/page.tsx b/packages/hub/src/app/(frontpage)/groups/page.tsx new file mode 100644 index 0000000000..f56d145bc0 --- /dev/null +++ b/packages/hub/src/app/(frontpage)/groups/page.tsx @@ -0,0 +1,13 @@ +import { loadPageQuery } from "@/relay/loadPageQuery"; + +import { GroupsPage } from "./GroupsPage"; + +import QueryNode, { + GroupsPageQuery, +} from "@/__generated__/GroupsPageQuery.graphql"; + +export default async function OuterGroupsPage() { + const query = await loadPageQuery(QueryNode, {}); + + return ; +} diff --git a/packages/hub/src/app/(frontpage)/layout.tsx b/packages/hub/src/app/(frontpage)/layout.tsx new file mode 100644 index 0000000000..5c84bd611a --- /dev/null +++ b/packages/hub/src/app/(frontpage)/layout.tsx @@ -0,0 +1,22 @@ +import { PropsWithChildren } from "react"; + +import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; +import { + StyledTabLink, + StyledTabLinkList, +} from "@/components/ui/StyledTabLink"; +import { definitionsRoute, groupsRoute, variablesRoute } from "@/routes"; + +export default function FrontPageLayout({ children }: PropsWithChildren) { + return ( + + + + + + + +
{children}
+
+ ); +} diff --git a/packages/hub/src/app/page.tsx b/packages/hub/src/app/(frontpage)/page.tsx similarity index 64% rename from packages/hub/src/app/page.tsx rename to packages/hub/src/app/(frontpage)/page.tsx index 0bec776b5d..1a0f0ba90e 100644 --- a/packages/hub/src/app/page.tsx +++ b/packages/hub/src/app/(frontpage)/page.tsx @@ -1,4 +1,3 @@ -import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; import { loadPageQuery } from "@/relay/loadPageQuery"; import { FrontPage } from "./FrontPage"; @@ -10,9 +9,5 @@ import QueryNode, { export default async function OuterFrontPage() { const query = await loadPageQuery(QueryNode, {}); - return ( - - - - ); + return ; } diff --git a/packages/hub/src/app/FrontPageVariableList.tsx b/packages/hub/src/app/(frontpage)/variables/FrontPageVariableList.tsx similarity index 100% rename from packages/hub/src/app/FrontPageVariableList.tsx rename to packages/hub/src/app/(frontpage)/variables/FrontPageVariableList.tsx diff --git a/packages/hub/src/app/(frontpage)/variables/VariablesPage.tsx b/packages/hub/src/app/(frontpage)/variables/VariablesPage.tsx new file mode 100644 index 0000000000..d27070c0e9 --- /dev/null +++ b/packages/hub/src/app/(frontpage)/variables/VariablesPage.tsx @@ -0,0 +1,24 @@ +"use client"; +import { FC } from "react"; +import { graphql } from "relay-runtime"; + +import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; +import { usePageQuery } from "@/relay/usePageQuery"; + +import { FrontPageVariableList } from "./FrontPageVariableList"; + +import { VariablesPageQuery } from "@/__generated__/VariablesPageQuery.graphql"; + +const Query = graphql` + query VariablesPageQuery { + ...FrontPageVariableList + } +`; + +export const VariablesPage: FC<{ + query: SerializablePreloadedQuery; +}> = ({ query }) => { + const [data] = usePageQuery(Query, query); + + return ; +}; diff --git a/packages/hub/src/app/(frontpage)/variables/page.tsx b/packages/hub/src/app/(frontpage)/variables/page.tsx new file mode 100644 index 0000000000..581bbc3bfe --- /dev/null +++ b/packages/hub/src/app/(frontpage)/variables/page.tsx @@ -0,0 +1,13 @@ +import { loadPageQuery } from "@/relay/loadPageQuery"; + +import { VariablesPage } from "./VariablesPage"; + +import QueryNode, { + VariablesPageQuery, +} from "@/__generated__/VariablesPageQuery.graphql"; + +export default async function OuterVariablesPage() { + const query = await loadPageQuery(QueryNode, {}); + + return ; +} diff --git a/packages/hub/src/app/FrontPage.tsx b/packages/hub/src/app/FrontPage.tsx deleted file mode 100644 index 79df9c5439..0000000000 --- a/packages/hub/src/app/FrontPage.tsx +++ /dev/null @@ -1,57 +0,0 @@ -"use client"; -import { FC } from "react"; -import { graphql } from "relay-runtime"; - -import { StyledTab } from "@quri/ui"; - -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; - -import { FrontPageDefinitionList } from "./FrontPageDefinitionList"; -import { FrontPageGroupList } from "./FrontPageGroupList"; -import { FrontPageModelList } from "./FrontPageModelList"; -import { FrontPageVariableList } from "./FrontPageVariableList"; - -import { FrontPageQuery } from "@/__generated__/FrontPageQuery.graphql"; - -const Query = graphql` - query FrontPageQuery { - ...FrontPageModelList - ...FrontPageVariableList - ...FrontPageDefinitionList - ...FrontPageGroupList - } -`; - -export const FrontPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [data] = usePageQuery(Query, query); - - return ( - - - - - - - -
- - - - - - - - - - - - - - -
-
- ); -}; diff --git a/packages/hub/src/components/ui/StyledTabLink.tsx b/packages/hub/src/components/ui/StyledTabLink.tsx index bc1bc7eaa0..babeb53f08 100644 --- a/packages/hub/src/components/ui/StyledTabLink.tsx +++ b/packages/hub/src/components/ui/StyledTabLink.tsx @@ -11,6 +11,7 @@ type StyledTabLinkProps = { href: string; icon?: FC; selected?: (pathname: string, href: string) => boolean; + prefetch?: boolean; }; type StyledTabLinkType = React.FC & { @@ -22,15 +23,19 @@ export const StyledTabLink: StyledTabLinkType = ({ href, icon: Icon, selected, + prefetch, }) => { const pathname = usePathname(); const isSelected = selected ? selected(pathname, href) : pathname === href; return ( - + ); }; -StyledTabLink.List = StyledTab.ListDiv; +const StyledTabLinkList = StyledTab.ListDiv; +StyledTabLink.List = StyledTabLinkList; + +export { StyledTabLinkList }; diff --git a/packages/hub/src/routes.ts b/packages/hub/src/routes.ts index be49bcbf72..5640823bf0 100644 --- a/packages/hub/src/routes.ts +++ b/packages/hub/src/routes.ts @@ -148,6 +148,18 @@ export function userVariablesRoute({ username }: { username: string }) { return `/users/${username}/variables`; } +export function groupsRoute() { + return "/groups"; +} + +export function definitionsRoute() { + return "/definitions"; +} + +export function variablesRoute() { + return "/variables"; +} + export function groupRoute({ slug }: { slug: string }) { return `/groups/${slug}`; } From 2da3cf500cf0852f3bfe485ccdd70450b3634cee Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 26 Nov 2024 21:30:30 -0300 Subject: [PATCH 09/68] fix margin --- packages/hub/src/app/(frontpage)/layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hub/src/app/(frontpage)/layout.tsx b/packages/hub/src/app/(frontpage)/layout.tsx index 5c84bd611a..2a12752081 100644 --- a/packages/hub/src/app/(frontpage)/layout.tsx +++ b/packages/hub/src/app/(frontpage)/layout.tsx @@ -16,7 +16,7 @@ export default function FrontPageLayout({ children }: PropsWithChildren) { -
{children}
+
{children}
); } From 3e33ca3352316aade2fe7d8eefba488c8063f164 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 26 Nov 2024 21:38:22 -0300 Subject: [PATCH 10/68] loading state for front page --- packages/hub/src/app/(frontpage)/loading.tsx | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/hub/src/app/(frontpage)/loading.tsx diff --git a/packages/hub/src/app/(frontpage)/loading.tsx b/packages/hub/src/app/(frontpage)/loading.tsx new file mode 100644 index 0000000000..702e460f6c --- /dev/null +++ b/packages/hub/src/app/(frontpage)/loading.tsx @@ -0,0 +1,5 @@ +import Skeleton from "react-loading-skeleton"; + +export default function Loading() { + return ; +} From 9c4ba442ae76affbd607712ffdf5c0eed41d1be7 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Wed, 27 Nov 2024 01:10:31 -0300 Subject: [PATCH 11/68] cleanups --- packages/hub/src/app/new/model/page.tsx | 2 +- packages/hub/src/models/components/ModelCard.tsx | 5 ----- packages/hub/src/server/ai/analytics/index.ts | 2 +- packages/hub/src/server/ai/data.ts | 2 +- packages/hub/src/server/{helpers.ts => userHelpers.ts} | 0 5 files changed, 3 insertions(+), 8 deletions(-) rename packages/hub/src/server/{helpers.ts => userHelpers.ts} (100%) diff --git a/packages/hub/src/app/new/model/page.tsx b/packages/hub/src/app/new/model/page.tsx index ac979abdad..616c86b872 100644 --- a/packages/hub/src/app/new/model/page.tsx +++ b/packages/hub/src/app/new/model/page.tsx @@ -5,7 +5,7 @@ import { z } from "zod"; import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; import { SelectGroupOption } from "@/components/SelectGroup"; import { getCurrentEnvironment } from "@/relay/environment"; -import { getSessionUserOrRedirect } from "@/server/helpers"; +import { getSessionUserOrRedirect } from "@/server/userHelpers"; import { NewModel } from "./NewModel"; diff --git a/packages/hub/src/models/components/ModelCard.tsx b/packages/hub/src/models/components/ModelCard.tsx index 0cff1193e8..754f2d2f2d 100644 --- a/packages/hub/src/models/components/ModelCard.tsx +++ b/packages/hub/src/models/components/ModelCard.tsx @@ -45,11 +45,6 @@ const Fragment = graphql` ... on SquiggleSnippet { id code - version - seed - autorunMode - sampleCount - xyPointLength } } relativeValuesExports { diff --git a/packages/hub/src/server/ai/analytics/index.ts b/packages/hub/src/server/ai/analytics/index.ts index 9e592cd4df..eb5ab480a2 100644 --- a/packages/hub/src/server/ai/analytics/index.ts +++ b/packages/hub/src/server/ai/analytics/index.ts @@ -7,7 +7,7 @@ import { CodeArtifact, Workflow } from "@quri/squiggle-ai/server"; import { prisma } from "@/prisma"; import { getAiCodec } from "@/server/ai/utils"; import { v2WorkflowDataSchema } from "@/server/ai/v2_0"; -import { checkRootUser } from "@/server/helpers"; +import { checkRootUser } from "@/server/userHelpers"; async function loadWorkflows() { await checkRootUser(); diff --git a/packages/hub/src/server/ai/data.ts b/packages/hub/src/server/ai/data.ts index e8d400c750..b5c83172eb 100644 --- a/packages/hub/src/server/ai/data.ts +++ b/packages/hub/src/server/ai/data.ts @@ -2,7 +2,7 @@ import "server-only"; import { prisma } from "@/prisma"; -import { getSessionUserOrRedirect } from "../helpers"; +import { getSessionUserOrRedirect } from "../userHelpers"; import { decodeDbWorkflowToClientWorkflow } from "./storage"; export async function loadWorkflows({ diff --git a/packages/hub/src/server/helpers.ts b/packages/hub/src/server/userHelpers.ts similarity index 100% rename from packages/hub/src/server/helpers.ts rename to packages/hub/src/server/userHelpers.ts From 75e9822dad884329e72dfd2c54c6f0d337d0fbb4 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Wed, 27 Nov 2024 12:59:18 -0300 Subject: [PATCH 12/68] rewrite front page to RSC (no pagination yet) --- .../hub/src/app/(frontpage)/FrontPage.tsx | 24 ---- .../app/(frontpage)/FrontPageModelList.tsx | 45 ------ packages/hub/src/app/(frontpage)/page.tsx | 21 +-- .../hub/src/models/components/ModelCard.tsx | 71 ++-------- .../hub/src/models/components/ModelList.tsx | 39 ++---- packages/hub/src/server/modelHelpers.ts | 131 ++++++++++++++++++ 6 files changed, 162 insertions(+), 169 deletions(-) delete mode 100644 packages/hub/src/app/(frontpage)/FrontPage.tsx delete mode 100644 packages/hub/src/app/(frontpage)/FrontPageModelList.tsx create mode 100644 packages/hub/src/server/modelHelpers.ts diff --git a/packages/hub/src/app/(frontpage)/FrontPage.tsx b/packages/hub/src/app/(frontpage)/FrontPage.tsx deleted file mode 100644 index 2bc375cafb..0000000000 --- a/packages/hub/src/app/(frontpage)/FrontPage.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client"; -import { FC } from "react"; -import { graphql } from "relay-runtime"; - -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; - -import { FrontPageModelList } from "./FrontPageModelList"; - -import { FrontPageQuery } from "@/__generated__/FrontPageQuery.graphql"; - -const Query = graphql` - query FrontPageQuery { - ...FrontPageModelList - } -`; - -export const FrontPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [data] = usePageQuery(Query, query); - - return ; -}; diff --git a/packages/hub/src/app/(frontpage)/FrontPageModelList.tsx b/packages/hub/src/app/(frontpage)/FrontPageModelList.tsx deleted file mode 100644 index 1f37ef31f9..0000000000 --- a/packages/hub/src/app/(frontpage)/FrontPageModelList.tsx +++ /dev/null @@ -1,45 +0,0 @@ -"use client"; - -import { FC } from "react"; -import { graphql, usePaginationFragment } from "react-relay"; - -import { ModelList } from "@/models/components/ModelList"; - -import { FrontPageModelList$key } from "@/__generated__/FrontPageModelList.graphql"; -import { FrontPageModelListPaginationQuery } from "@/__generated__/FrontPageModelListPaginationQuery.graphql"; - -const Fragment = graphql` - fragment FrontPageModelList on Query - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 8 } - ) - @refetchable(queryName: "FrontPageModelListPaginationQuery") { - models(first: $count, after: $cursor) - @connection(key: "FrontPageModelList_models") { - # necessary for Relay - edges { - __typename - } - ...ModelList - } - } -`; - -type Props = { - dataRef: FrontPageModelList$key; -}; - -export const FrontPageModelList: FC = ({ dataRef }) => { - const { - data: { models }, - loadNext, - } = usePaginationFragment< - FrontPageModelListPaginationQuery, - FrontPageModelList$key - >(Fragment, dataRef); - - return ( - - ); -}; diff --git a/packages/hub/src/app/(frontpage)/page.tsx b/packages/hub/src/app/(frontpage)/page.tsx index 1a0f0ba90e..0eb77e9b8a 100644 --- a/packages/hub/src/app/(frontpage)/page.tsx +++ b/packages/hub/src/app/(frontpage)/page.tsx @@ -1,13 +1,14 @@ -import { loadPageQuery } from "@/relay/loadPageQuery"; - -import { FrontPage } from "./FrontPage"; - -import QueryNode, { - FrontPageQuery, -} from "@/__generated__/FrontPageQuery.graphql"; +import { ModelList } from "@/models/components/ModelList"; +import { loadModelCards } from "@/server/modelHelpers"; export default async function OuterFrontPage() { - const query = await loadPageQuery(QueryNode, {}); - - return ; + const { models } = await loadModelCards(); + + return ( + + ); } diff --git a/packages/hub/src/models/components/ModelCard.tsx b/packages/hub/src/models/components/ModelCard.tsx index 754f2d2f2d..8961026d82 100644 --- a/packages/hub/src/models/components/ModelCard.tsx +++ b/packages/hub/src/models/components/ModelCard.tsx @@ -1,6 +1,4 @@ import { FC } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; import { CodeSyntaxHighlighter, NumberShower } from "@quri/squiggle-components"; import { XIcon } from "@quri/ui"; @@ -19,50 +17,10 @@ import { VariablesDropdown, } from "@/lib/VariablesDropdown"; import { modelRoute, ownerRoute } from "@/routes"; - -import { ModelCard$key } from "@/__generated__/ModelCard.graphql"; - -const Fragment = graphql` - fragment ModelCard on Model { - id - slug - updatedAtTimestamp - owner { - __typename - slug - } - isPrivate - variables { - variableName - currentRevision { - variableType - title - } - } - currentRevision { - content { - __typename - ... on SquiggleSnippet { - id - code - } - } - relativeValuesExports { - variableName - definition { - slug - } - } - buildStatus - lastBuild { - runSeconds - } - } - } -`; +import { ModelCardData } from "@/server/modelHelpers"; type Props = { - modelRef: ModelCard$key; + model: ModelCardData; showOwner?: boolean; }; @@ -99,16 +57,9 @@ const RunTime: FC<{ seconds: number }> = ({ seconds }) => (
); -export const ModelCard: FC = ({ modelRef, showOwner = true }) => { - const model = useFragment(Fragment, modelRef); - const { - owner, - slug, - updatedAtTimestamp, - isPrivate, - variables, - currentRevision, - } = model; +export const ModelCard: FC = ({ model, showOwner = true }) => { + const { owner, slug, updatedAt, isPrivate, variables, currentRevision } = + model; const variableRevisions: VariableRevision[] = variables.map((v) => ({ variableName: v.variableName, @@ -128,9 +79,11 @@ export const ModelCard: FC = ({ modelRef, showOwner = true }) => { variableRevisions, relativeValuesExports ); - const { buildStatus, lastBuild, content } = currentRevision; - const body = - content.__typename === "SquiggleSnippet" ? content.code : undefined; + const { + // buildStatus, lastBuild, + squiggleSnippet, + } = currentRevision; + const body = squiggleSnippet?.code; const menuItems = ( = ({ modelRef, showOwner = true }) => { ), isPrivate && , - updatedAtTimestamp && ( - + updatedAt && ( + ), ]} /> diff --git a/packages/hub/src/models/components/ModelList.tsx b/packages/hub/src/models/components/ModelList.tsx index 88a98bac9a..76b9384679 100644 --- a/packages/hub/src/models/components/ModelList.tsx +++ b/packages/hub/src/models/components/ModelList.tsx @@ -1,52 +1,29 @@ "use client"; import { FC } from "react"; -import { graphql, useFragment } from "react-relay"; -import { LoadMore } from "@/components/LoadMore"; +import { ModelCardData } from "@/server/modelHelpers"; import { ModelCard } from "./ModelCard"; -import { ModelList$key } from "@/__generated__/ModelList.graphql"; - -const Fragment = graphql` - fragment ModelList on ModelConnection { - edges { - node { - id - ...ModelCard - } - } - pageInfo { - hasNextPage - } - } -`; - type Props = { - connectionRef: ModelList$key; - loadNext(count: number): unknown; + models: ModelCardData[]; + // loadNext(count: number): unknown; showOwner?: boolean; }; export const ModelList: FC = ({ - connectionRef, - loadNext, + models, + // loadNext, showOwner, }) => { - const connection = useFragment(Fragment, connectionRef); - return (
- {connection.edges.map((edge) => ( - + {models.map((model) => ( + ))}
- {connection.pageInfo.hasNextPage && } + {/* {connection.pageInfo.hasNextPage && } */}
); }; diff --git a/packages/hub/src/server/modelHelpers.ts b/packages/hub/src/server/modelHelpers.ts new file mode 100644 index 0000000000..b4493a9c90 --- /dev/null +++ b/packages/hub/src/server/modelHelpers.ts @@ -0,0 +1,131 @@ +import "server-only"; + +import { auth } from "@/auth"; +import { prisma } from "@/prisma"; + +function ownerToGraphqlCompatible(owner: { + slug: string; + user: { id: string } | null; + group: { id: string } | null; +}) { + const __typename = owner.user ? "User" : "Group"; + return { slug: owner.slug, __typename }; +} + +export async function loadModelCards() { + const limit = 20; + const session = await auth(); + + const dbModels = await prisma.model.findMany({ + select: { + id: true, + slug: true, + updatedAt: true, + owner: { + select: { + slug: true, + user: { + select: { id: true }, + }, + group: { + select: { id: true }, + }, + }, + }, + isPrivate: true, + variables: { + select: { + variableName: true, + currentRevision: { + select: { + variableType: true, + title: true, + }, + }, + }, + }, + currentRevision: { + select: { + contentType: true, + squiggleSnippet: { + select: { + id: true, + code: true, + }, + }, + relativeValuesExports: { + select: { + variableName: true, + definition: { + select: { + slug: true, + }, + }, + }, + }, + builds: { + select: { + runSeconds: true, + errors: true, + }, + orderBy: { + createdAt: "desc", + }, + take: 1, + }, + }, + }, + }, + orderBy: { updatedAt: "desc" }, + where: { + OR: [ + { isPrivate: false }, + ...(session + ? [ + { + owner: { + user: { email: session.user.email }, + }, + }, + { + owner: { + group: { + memberships: { + some: { + user: { email: session.user.email }, + }, + }, + }, + }, + }, + ] + : []), + ], + }, + take: limit + 1, + }); + + type DbModel = (typeof dbModels)[number]; + + const models = dbModels + .filter( + ( + model + ): model is Omit & { + currentRevision: NonNullable; + } => !!model.currentRevision + ) + .map((model) => ({ + ...model, + owner: ownerToGraphqlCompatible(model.owner), + })); + + return { + models: limit ? models.slice(0, limit) : models, + hasMore: limit ? models.length > limit : false, + }; +} + +export type ModelCardData = Awaited< + ReturnType +>["models"][number]; From 623b4908fc2b396618e2a1093f2acb8a2e08e606 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Wed, 27 Nov 2024 14:30:35 -0300 Subject: [PATCH 13/68] new ModelList in more components; fix import tooltips --- packages/hub/src/app/(frontpage)/page.tsx | 4 +- .../src/app/groups/[slug]/GroupModelList.tsx | 56 ------ .../hub/src/app/groups/[slug]/GroupPage.tsx | 34 ---- packages/hub/src/app/groups/[slug]/page.tsx | 34 +++- .../[slug]/EditSquiggleSnippetModel.tsx | 2 +- .../app/users/[username]/UserModelList.tsx | 49 ----- .../hub/src/app/users/[username]/UserPage.tsx | 32 --- .../hub/src/app/users/[username]/page.tsx | 29 +-- packages/hub/src/components/ReactRoot.tsx | 28 ++- .../hub/src/models/components/ModelCard.tsx | 2 +- .../hub/src/models/components/ModelList.tsx | 2 +- packages/hub/src/server/groupHelpers.ts | 21 ++ packages/hub/src/server/modelHelpers.ts | 131 ------------- packages/hub/src/server/models/actions.ts | 13 ++ packages/hub/src/server/models/data.ts | 185 ++++++++++++++++++ .../src/squiggle/components/ImportTooltip.tsx | 45 ++--- 16 files changed, 308 insertions(+), 359 deletions(-) delete mode 100644 packages/hub/src/app/groups/[slug]/GroupModelList.tsx delete mode 100644 packages/hub/src/app/groups/[slug]/GroupPage.tsx delete mode 100644 packages/hub/src/app/users/[username]/UserModelList.tsx delete mode 100644 packages/hub/src/app/users/[username]/UserPage.tsx create mode 100644 packages/hub/src/server/groupHelpers.ts delete mode 100644 packages/hub/src/server/modelHelpers.ts create mode 100644 packages/hub/src/server/models/actions.ts create mode 100644 packages/hub/src/server/models/data.ts diff --git a/packages/hub/src/app/(frontpage)/page.tsx b/packages/hub/src/app/(frontpage)/page.tsx index 0eb77e9b8a..f5b352bb08 100644 --- a/packages/hub/src/app/(frontpage)/page.tsx +++ b/packages/hub/src/app/(frontpage)/page.tsx @@ -1,7 +1,7 @@ import { ModelList } from "@/models/components/ModelList"; -import { loadModelCards } from "@/server/modelHelpers"; +import { loadModelCards } from "@/server/models/data"; -export default async function OuterFrontPage() { +export default async function FrontPage() { const { models } = await loadModelCards(); return ( diff --git a/packages/hub/src/app/groups/[slug]/GroupModelList.tsx b/packages/hub/src/app/groups/[slug]/GroupModelList.tsx deleted file mode 100644 index 27fb1e43fa..0000000000 --- a/packages/hub/src/app/groups/[slug]/GroupModelList.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { FC } from "react"; -import { usePaginationFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { ModelList } from "@/models/components/ModelList"; - -import { useIsGroupMember } from "./hooks"; - -import { GroupModelList$key } from "@/__generated__/GroupModelList.graphql"; - -const Fragment = graphql` - fragment GroupModelList on Group - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 20 } - ) - @refetchable(queryName: "GroupModelListPaginationQuery") { - ...hooks_useIsGroupMember - models(first: $count, after: $cursor) - @connection(key: "GroupModelList_models") { - edges { - __typename - } - ...ModelList - } - } -`; - -type Props = { - groupRef: GroupModelList$key; -}; - -export const GroupModelList: FC = ({ groupRef }) => { - const { data: group, loadNext } = usePaginationFragment(Fragment, groupRef); - - const { models } = group; - const isMember = useIsGroupMember(group); - - return ( -
- {models.edges.length ? ( - - ) : ( -
- {isMember - ? "This group doesn't have any models." - : "This group does not have any public models."} -
- )} -
- ); -}; diff --git a/packages/hub/src/app/groups/[slug]/GroupPage.tsx b/packages/hub/src/app/groups/[slug]/GroupPage.tsx deleted file mode 100644 index beb93eafdb..0000000000 --- a/packages/hub/src/app/groups/[slug]/GroupPage.tsx +++ /dev/null @@ -1,34 +0,0 @@ -"use client"; -import { FC } from "react"; -import { graphql } from "relay-runtime"; - -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; - -import { GroupModelList } from "./GroupModelList"; - -import { GroupPageQuery } from "@/__generated__/GroupPageQuery.graphql"; - -const Query = graphql` - query GroupPageQuery($slug: String!) { - result: group(slug: $slug) { - __typename - ... on Group { - id - slug - ...GroupModelList - } - } - } -`; - -export const GroupPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [{ result }] = usePageQuery(Query, query); - - const group = extractFromGraphqlErrorUnion(result, "Group"); - - return ; -}; diff --git a/packages/hub/src/app/groups/[slug]/page.tsx b/packages/hub/src/app/groups/[slug]/page.tsx index abfe56f6c1..b9eea23548 100644 --- a/packages/hub/src/app/groups/[slug]/page.tsx +++ b/packages/hub/src/app/groups/[slug]/page.tsx @@ -1,10 +1,6 @@ -import { loadPageQuery } from "@/relay/loadPageQuery"; - -import { GroupPage } from "./GroupPage"; - -import QueryNode, { - GroupPageQuery, -} from "@/__generated__/GroupPageQuery.graphql"; +import { ModelList } from "@/models/components/ModelList"; +import { hasGroupMembership } from "@/server/groupHelpers"; +import { loadModelCards } from "@/server/models/data"; type Props = { params: Promise<{ slug: string }>; @@ -12,9 +8,27 @@ type Props = { export default async function OuterGroupPage({ params }: Props) { const { slug } = await params; - const query = await loadPageQuery(QueryNode, { - slug, + + const { models } = await loadModelCards({ + ownerSlug: slug, }); + const isMember = await hasGroupMembership(slug); - return ; + return ( +
+ {models.length ? ( + + ) : ( +
+ {isMember + ? "This group doesn't have any models." + : "This group does not have any public models."} +
+ )} +
+ ); } diff --git a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx index c7171c6973..2a912078fa 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx @@ -453,7 +453,7 @@ export const EditSquiggleSnippetModel: FC = ({ }: { importId: string; }) => ( - + ); diff --git a/packages/hub/src/app/users/[username]/UserModelList.tsx b/packages/hub/src/app/users/[username]/UserModelList.tsx deleted file mode 100644 index 8740e2f3db..0000000000 --- a/packages/hub/src/app/users/[username]/UserModelList.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { FC } from "react"; -import { usePaginationFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { ModelList } from "@/models/components/ModelList"; - -import { UserModelList$key } from "@/__generated__/UserModelList.graphql"; - -const Fragment = graphql` - fragment UserModelList on User - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 20 } - ) - @refetchable(queryName: "UserModelListPaginationQuery") { - models(first: $count, after: $cursor) - @connection(key: "UserModelList_models") { - edges { - __typename - } - ...ModelList - } - } -`; - -type Props = { - dataRef: UserModelList$key; -}; - -export const UserModelList: FC = ({ dataRef }) => { - const { - data: { models }, - loadNext, - } = usePaginationFragment(Fragment, dataRef); - - return ( -
- {models.edges.length ? ( - - ) : ( -
No models to show.
- )} -
- ); -}; diff --git a/packages/hub/src/app/users/[username]/UserPage.tsx b/packages/hub/src/app/users/[username]/UserPage.tsx deleted file mode 100644 index 420094c54a..0000000000 --- a/packages/hub/src/app/users/[username]/UserPage.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; -import { FC } from "react"; -import { graphql } from "relay-runtime"; - -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; - -import { UserModelList } from "./UserModelList"; - -import { UserPageQuery } from "@/__generated__/UserPageQuery.graphql"; - -const Query = graphql` - query UserPageQuery($username: String!) { - userByUsername(username: $username) { - __typename - ... on User { - ...UserModelList - } - } - } -`; - -export const UserPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [{ userByUsername: result }] = usePageQuery(Query, query); - - const user = extractFromGraphqlErrorUnion(result, "User"); - - return ; -}; diff --git a/packages/hub/src/app/users/[username]/page.tsx b/packages/hub/src/app/users/[username]/page.tsx index c3c5f53180..15ee05957c 100644 --- a/packages/hub/src/app/users/[username]/page.tsx +++ b/packages/hub/src/app/users/[username]/page.tsx @@ -1,24 +1,31 @@ import { Metadata } from "next"; -import { loadPageQuery } from "@/relay/loadPageQuery"; - -import { UserPage } from "./UserPage"; - -import QueryNode, { - UserPageQuery, -} from "@/__generated__/UserPageQuery.graphql"; +import { ModelList } from "@/models/components/ModelList"; +import { loadModelCards } from "@/server/models/data"; type Props = { params: Promise<{ username: string }>; }; -export default async function OuterUserPage({ params }: Props) { +export default async function UserPage({ params }: Props) { const { username } = await params; - const query = await loadPageQuery(QueryNode, { - username, + const { models } = await loadModelCards({ + ownerSlug: username, }); - return ; + return ( +
+ {models.length ? ( + + ) : ( +
No models to show.
+ )} +
+ ); } export async function generateMetadata({ params }: Props): Promise { diff --git a/packages/hub/src/components/ReactRoot.tsx b/packages/hub/src/components/ReactRoot.tsx index b868ace920..33fdba82a3 100644 --- a/packages/hub/src/components/ReactRoot.tsx +++ b/packages/hub/src/components/ReactRoot.tsx @@ -9,18 +9,34 @@ import { getCurrentEnvironment } from "@/relay/environment"; import { ExitConfirmationWrapper } from "./ExitConfirmationWrapper"; +type Props = PropsWithChildren<{ + // CodeMirror tooltips are not compatible with ConfirmationWrapper, because they use a separate React root. + // In a separate root, `useRouter` is not available, because it depends on a global Next.js context. + // TODO: reimplement CodeMirror editor with portals. + confirmationWrapper?: boolean; +}>; + // This component is used in the app's root layout to configure all common providers and wrappers. // It's also useful when you want to mount a separate React root. One example is CodeMirror tooltips, which are mounted as separate DOM elements. -export const ReactRoot: FC = ({ children }) => { +export const ReactRoot: FC = ({ + children, + confirmationWrapper = true, +}) => { const environment = getCurrentEnvironment(); + let content = ( + + {children} + + ); + + if (confirmationWrapper) { + content = {content}; + } + return ( - - - {children} - - + {content} ); }; diff --git a/packages/hub/src/models/components/ModelCard.tsx b/packages/hub/src/models/components/ModelCard.tsx index 8961026d82..06fde66522 100644 --- a/packages/hub/src/models/components/ModelCard.tsx +++ b/packages/hub/src/models/components/ModelCard.tsx @@ -17,7 +17,7 @@ import { VariablesDropdown, } from "@/lib/VariablesDropdown"; import { modelRoute, ownerRoute } from "@/routes"; -import { ModelCardData } from "@/server/modelHelpers"; +import { ModelCardData } from "@/server/models/data"; type Props = { model: ModelCardData; diff --git a/packages/hub/src/models/components/ModelList.tsx b/packages/hub/src/models/components/ModelList.tsx index 76b9384679..c8ac77a842 100644 --- a/packages/hub/src/models/components/ModelList.tsx +++ b/packages/hub/src/models/components/ModelList.tsx @@ -1,7 +1,7 @@ "use client"; import { FC } from "react"; -import { ModelCardData } from "@/server/modelHelpers"; +import { ModelCardData } from "@/server/models/data"; import { ModelCard } from "./ModelCard"; diff --git a/packages/hub/src/server/groupHelpers.ts b/packages/hub/src/server/groupHelpers.ts new file mode 100644 index 0000000000..081d68effe --- /dev/null +++ b/packages/hub/src/server/groupHelpers.ts @@ -0,0 +1,21 @@ +import { auth } from "@/auth"; +import { prisma } from "@/prisma"; + +export async function hasGroupMembership(groupSlug: string) { + const session = await auth(); + const userId = session?.user.id; + if (!userId) { + return false; + } + + const group = await prisma.group.findFirstOrThrow({ + select: { id: true }, + where: { + asOwner: { slug: groupSlug }, + memberships: { + some: { userId }, + }, + }, + }); + return !!group; +} diff --git a/packages/hub/src/server/modelHelpers.ts b/packages/hub/src/server/modelHelpers.ts deleted file mode 100644 index b4493a9c90..0000000000 --- a/packages/hub/src/server/modelHelpers.ts +++ /dev/null @@ -1,131 +0,0 @@ -import "server-only"; - -import { auth } from "@/auth"; -import { prisma } from "@/prisma"; - -function ownerToGraphqlCompatible(owner: { - slug: string; - user: { id: string } | null; - group: { id: string } | null; -}) { - const __typename = owner.user ? "User" : "Group"; - return { slug: owner.slug, __typename }; -} - -export async function loadModelCards() { - const limit = 20; - const session = await auth(); - - const dbModels = await prisma.model.findMany({ - select: { - id: true, - slug: true, - updatedAt: true, - owner: { - select: { - slug: true, - user: { - select: { id: true }, - }, - group: { - select: { id: true }, - }, - }, - }, - isPrivate: true, - variables: { - select: { - variableName: true, - currentRevision: { - select: { - variableType: true, - title: true, - }, - }, - }, - }, - currentRevision: { - select: { - contentType: true, - squiggleSnippet: { - select: { - id: true, - code: true, - }, - }, - relativeValuesExports: { - select: { - variableName: true, - definition: { - select: { - slug: true, - }, - }, - }, - }, - builds: { - select: { - runSeconds: true, - errors: true, - }, - orderBy: { - createdAt: "desc", - }, - take: 1, - }, - }, - }, - }, - orderBy: { updatedAt: "desc" }, - where: { - OR: [ - { isPrivate: false }, - ...(session - ? [ - { - owner: { - user: { email: session.user.email }, - }, - }, - { - owner: { - group: { - memberships: { - some: { - user: { email: session.user.email }, - }, - }, - }, - }, - }, - ] - : []), - ], - }, - take: limit + 1, - }); - - type DbModel = (typeof dbModels)[number]; - - const models = dbModels - .filter( - ( - model - ): model is Omit & { - currentRevision: NonNullable; - } => !!model.currentRevision - ) - .map((model) => ({ - ...model, - owner: ownerToGraphqlCompatible(model.owner), - })); - - return { - models: limit ? models.slice(0, limit) : models, - hasMore: limit ? models.length > limit : false, - }; -} - -export type ModelCardData = Awaited< - ReturnType ->["models"][number]; diff --git a/packages/hub/src/server/models/actions.ts b/packages/hub/src/server/models/actions.ts new file mode 100644 index 0000000000..2c21e4225d --- /dev/null +++ b/packages/hub/src/server/models/actions.ts @@ -0,0 +1,13 @@ +"use server"; +import { loadModelCard, ModelCardData } from "./data"; + +// used in ImportTooltip +export async function loadModelCardAction({ + owner, + slug, +}: { + owner: string; + slug: string; +}): Promise { + return loadModelCard({ owner, slug }); +} diff --git a/packages/hub/src/server/models/data.ts b/packages/hub/src/server/models/data.ts new file mode 100644 index 0000000000..6e975b1ca0 --- /dev/null +++ b/packages/hub/src/server/models/data.ts @@ -0,0 +1,185 @@ +import "server-only"; + +import { Prisma } from "@prisma/client"; + +import { auth } from "@/auth"; +import { prisma } from "@/prisma"; + +// duplicates code in graphql/helpers/modelHelpers.ts +async function modelWhereHasAccess(): Promise { + const session = await auth(); + const orParts: Prisma.ModelWhereInput[] = [{ isPrivate: false }]; + if (session) { + orParts.push({ + owner: { + OR: [ + { + user: { email: session.user.email }, + }, + { + group: { + memberships: { + some: { + user: { email: session.user.email }, + }, + }, + }, + }, + ], + }, + }); + } + return orParts; +} + +function dbModelToModelCard(dbModel: DbModelCard) { + function check(model: DbModelCard): asserts model is Omit< + DbModelCard, + "currentRevision" + > & { + currentRevision: NonNullable; + } { + if (!model.currentRevision) { + throw new Error("Model has no current revision"); + } + } + check(dbModel); + + const ownerToGraphqlCompatible = (owner: { + slug: string; + user: { id: string } | null; + group: { id: string } | null; + }) => { + const __typename = owner.user ? "User" : "Group"; + return { slug: owner.slug, __typename }; + }; + + return { + ...dbModel, + owner: ownerToGraphqlCompatible(dbModel.owner), + }; +} + +const modelCardSelect = { + id: true, + slug: true, + updatedAt: true, + owner: { + select: { + slug: true, + user: { + select: { id: true }, + }, + group: { + select: { id: true }, + }, + }, + }, + isPrivate: true, + variables: { + select: { + variableName: true, + currentRevision: { + select: { + variableType: true, + title: true, + }, + }, + }, + }, + currentRevision: { + select: { + contentType: true, + squiggleSnippet: { + select: { + id: true, + code: true, + }, + }, + relativeValuesExports: { + select: { + variableName: true, + definition: { + select: { + slug: true, + }, + }, + }, + }, + builds: { + select: { + runSeconds: true, + errors: true, + }, + orderBy: { + createdAt: "desc", + }, + take: 1, + }, + }, + }, +} satisfies Prisma.ModelSelect; + +type DbModelCard = NonNullable< + Awaited< + ReturnType< + typeof prisma.model.findFirst<{ select: typeof modelCardSelect }> + > + > +>; + +export type ModelCardData = ReturnType; + +export async function loadModelCards( + filters: { + ownerSlug?: string; + } = {} +) { + const limit = 20; + + const dbModels = await prisma.model.findMany({ + select: modelCardSelect, + orderBy: { updatedAt: "desc" }, + where: { + ...(filters.ownerSlug + ? { + owner: { + slug: filters.ownerSlug, + }, + } + : {}), + OR: await modelWhereHasAccess(), + }, + take: limit + 1, + }); + + const models = dbModels.map(dbModelToModelCard); + + return { + models: limit ? models.slice(0, limit) : models, + hasMore: limit ? models.length > limit : false, + }; +} + +export async function loadModelCard({ + owner, + slug, +}: { + owner: string; + slug: string; +}): Promise { + const dbModel = await prisma.model.findFirst({ + select: modelCardSelect, + where: { + slug: slug, + owner: { slug: owner }, + OR: await modelWhereHasAccess(), + }, + }); + + if (!dbModel) { + return null; + } + + return dbModelToModelCard(dbModel); +} diff --git a/packages/hub/src/squiggle/components/ImportTooltip.tsx b/packages/hub/src/squiggle/components/ImportTooltip.tsx index a8e5139029..4459db0ac1 100644 --- a/packages/hub/src/squiggle/components/ImportTooltip.tsx +++ b/packages/hub/src/squiggle/components/ImportTooltip.tsx @@ -1,13 +1,13 @@ import clsx from "clsx"; -import { FC } from "react"; -import { graphql, useLazyLoadQuery } from "react-relay"; +import { FC, useEffect, useState } from "react"; +import Skeleton from "react-loading-skeleton"; import { ModelCard } from "@/models/components/ModelCard"; +import { loadModelCardAction } from "@/server/models/actions"; +import { ModelCardData } from "@/server/models/data"; import { parseSourceId } from "./linker"; -import { ImportTooltipQuery } from "@/__generated__/ImportTooltipQuery.graphql"; - type Props = { importId: string; }; @@ -15,28 +15,15 @@ type Props = { export const ImportTooltip: FC = ({ importId }) => { const { owner, slug } = parseSourceId(importId); - const { model } = useLazyLoadQuery( - graphql` - query ImportTooltipQuery($input: QueryModelInput!) { - model(input: $input) { - __typename - ... on Model { - ...ModelCard - } - } - } - `, - { input: { owner, slug } } + const [model, setModel] = useState( + "loading" ); - if (model.__typename !== "Model") { - return ( -
- {"Couldn't load "} - {owner}/{slug} -
- ); - } + useEffect(() => { + // TODO - this is done with a server action, so it's not cached. + // A route would be better. + loadModelCardAction({ owner, slug }).then(setModel); + }, []); return (
= ({ importId }) => { "text-base" )} > - + {model === "loading" ? ( + + ) : model ? ( + + ) : ( +
+ Model not found. +
+ )}
); }; From 77524612f2c519aec5cdb1aede620ea10f1e516f Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Wed, 27 Nov 2024 15:06:52 -0300 Subject: [PATCH 14/68] pagination for models --- packages/hub/src/app/(frontpage)/page.tsx | 10 ++----- packages/hub/src/app/groups/[slug]/page.tsx | 10 ++----- .../hub/src/app/users/[username]/page.tsx | 10 ++----- packages/hub/src/hooks/usePaginator.ts | 28 ++++++++++++++++++ .../hub/src/models/components/ModelList.tsx | 17 +++++------ packages/hub/src/server/models/data.ts | 29 ++++++++++++++----- 6 files changed, 66 insertions(+), 38 deletions(-) create mode 100644 packages/hub/src/hooks/usePaginator.ts diff --git a/packages/hub/src/app/(frontpage)/page.tsx b/packages/hub/src/app/(frontpage)/page.tsx index f5b352bb08..a992ec27f6 100644 --- a/packages/hub/src/app/(frontpage)/page.tsx +++ b/packages/hub/src/app/(frontpage)/page.tsx @@ -2,13 +2,7 @@ import { ModelList } from "@/models/components/ModelList"; import { loadModelCards } from "@/server/models/data"; export default async function FrontPage() { - const { models } = await loadModelCards(); + const page = await loadModelCards(); - return ( - - ); + return ; } diff --git a/packages/hub/src/app/groups/[slug]/page.tsx b/packages/hub/src/app/groups/[slug]/page.tsx index b9eea23548..b3c9fdd4c2 100644 --- a/packages/hub/src/app/groups/[slug]/page.tsx +++ b/packages/hub/src/app/groups/[slug]/page.tsx @@ -9,19 +9,15 @@ type Props = { export default async function OuterGroupPage({ params }: Props) { const { slug } = await params; - const { models } = await loadModelCards({ + const page = await loadModelCards({ ownerSlug: slug, }); const isMember = await hasGroupMembership(slug); return (
- {models.length ? ( - + {page.items.length ? ( + ) : (
{isMember diff --git a/packages/hub/src/app/users/[username]/page.tsx b/packages/hub/src/app/users/[username]/page.tsx index 15ee05957c..3d9c7e1af4 100644 --- a/packages/hub/src/app/users/[username]/page.tsx +++ b/packages/hub/src/app/users/[username]/page.tsx @@ -9,18 +9,14 @@ type Props = { export default async function UserPage({ params }: Props) { const { username } = await params; - const { models } = await loadModelCards({ + const page = await loadModelCards({ ownerSlug: username, }); return (
- {models.length ? ( - + {page.items.length ? ( + ) : (
No models to show.
)} diff --git a/packages/hub/src/hooks/usePaginator.ts b/packages/hub/src/hooks/usePaginator.ts new file mode 100644 index 0000000000..6461931de0 --- /dev/null +++ b/packages/hub/src/hooks/usePaginator.ts @@ -0,0 +1,28 @@ +import { useState } from "react"; + +import { Paginated } from "@/server/models/data"; + +export function usePaginator(initialPage: Paginated): { + items: T[]; + // this is intentionally named `loadNext` instead of `loadMore` to avoid confusion + loadNext?: (limit: number) => void; +} { + const [{ items, loadMore }, setPage] = useState(initialPage); + + return { + items, + loadNext: loadMore + ? (limit: number) => { + loadMore(limit).then(({ items: newItems, loadMore: newLoadMore }) => { + // In theory, there should be no duplicates, if `loadMore` is implemented correctly. + // But maybe we should check for duplicate keys and skip them. + // This would require a separate (optional?) `getKey` parameter to this hook. + setPage(({ items }) => ({ + items: [...items, ...newItems], + loadMore: newLoadMore, + })); + }); + } + : undefined, + }; +} diff --git a/packages/hub/src/models/components/ModelList.tsx b/packages/hub/src/models/components/ModelList.tsx index c8ac77a842..15a33ab954 100644 --- a/packages/hub/src/models/components/ModelList.tsx +++ b/packages/hub/src/models/components/ModelList.tsx @@ -1,21 +1,20 @@ "use client"; import { FC } from "react"; -import { ModelCardData } from "@/server/models/data"; +import { LoadMore } from "@/components/LoadMore"; +import { usePaginator } from "@/hooks/usePaginator"; +import { ModelCardData, Paginated } from "@/server/models/data"; import { ModelCard } from "./ModelCard"; type Props = { - models: ModelCardData[]; - // loadNext(count: number): unknown; + page: Paginated; showOwner?: boolean; }; -export const ModelList: FC = ({ - models, - // loadNext, - showOwner, -}) => { +export const ModelList: FC = ({ page, showOwner }) => { + const { items: models, loadNext } = usePaginator(page); + return (
@@ -23,7 +22,7 @@ export const ModelList: FC = ({ ))}
- {/* {connection.pageInfo.hasNextPage && } */} + {loadNext && }
); }; diff --git a/packages/hub/src/server/models/data.ts b/packages/hub/src/server/models/data.ts index 6e975b1ca0..6f10932b17 100644 --- a/packages/hub/src/server/models/data.ts +++ b/packages/hub/src/server/models/data.ts @@ -130,21 +130,29 @@ type DbModelCard = NonNullable< export type ModelCardData = ReturnType; +export type Paginated = { + items: T[]; + loadMore?: (limit: number) => Promise>; +}; + export async function loadModelCards( - filters: { + params: { ownerSlug?: string; + cursor?: string; + limit?: number; } = {} -) { - const limit = 20; +): Promise> { + const limit = params.limit ?? 20; const dbModels = await prisma.model.findMany({ select: modelCardSelect, orderBy: { updatedAt: "desc" }, + cursor: params.cursor ? { id: params.cursor } : undefined, where: { - ...(filters.ownerSlug + ...(params.ownerSlug ? { owner: { - slug: filters.ownerSlug, + slug: params.ownerSlug, }, } : {}), @@ -155,9 +163,16 @@ export async function loadModelCards( const models = dbModels.map(dbModelToModelCard); + const nextCursor = models[models.length - 1]?.id; + + async function loadMore(limit: number) { + "use server"; + return loadModelCards({ ...params, cursor: nextCursor, limit }); + } + return { - models: limit ? models.slice(0, limit) : models, - hasMore: limit ? models.length > limit : false, + items: models.slice(0, limit), + loadMore: models.length > limit ? loadMore : undefined, }; } From d210ee9bea00b3beff7927017ef0ced5d46b7eaa Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Wed, 27 Nov 2024 15:26:20 -0300 Subject: [PATCH 15/68] migrate GroupList to RSC --- .../(frontpage)/groups/FrontPageGroupList.tsx | 41 -------- .../src/app/(frontpage)/groups/GroupsPage.tsx | 24 ----- .../hub/src/app/(frontpage)/groups/page.tsx | 13 +-- packages/hub/src/app/groups/[slug]/page.tsx | 2 +- .../users/[username]/groups/UserGroupList.tsx | 45 --------- .../[username]/groups/UserGroupsPage.tsx | 32 ------ .../src/app/users/[username]/groups/page.tsx | 23 ++--- .../hub/src/groups/components/GroupCard.tsx | 21 +--- .../hub/src/groups/components/GroupList.tsx | 33 ++----- packages/hub/src/server/groupHelpers.ts | 21 ---- packages/hub/src/server/groups/data.ts | 97 +++++++++++++++++++ 11 files changed, 127 insertions(+), 225 deletions(-) delete mode 100644 packages/hub/src/app/(frontpage)/groups/FrontPageGroupList.tsx delete mode 100644 packages/hub/src/app/(frontpage)/groups/GroupsPage.tsx delete mode 100644 packages/hub/src/app/users/[username]/groups/UserGroupList.tsx delete mode 100644 packages/hub/src/app/users/[username]/groups/UserGroupsPage.tsx delete mode 100644 packages/hub/src/server/groupHelpers.ts create mode 100644 packages/hub/src/server/groups/data.ts diff --git a/packages/hub/src/app/(frontpage)/groups/FrontPageGroupList.tsx b/packages/hub/src/app/(frontpage)/groups/FrontPageGroupList.tsx deleted file mode 100644 index efff1707c4..0000000000 --- a/packages/hub/src/app/(frontpage)/groups/FrontPageGroupList.tsx +++ /dev/null @@ -1,41 +0,0 @@ -"use client"; -import { FC } from "react"; -import { graphql, usePaginationFragment } from "react-relay"; - -import { GroupList } from "@/groups/components/GroupList"; - -import { FrontPageGroupList$key } from "@/__generated__/FrontPageGroupList.graphql"; -import { FrontPageGroupListPaginationQuery } from "@/__generated__/FrontPageGroupListPaginationQuery.graphql"; - -const Fragment = graphql` - fragment FrontPageGroupList on Query - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 20 } - ) - @refetchable(queryName: "FrontPageGroupListPaginationQuery") { - groups(first: $count, after: $cursor) - @connection(key: "FrontPageGroupList_groups") { - edges { - __typename - } - ...GroupList - } - } -`; - -type Props = { - dataRef: FrontPageGroupList$key; -}; - -export const FrontPageGroupList: FC = ({ dataRef }) => { - const { - data: { groups }, - loadNext, - } = usePaginationFragment< - FrontPageGroupListPaginationQuery, - FrontPageGroupList$key - >(Fragment, dataRef); - - return ; -}; diff --git a/packages/hub/src/app/(frontpage)/groups/GroupsPage.tsx b/packages/hub/src/app/(frontpage)/groups/GroupsPage.tsx deleted file mode 100644 index 2eacc47f6a..0000000000 --- a/packages/hub/src/app/(frontpage)/groups/GroupsPage.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client"; -import { FC } from "react"; -import { graphql } from "relay-runtime"; - -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; - -import { FrontPageGroupList } from "./FrontPageGroupList"; - -import { GroupsPageQuery } from "@/__generated__/GroupsPageQuery.graphql"; - -const Query = graphql` - query GroupsPageQuery { - ...FrontPageGroupList - } -`; - -export const GroupsPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [data] = usePageQuery(Query, query); - - return ; -}; diff --git a/packages/hub/src/app/(frontpage)/groups/page.tsx b/packages/hub/src/app/(frontpage)/groups/page.tsx index f56d145bc0..030ebcfc5f 100644 --- a/packages/hub/src/app/(frontpage)/groups/page.tsx +++ b/packages/hub/src/app/(frontpage)/groups/page.tsx @@ -1,13 +1,8 @@ -import { loadPageQuery } from "@/relay/loadPageQuery"; - -import { GroupsPage } from "./GroupsPage"; - -import QueryNode, { - GroupsPageQuery, -} from "@/__generated__/GroupsPageQuery.graphql"; +import { GroupList } from "@/groups/components/GroupList"; +import { loadGroupCards } from "@/server/groups/data"; export default async function OuterGroupsPage() { - const query = await loadPageQuery(QueryNode, {}); + const page = await loadGroupCards(); - return ; + return ; } diff --git a/packages/hub/src/app/groups/[slug]/page.tsx b/packages/hub/src/app/groups/[slug]/page.tsx index b3c9fdd4c2..b14dedac0f 100644 --- a/packages/hub/src/app/groups/[slug]/page.tsx +++ b/packages/hub/src/app/groups/[slug]/page.tsx @@ -1,5 +1,5 @@ import { ModelList } from "@/models/components/ModelList"; -import { hasGroupMembership } from "@/server/groupHelpers"; +import { hasGroupMembership } from "@/server/groups/data"; import { loadModelCards } from "@/server/models/data"; type Props = { diff --git a/packages/hub/src/app/users/[username]/groups/UserGroupList.tsx b/packages/hub/src/app/users/[username]/groups/UserGroupList.tsx deleted file mode 100644 index c1a96d19d6..0000000000 --- a/packages/hub/src/app/users/[username]/groups/UserGroupList.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { FC } from "react"; -import { usePaginationFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { GroupList } from "@/groups/components/GroupList"; - -import { UserGroupList$key } from "@/__generated__/UserGroupList.graphql"; - -const Fragment = graphql` - fragment UserGroupList on User - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 20 } - ) - @refetchable(queryName: "UserGroupListPaginationQuery") { - groups(first: $count, after: $cursor) - @connection(key: "UserGroupList_groups") { - edges { - __typename - } - ...GroupList - } - } -`; - -type Props = { - dataRef: UserGroupList$key; -}; - -export const UserGroupList: FC = ({ dataRef }) => { - const { - data: { groups }, - loadNext, - } = usePaginationFragment(Fragment, dataRef); - - return ( -
- {groups.edges.length ? ( - - ) : ( -
No groups to show.
- )} -
- ); -}; diff --git a/packages/hub/src/app/users/[username]/groups/UserGroupsPage.tsx b/packages/hub/src/app/users/[username]/groups/UserGroupsPage.tsx deleted file mode 100644 index 999b8f8c39..0000000000 --- a/packages/hub/src/app/users/[username]/groups/UserGroupsPage.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; -import { FC } from "react"; -import { graphql } from "relay-runtime"; - -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; - -import { UserGroupList } from "./UserGroupList"; - -import { UserGroupsPageQuery } from "@/__generated__/UserGroupsPageQuery.graphql"; - -const Query = graphql` - query UserGroupsPageQuery($username: String!) { - userByUsername(username: $username) { - __typename - ... on User { - ...UserGroupList - } - } - } -`; - -export const UserGroupsPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [{ userByUsername: result }] = usePageQuery(Query, query); - - const user = extractFromGraphqlErrorUnion(result, "User"); - - return ; -}; diff --git a/packages/hub/src/app/users/[username]/groups/page.tsx b/packages/hub/src/app/users/[username]/groups/page.tsx index 7246da8a27..515ea4fb32 100644 --- a/packages/hub/src/app/users/[username]/groups/page.tsx +++ b/packages/hub/src/app/users/[username]/groups/page.tsx @@ -1,12 +1,7 @@ import { Metadata } from "next"; -import { loadPageQuery } from "@/relay/loadPageQuery"; - -import { UserGroupsPage } from "./UserGroupsPage"; - -import QueryNode, { - UserGroupsPageQuery, -} from "@/__generated__/UserGroupsPageQuery.graphql"; +import { GroupList } from "@/groups/components/GroupList"; +import { loadGroupCards } from "@/server/groups/data"; type Props = { params: Promise<{ username: string }>; @@ -14,11 +9,17 @@ type Props = { export default async function OuterUserGroupsPage({ params }: Props) { const { username } = await params; - const query = await loadPageQuery(QueryNode, { - username, - }); + const page = await loadGroupCards({ username }); - return ; + return ( +
+ {page.items.length ? ( + + ) : ( +
No groups to show.
+ )} +
+ ); } export async function generateMetadata({ params }: Props): Promise { diff --git a/packages/hub/src/groups/components/GroupCard.tsx b/packages/hub/src/groups/components/GroupCard.tsx index a18313a7fb..6195d49151 100644 --- a/packages/hub/src/groups/components/GroupCard.tsx +++ b/packages/hub/src/groups/components/GroupCard.tsx @@ -1,27 +1,14 @@ import { FC } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; import { EntityCard, UpdatedStatus } from "@/components/EntityCard"; import { groupRoute } from "@/routes"; - -import { GroupCard$key } from "@/__generated__/GroupCard.graphql"; - -const Fragment = graphql` - fragment GroupCard on Group { - id - slug - updatedAtTimestamp - } -`; +import { GroupCardData } from "@/server/groups/data"; type Props = { - groupRef: GroupCard$key; + group: GroupCardData; }; -export const GroupCard: FC = ({ groupRef }) => { - const group = useFragment(Fragment, groupRef); - +export const GroupCard: FC = ({ group }) => { return ( = ({ groupRef }) => { })} showOwner={false} slug={group.slug} - menuItems={} + menuItems={} /> ); }; diff --git a/packages/hub/src/groups/components/GroupList.tsx b/packages/hub/src/groups/components/GroupList.tsx index 96a95ebbac..a7b658833f 100644 --- a/packages/hub/src/groups/components/GroupList.tsx +++ b/packages/hub/src/groups/components/GroupList.tsx @@ -1,43 +1,28 @@ "use client"; import { FC } from "react"; -import { graphql, useFragment } from "react-relay"; import { LoadMore } from "@/components/LoadMore"; +import { usePaginator } from "@/hooks/usePaginator"; +import { GroupCardData } from "@/server/groups/data"; +import { Paginated } from "@/server/models/data"; import { GroupCard } from "./GroupCard"; -import { GroupList$key } from "@/__generated__/GroupList.graphql"; - -const Fragment = graphql` - fragment GroupList on GroupConnection { - edges { - node { - id - ...GroupCard - } - } - pageInfo { - hasNextPage - } - } -`; - type Props = { - connectionRef: GroupList$key; - loadNext(count: number): unknown; + page: Paginated; }; -export const GroupList: FC = ({ connectionRef, loadNext }) => { - const connection = useFragment(Fragment, connectionRef); +export const GroupList: FC = ({ page: initialPage }) => { + const page = usePaginator(initialPage); return (
- {connection.edges.map((edge) => ( - + {page.items.map((group) => ( + ))}
- {connection.pageInfo.hasNextPage && } + {page.loadNext && }
); }; diff --git a/packages/hub/src/server/groupHelpers.ts b/packages/hub/src/server/groupHelpers.ts deleted file mode 100644 index 081d68effe..0000000000 --- a/packages/hub/src/server/groupHelpers.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { auth } from "@/auth"; -import { prisma } from "@/prisma"; - -export async function hasGroupMembership(groupSlug: string) { - const session = await auth(); - const userId = session?.user.id; - if (!userId) { - return false; - } - - const group = await prisma.group.findFirstOrThrow({ - select: { id: true }, - where: { - asOwner: { slug: groupSlug }, - memberships: { - some: { userId }, - }, - }, - }); - return !!group; -} diff --git a/packages/hub/src/server/groups/data.ts b/packages/hub/src/server/groups/data.ts new file mode 100644 index 0000000000..4839e9685f --- /dev/null +++ b/packages/hub/src/server/groups/data.ts @@ -0,0 +1,97 @@ +import "server-only"; + +import { Prisma } from "@prisma/client"; + +import { auth } from "@/auth"; +import { prisma } from "@/prisma"; + +import { Paginated } from "../models/data"; + +export async function hasGroupMembership(groupSlug: string) { + const session = await auth(); + const userId = session?.user.id; + if (!userId) { + return false; + } + + const group = await prisma.group.findFirstOrThrow({ + select: { id: true }, + where: { + asOwner: { slug: groupSlug }, + memberships: { + some: { userId }, + }, + }, + }); + return !!group; +} + +const groupCardSelect = { + id: true, + asOwner: { + select: { + slug: true, + }, + }, + updatedAt: true, +} satisfies Prisma.GroupSelect; + +type DbGroupCard = NonNullable< + Awaited< + ReturnType< + typeof prisma.group.findFirst<{ select: typeof groupCardSelect }> + > + > +>; + +export function dbGroupToGroupCard(dbGroup: DbGroupCard) { + return { + id: dbGroup.id, + slug: dbGroup.asOwner.slug, + updatedAt: dbGroup.updatedAt, + }; +} + +export type GroupCardData = ReturnType; + +export async function loadGroupCards( + params: { + username?: string; + cursor?: string; + limit?: number; + } = {} +): Promise> { + const limit = params.limit ?? 20; + + const dbGroups = await prisma.group.findMany({ + select: groupCardSelect, + orderBy: { updatedAt: "desc" }, + cursor: params.cursor ? { id: params.cursor } : undefined, + where: { + memberships: { + some: { + user: { + asOwner: { + slug: params.username, + }, + }, + }, + }, + }, + take: limit + 1, + }); + + const groups = dbGroups.map(dbGroupToGroupCard); + + const nextCursor = groups[groups.length - 1]?.id; + + async function loadMore(limit: number) { + "use server"; + return loadGroupCards({ ...params, cursor: nextCursor, limit }); + } + + return { + items: groups.slice(0, limit), + loadMore: groups.length > limit ? loadMore : undefined, + }; +} From 3ece67738eb1f19c591c6667c54c3943eddbfea5 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Wed, 27 Nov 2024 15:49:29 -0300 Subject: [PATCH 16/68] convert /definitions and /users/[username]/definitions --- .../definitions/DefinitionsPage.tsx | 24 ------ .../definitions/FrontPageDefinitionList.tsx | 49 ------------ .../src/app/(frontpage)/definitions/page.tsx | 15 ++-- .../definitions/UserDefinitionList.tsx | 49 ------------ .../definitions/UserDefinitionsPage.tsx | 32 -------- .../app/users/[username]/definitions/page.tsx | 15 ++-- .../RelativeValuesDefinitionCard.tsx | 23 +----- .../RelativeValuesDefinitionList.tsx | 40 ++++------ .../hub/src/server/relative-values/data.ts | 78 +++++++++++++++++++ 9 files changed, 106 insertions(+), 219 deletions(-) delete mode 100644 packages/hub/src/app/(frontpage)/definitions/DefinitionsPage.tsx delete mode 100644 packages/hub/src/app/(frontpage)/definitions/FrontPageDefinitionList.tsx delete mode 100644 packages/hub/src/app/users/[username]/definitions/UserDefinitionList.tsx delete mode 100644 packages/hub/src/app/users/[username]/definitions/UserDefinitionsPage.tsx create mode 100644 packages/hub/src/server/relative-values/data.ts diff --git a/packages/hub/src/app/(frontpage)/definitions/DefinitionsPage.tsx b/packages/hub/src/app/(frontpage)/definitions/DefinitionsPage.tsx deleted file mode 100644 index d59a37e234..0000000000 --- a/packages/hub/src/app/(frontpage)/definitions/DefinitionsPage.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client"; -import { FC } from "react"; -import { graphql } from "relay-runtime"; - -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; - -import { FrontPageDefinitionList } from "./FrontPageDefinitionList"; - -import { DefinitionsPageQuery } from "@/__generated__/DefinitionsPageQuery.graphql"; - -const Query = graphql` - query DefinitionsPageQuery { - ...FrontPageDefinitionList - } -`; - -export const DefinitionsPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [data] = usePageQuery(Query, query); - - return ; -}; diff --git a/packages/hub/src/app/(frontpage)/definitions/FrontPageDefinitionList.tsx b/packages/hub/src/app/(frontpage)/definitions/FrontPageDefinitionList.tsx deleted file mode 100644 index 680e186fd1..0000000000 --- a/packages/hub/src/app/(frontpage)/definitions/FrontPageDefinitionList.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"use client"; - -import { FC } from "react"; -import { graphql, usePaginationFragment } from "react-relay"; - -import { RelativeValuesDefinitionList } from "@/relative-values/components/RelativeValuesDefinitionList"; - -import { FrontPageDefinitionList$key } from "@/__generated__/FrontPageDefinitionList.graphql"; -import { FrontPageDefinitionListPaginationQuery } from "@/__generated__/FrontPageDefinitionListPaginationQuery.graphql"; - -const Fragment = graphql` - fragment FrontPageDefinitionList on Query - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 20 } - ) - @refetchable(queryName: "FrontPageDefinitionListPaginationQuery") { - relativeValuesDefinitions(first: $count, after: $cursor) - @connection(key: "FrontPageDefinitionList_relativeValuesDefinitions") { - # necessary for Relay - edges { - __typename - } - ...RelativeValuesDefinitionList - } - } -`; - -type Props = { - dataRef: FrontPageDefinitionList$key; -}; - -export const FrontPageDefinitionList: FC = ({ dataRef }) => { - const { - data: { relativeValuesDefinitions }, - loadNext, - } = usePaginationFragment< - FrontPageDefinitionListPaginationQuery, - FrontPageDefinitionList$key - >(Fragment, dataRef); - - return ( - - ); -}; diff --git a/packages/hub/src/app/(frontpage)/definitions/page.tsx b/packages/hub/src/app/(frontpage)/definitions/page.tsx index f5841c782d..92df30d193 100644 --- a/packages/hub/src/app/(frontpage)/definitions/page.tsx +++ b/packages/hub/src/app/(frontpage)/definitions/page.tsx @@ -1,13 +1,8 @@ -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { RelativeValuesDefinitionList } from "@/relative-values/components/RelativeValuesDefinitionList"; +import { loadDefinitionCards } from "@/server/relative-values/data"; -import { DefinitionsPage } from "./DefinitionsPage"; +export default async function DefinitionsPage() { + const page = await loadDefinitionCards(); -import QueryNode, { - DefinitionsPageQuery, -} from "@/__generated__/DefinitionsPageQuery.graphql"; - -export default async function OuterDefinitionsPage() { - const query = await loadPageQuery(QueryNode, {}); - - return ; + return ; } diff --git a/packages/hub/src/app/users/[username]/definitions/UserDefinitionList.tsx b/packages/hub/src/app/users/[username]/definitions/UserDefinitionList.tsx deleted file mode 100644 index a72bbf894b..0000000000 --- a/packages/hub/src/app/users/[username]/definitions/UserDefinitionList.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { FC } from "react"; -import { usePaginationFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { RelativeValuesDefinitionList } from "@/relative-values/components/RelativeValuesDefinitionList"; - -import { UserDefinitionList$key } from "@/__generated__/UserDefinitionList.graphql"; - -const Fragment = graphql` - fragment UserDefinitionList on User - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 20 } - ) - @refetchable(queryName: "UserDefinitionListPaginationQuery") { - relativeValuesDefinitions(first: $count, after: $cursor) - @connection(key: "UserViewList_relativeValuesDefinitions") { - edges { - __typename - } - ...RelativeValuesDefinitionList - } - } -`; - -type Props = { - dataRef: UserDefinitionList$key; -}; - -export const UserDefinitionList: FC = ({ dataRef }) => { - const { - data: { relativeValuesDefinitions }, - loadNext, - } = usePaginationFragment(Fragment, dataRef); - - return ( -
- {relativeValuesDefinitions.edges.length ? ( - - ) : ( -
No definitions to show.
- )} -
- ); -}; diff --git a/packages/hub/src/app/users/[username]/definitions/UserDefinitionsPage.tsx b/packages/hub/src/app/users/[username]/definitions/UserDefinitionsPage.tsx deleted file mode 100644 index 2332e7acab..0000000000 --- a/packages/hub/src/app/users/[username]/definitions/UserDefinitionsPage.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; -import { FC } from "react"; -import { graphql } from "relay-runtime"; - -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; - -import { UserDefinitionList } from "./UserDefinitionList"; - -import { UserDefinitionsPageQuery } from "@/__generated__/UserDefinitionsPageQuery.graphql"; - -const Query = graphql` - query UserDefinitionsPageQuery($username: String!) { - userByUsername(username: $username) { - __typename - ... on User { - ...UserDefinitionList - } - } - } -`; - -export const UserDefinitionsPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [{ userByUsername: result }] = usePageQuery(Query, query); - - const user = extractFromGraphqlErrorUnion(result, "User"); - - return ; -}; diff --git a/packages/hub/src/app/users/[username]/definitions/page.tsx b/packages/hub/src/app/users/[username]/definitions/page.tsx index 372d767814..524b321b99 100644 --- a/packages/hub/src/app/users/[username]/definitions/page.tsx +++ b/packages/hub/src/app/users/[username]/definitions/page.tsx @@ -1,24 +1,19 @@ import { Metadata } from "next"; -import { loadPageQuery } from "@/relay/loadPageQuery"; - -import { UserDefinitionsPage } from "./UserDefinitionsPage"; - -import QueryNode, { - UserDefinitionsPageQuery, -} from "@/__generated__/UserDefinitionsPageQuery.graphql"; +import { RelativeValuesDefinitionList } from "@/relative-values/components/RelativeValuesDefinitionList"; +import { loadDefinitionCards } from "@/server/relative-values/data"; type Props = { params: Promise<{ username: string }>; }; -export default async function OuterUserDefinitionsPage({ params }: Props) { +export default async function UserDefinitionsPage({ params }: Props) { const { username } = await params; - const query = await loadPageQuery(QueryNode, { + const page = await loadDefinitionCards({ username, }); - return ; + return ; } export async function generateMetadata({ params }: Props): Promise { diff --git a/packages/hub/src/relative-values/components/RelativeValuesDefinitionCard.tsx b/packages/hub/src/relative-values/components/RelativeValuesDefinitionCard.tsx index 39fa14a795..182c66196f 100644 --- a/packages/hub/src/relative-values/components/RelativeValuesDefinitionCard.tsx +++ b/packages/hub/src/relative-values/components/RelativeValuesDefinitionCard.tsx @@ -1,33 +1,18 @@ import { FC } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; import { EntityCard, UpdatedStatus } from "@/components/EntityCard"; import { relativeValuesRoute } from "@/routes"; - -import { RelativeValuesDefinitionCard$key } from "@/__generated__/RelativeValuesDefinitionCard.graphql"; - -const Fragment = graphql` - fragment RelativeValuesDefinitionCard on RelativeValuesDefinition { - slug - updatedAtTimestamp - owner { - slug - } - } -`; +import { RelativeValuesDefinitionCardData } from "@/server/relative-values/data"; type Props = { - definitionRef: RelativeValuesDefinitionCard$key; + definition: RelativeValuesDefinitionCardData; showOwner?: boolean; }; export const RelativeValuesDefinitionCard: FC = ({ - definitionRef, + definition, showOwner = true, }) => { - const definition = useFragment(Fragment, definitionRef); - return ( = ({ slug={definition.slug} menuItems={ <> - + } /> diff --git a/packages/hub/src/relative-values/components/RelativeValuesDefinitionList.tsx b/packages/hub/src/relative-values/components/RelativeValuesDefinitionList.tsx index 6199482c6e..ac2b2a35cc 100644 --- a/packages/hub/src/relative-values/components/RelativeValuesDefinitionList.tsx +++ b/packages/hub/src/relative-values/components/RelativeValuesDefinitionList.tsx @@ -1,53 +1,41 @@ "use client"; import { FC } from "react"; -import { graphql, useFragment } from "react-relay"; import { LoadMore } from "@/components/LoadMore"; +import { usePaginator } from "@/hooks/usePaginator"; +import { Paginated } from "@/server/models/data"; +import { RelativeValuesDefinitionCardData } from "@/server/relative-values/data"; import { RelativeValuesDefinitionCard } from "./RelativeValuesDefinitionCard"; -import { RelativeValuesDefinitionList$key } from "@/__generated__/RelativeValuesDefinitionList.graphql"; - -const Fragment = graphql` - fragment RelativeValuesDefinitionList on RelativeValuesDefinitionConnection { - edges { - node { - id - ...RelativeValuesDefinitionCard - } - } - pageInfo { - hasNextPage - } - } -`; - type Props = { - connectionRef: RelativeValuesDefinitionList$key; - loadNext(count: number): unknown; + page: Paginated; showOwner?: boolean; }; export const RelativeValuesDefinitionList: FC = ({ - connectionRef, - loadNext, + page: initialPage, showOwner, }) => { - const connection = useFragment(Fragment, connectionRef); + const page = usePaginator(initialPage); + + if (!page.items.length) { + return
No definitions to show.
; + } return (
- {connection.edges.map((edge) => ( + {page.items.map((definition) => ( ))}
- {connection.pageInfo.hasNextPage && } + {page.loadNext && }
); }; diff --git a/packages/hub/src/server/relative-values/data.ts b/packages/hub/src/server/relative-values/data.ts new file mode 100644 index 0000000000..df379e2ad1 --- /dev/null +++ b/packages/hub/src/server/relative-values/data.ts @@ -0,0 +1,78 @@ +import "server-only"; + +import { Prisma } from "@prisma/client"; + +import { prisma } from "@/prisma"; + +import { Paginated } from "../models/data"; + +const definitionCardSelect = { + id: true, + slug: true, + updatedAt: true, + owner: { + select: { + slug: true, + }, + }, +} satisfies Prisma.RelativeValuesDefinitionSelect; + +type DbDefinitionCard = NonNullable< + Awaited< + ReturnType< + typeof prisma.relativeValuesDefinition.findFirst<{ + select: typeof definitionCardSelect; + }> + > + > +>; + +export function dbDefinitionToDefinitionCard(dbDefinition: DbDefinitionCard) { + return { + slug: dbDefinition.slug, + owner: { + slug: dbDefinition.owner.slug, + }, + updatedAt: dbDefinition.updatedAt, + }; +} + +export type RelativeValuesDefinitionCardData = ReturnType< + typeof dbDefinitionToDefinitionCard +>; + +export async function loadDefinitionCards( + params: { + username?: string; + cursor?: string; + limit?: number; + } = {} +): Promise> { + const limit = params.limit ?? 20; + + const dbDefinitions = await prisma.relativeValuesDefinition.findMany({ + select: definitionCardSelect, + orderBy: { updatedAt: "desc" }, + cursor: params.cursor ? { id: params.cursor } : undefined, + where: { + owner: { + slug: params.username, + }, + }, + take: limit + 1, + }); + + const definitions = dbDefinitions.map(dbDefinitionToDefinitionCard); + + const nextCursor = definitions[definitions.length - 1]?.id; + + async function loadMore(limit: number) { + "use server"; + return loadDefinitionCards({ ...params, cursor: nextCursor, limit }); + } + + return { + items: definitions.slice(0, limit), + loadMore: definitions.length > limit ? loadMore : undefined, + }; +} From e9922774e0761572134ebfa0cda15249e3746a60 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Wed, 27 Nov 2024 16:43:28 -0300 Subject: [PATCH 17/68] type fix --- .../relative-values/components/RelativeValuesDefinitionList.tsx | 2 +- packages/hub/src/server/relative-values/data.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/hub/src/relative-values/components/RelativeValuesDefinitionList.tsx b/packages/hub/src/relative-values/components/RelativeValuesDefinitionList.tsx index ac2b2a35cc..7e1f5175d4 100644 --- a/packages/hub/src/relative-values/components/RelativeValuesDefinitionList.tsx +++ b/packages/hub/src/relative-values/components/RelativeValuesDefinitionList.tsx @@ -29,7 +29,7 @@ export const RelativeValuesDefinitionList: FC = ({
{page.items.map((definition) => ( diff --git a/packages/hub/src/server/relative-values/data.ts b/packages/hub/src/server/relative-values/data.ts index df379e2ad1..0e250a47f2 100644 --- a/packages/hub/src/server/relative-values/data.ts +++ b/packages/hub/src/server/relative-values/data.ts @@ -29,6 +29,7 @@ type DbDefinitionCard = NonNullable< export function dbDefinitionToDefinitionCard(dbDefinition: DbDefinitionCard) { return { + id: dbDefinition.id, slug: dbDefinition.slug, owner: { slug: dbDefinition.owner.slug, From 752c4b0268a3c12306a2c50a9aff3336d5dcac3b Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Wed, 27 Nov 2024 17:11:34 -0300 Subject: [PATCH 18/68] rewrite /variables and /user/[username]/variables with RSC --- .../variables/FrontPageVariableList.tsx | 42 -------- .../(frontpage)/variables/VariablesPage.tsx | 24 ----- .../src/app/(frontpage)/variables/page.tsx | 13 +-- .../[username]/variables/UserVariableList.tsx | 45 -------- .../variables/UserVariablesPage.tsx | 32 ------ .../app/users/[username]/variables/page.tsx | 15 +-- packages/hub/src/server/models/data.ts | 2 +- .../hub/src/server/relative-values/data.ts | 12 ++- packages/hub/src/server/variables/data.ts | 102 ++++++++++++++++++ .../src/variables/components/VariableCard.tsx | 44 ++------ .../src/variables/components/VariableList.tsx | 37 +++---- 11 files changed, 138 insertions(+), 230 deletions(-) delete mode 100644 packages/hub/src/app/(frontpage)/variables/FrontPageVariableList.tsx delete mode 100644 packages/hub/src/app/(frontpage)/variables/VariablesPage.tsx delete mode 100644 packages/hub/src/app/users/[username]/variables/UserVariableList.tsx delete mode 100644 packages/hub/src/app/users/[username]/variables/UserVariablesPage.tsx create mode 100644 packages/hub/src/server/variables/data.ts diff --git a/packages/hub/src/app/(frontpage)/variables/FrontPageVariableList.tsx b/packages/hub/src/app/(frontpage)/variables/FrontPageVariableList.tsx deleted file mode 100644 index d31749e805..0000000000 --- a/packages/hub/src/app/(frontpage)/variables/FrontPageVariableList.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client"; - -import { FC } from "react"; -import { graphql, usePaginationFragment } from "react-relay"; - -import { VariableList } from "@/variables/components/VariableList"; - -import { FrontPageVariableList$key } from "@/__generated__/FrontPageVariableList.graphql"; -import { FrontPageVariableListPaginationQuery } from "@/__generated__/FrontPageVariableListPaginationQuery.graphql"; - -const Fragment = graphql` - fragment FrontPageVariableList on Query - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 20 } - ) - @refetchable(queryName: "FrontPageVariableListPaginationQuery") { - variables(first: $count, after: $cursor) - @connection(key: "FrontPageVariableList_variables") { - edges { - __typename - } - ...VariableList - } - } -`; - -type Props = { - dataRef: FrontPageVariableList$key; -}; - -export const FrontPageVariableList: FC = ({ dataRef }) => { - const { - data: { variables }, - loadNext, - } = usePaginationFragment< - FrontPageVariableListPaginationQuery, - FrontPageVariableList$key - >(Fragment, dataRef); - - return ; -}; diff --git a/packages/hub/src/app/(frontpage)/variables/VariablesPage.tsx b/packages/hub/src/app/(frontpage)/variables/VariablesPage.tsx deleted file mode 100644 index d27070c0e9..0000000000 --- a/packages/hub/src/app/(frontpage)/variables/VariablesPage.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client"; -import { FC } from "react"; -import { graphql } from "relay-runtime"; - -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; - -import { FrontPageVariableList } from "./FrontPageVariableList"; - -import { VariablesPageQuery } from "@/__generated__/VariablesPageQuery.graphql"; - -const Query = graphql` - query VariablesPageQuery { - ...FrontPageVariableList - } -`; - -export const VariablesPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [data] = usePageQuery(Query, query); - - return ; -}; diff --git a/packages/hub/src/app/(frontpage)/variables/page.tsx b/packages/hub/src/app/(frontpage)/variables/page.tsx index 581bbc3bfe..a2908df253 100644 --- a/packages/hub/src/app/(frontpage)/variables/page.tsx +++ b/packages/hub/src/app/(frontpage)/variables/page.tsx @@ -1,13 +1,8 @@ -import { loadPageQuery } from "@/relay/loadPageQuery"; - -import { VariablesPage } from "./VariablesPage"; - -import QueryNode, { - VariablesPageQuery, -} from "@/__generated__/VariablesPageQuery.graphql"; +import { loadVariableCards } from "@/server/variables/data"; +import { VariableList } from "@/variables/components/VariableList"; export default async function OuterVariablesPage() { - const query = await loadPageQuery(QueryNode, {}); + const variables = await loadVariableCards(); - return ; + return ; } diff --git a/packages/hub/src/app/users/[username]/variables/UserVariableList.tsx b/packages/hub/src/app/users/[username]/variables/UserVariableList.tsx deleted file mode 100644 index 07f622ae1f..0000000000 --- a/packages/hub/src/app/users/[username]/variables/UserVariableList.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { FC } from "react"; -import { usePaginationFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { VariableList } from "@/variables/components/VariableList"; - -import { UserVariableList$key } from "@/__generated__/UserVariableList.graphql"; - -const Fragment = graphql` - fragment UserVariableList on User - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 20 } - ) - @refetchable(queryName: "UserVariableListPaginationQuery") { - variables(first: $count, after: $cursor) - @connection(key: "UserVariableList_variables") { - edges { - __typename - } - ...VariableList - } - } -`; - -type Props = { - dataRef: UserVariableList$key; -}; - -export const UserVariableList: FC = ({ dataRef }) => { - const { - data: { variables }, - loadNext, - } = usePaginationFragment(Fragment, dataRef); - - return ( -
- {variables.edges.length ? ( - - ) : ( -
No modelExport to show.
- )} -
- ); -}; diff --git a/packages/hub/src/app/users/[username]/variables/UserVariablesPage.tsx b/packages/hub/src/app/users/[username]/variables/UserVariablesPage.tsx deleted file mode 100644 index 4a04fb386b..0000000000 --- a/packages/hub/src/app/users/[username]/variables/UserVariablesPage.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; -import { FC } from "react"; -import { graphql } from "relay-runtime"; - -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; - -import { UserVariableList } from "./UserVariableList"; - -import { UserVariablesPageQuery } from "@/__generated__/UserVariablesPageQuery.graphql"; - -const Query = graphql` - query UserVariablesPageQuery($username: String!) { - userByUsername(username: $username) { - __typename - ... on User { - ...UserVariableList - } - } - } -`; - -export const UserVariablesPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [{ userByUsername: result }] = usePageQuery(Query, query); - - const user = extractFromGraphqlErrorUnion(result, "User"); - - return ; -}; diff --git a/packages/hub/src/app/users/[username]/variables/page.tsx b/packages/hub/src/app/users/[username]/variables/page.tsx index 005bd8c015..413a75b965 100644 --- a/packages/hub/src/app/users/[username]/variables/page.tsx +++ b/packages/hub/src/app/users/[username]/variables/page.tsx @@ -1,12 +1,7 @@ import { Metadata } from "next"; -import { loadPageQuery } from "@/relay/loadPageQuery"; - -import { UserVariablesPage } from "./UserVariablesPage"; - -import QueryNode, { - UserVariablesPageQuery, -} from "@/__generated__/UserVariablesPageQuery.graphql"; +import { loadVariableCards } from "@/server/variables/data"; +import { VariableList } from "@/variables/components/VariableList"; type Props = { params: Promise<{ username: string }>; @@ -14,11 +9,9 @@ type Props = { export default async function OuterUserVariablesPage({ params }: Props) { const { username } = await params; - const query = await loadPageQuery(QueryNode, { - username, - }); + const variables = await loadVariableCards({ ownerSlug: username }); - return ; + return ; } export async function generateMetadata({ params }: Props): Promise { diff --git a/packages/hub/src/server/models/data.ts b/packages/hub/src/server/models/data.ts index 6f10932b17..7d99c87347 100644 --- a/packages/hub/src/server/models/data.ts +++ b/packages/hub/src/server/models/data.ts @@ -6,7 +6,7 @@ import { auth } from "@/auth"; import { prisma } from "@/prisma"; // duplicates code in graphql/helpers/modelHelpers.ts -async function modelWhereHasAccess(): Promise { +export async function modelWhereHasAccess(): Promise { const session = await auth(); const orParts: Prisma.ModelWhereInput[] = [{ isPrivate: false }]; if (session) { diff --git a/packages/hub/src/server/relative-values/data.ts b/packages/hub/src/server/relative-values/data.ts index 0e250a47f2..6a66e6429f 100644 --- a/packages/hub/src/server/relative-values/data.ts +++ b/packages/hub/src/server/relative-values/data.ts @@ -55,11 +55,13 @@ export async function loadDefinitionCards( select: definitionCardSelect, orderBy: { updatedAt: "desc" }, cursor: params.cursor ? { id: params.cursor } : undefined, - where: { - owner: { - slug: params.username, - }, - }, + where: params.username + ? { + owner: { + slug: params.username, + }, + } + : undefined, take: limit + 1, }); diff --git a/packages/hub/src/server/variables/data.ts b/packages/hub/src/server/variables/data.ts new file mode 100644 index 0000000000..595b7e6d76 --- /dev/null +++ b/packages/hub/src/server/variables/data.ts @@ -0,0 +1,102 @@ +import "server-only"; + +import { Prisma } from "@prisma/client"; + +import { prisma } from "@/prisma"; + +import { modelWhereHasAccess, Paginated } from "../models/data"; + +const variableCardSelect = { + id: true, + variableName: true, + currentRevision: { + select: { + id: true, + title: true, + docstring: true, + variableType: true, + modelRevision: { + select: { + createdAt: true, + }, + }, + }, + }, + model: { + select: { + owner: { + select: { + slug: true, + }, + }, + slug: true, + isPrivate: true, + }, + }, +} satisfies Prisma.VariableSelect; + +type DbVariableCard = NonNullable< + Awaited< + ReturnType< + typeof prisma.variable.findFirst<{ + select: typeof variableCardSelect; + }> + > + > +>; + +export function dbVariableToVariableCard(dbVariable: DbVariableCard) { + // TODO - upgrade owner, at least + return dbVariable; +} + +export type VariableCardData = ReturnType; + +export async function loadVariableCards( + params: { + ownerSlug?: string; + cursor?: string; + limit?: number; + } = {} +): Promise> { + const limit = params.limit ?? 20; + + const dbVariables = await prisma.variable.findMany({ + select: variableCardSelect, + orderBy: { + currentRevision: { + modelRevision: { + createdAt: "desc", + }, + }, + }, + cursor: params.cursor ? { id: params.cursor } : undefined, + where: { + model: { + OR: await modelWhereHasAccess(), + ...(params.ownerSlug + ? { + owner: { + slug: params.ownerSlug, + }, + } + : undefined), + }, + }, + take: limit + 1, + }); + + const variables = dbVariables.map(dbVariableToVariableCard); + + const nextCursor = variables[variables.length - 1]?.id; + + async function loadMore(limit: number) { + "use server"; + return loadVariableCards({ ...params, cursor: nextCursor, limit }); + } + + return { + items: variables.slice(0, limit), + loadMore: variables.length > limit ? loadMore : undefined, + }; +} diff --git a/packages/hub/src/variables/components/VariableCard.tsx b/packages/hub/src/variables/components/VariableCard.tsx index e5b592db33..6154c918cc 100644 --- a/packages/hub/src/variables/components/VariableCard.tsx +++ b/packages/hub/src/variables/components/VariableCard.tsx @@ -1,6 +1,4 @@ import { FC } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; import { MarkdownViewer } from "@quri/squiggle-components"; import { CodeBracketSquareIcon } from "@quri/ui"; @@ -16,39 +14,13 @@ import { import { Link } from "@/components/ui/Link"; import { exportTypeIcon } from "@/lib/typeIcon"; import { modelRoute, variableRoute } from "@/routes"; - -import { VariableCard$key } from "@/__generated__/VariableCard.graphql"; - -const Fragment = graphql` - fragment VariableCard on Variable { - id - variableName - currentRevision { - id - title - docstring - variableType - modelRevision { - createdAtTimestamp - } - } - owner { - slug - } - model { - slug - isPrivate - } - } -`; +import { VariableCardData } from "@/server/variables/data"; type Props = { - variableRef: VariableCard$key; + variable: VariableCardData; }; -export const VariableCard: FC = ({ variableRef }) => { - const variable = useFragment(Fragment, variableRef); - +export const VariableCard: FC = ({ variable }) => { const currentRevision = variable.currentRevision; if (!currentRevision) { @@ -59,7 +31,7 @@ export const VariableCard: FC = ({ variableRef }) => { // This will have problems with markdown tags, but I looked into markdown-truncation packages, and they can get complicated. Will try this for now. - const { createdAtTimestamp } = currentRevision.modelRevision; + const { createdAt } = currentRevision.modelRevision; return (
@@ -70,7 +42,7 @@ export const VariableCard: FC = ({ variableRef }) => { href={variableRoute({ modelSlug: variable.model.slug, variableName: variable.variableName, - owner: variable.owner.slug, + owner: variable.model.owner.slug, })} > {variable.currentRevision?.title || variable.variableName} @@ -80,12 +52,12 @@ export const VariableCard: FC = ({ variableRef }) => { - {`${variable.owner.slug}/${variable.model.slug}`} + {`${variable.model.owner.slug}/${variable.model.slug}`}
@@ -95,7 +67,7 @@ export const VariableCard: FC = ({ variableRef }) => { {currentRevision.variableType} , - , + , variable.model.isPrivate && , ]} /> diff --git a/packages/hub/src/variables/components/VariableList.tsx b/packages/hub/src/variables/components/VariableList.tsx index 00a7c31168..cd23da3428 100644 --- a/packages/hub/src/variables/components/VariableList.tsx +++ b/packages/hub/src/variables/components/VariableList.tsx @@ -1,43 +1,30 @@ "use client"; import { FC } from "react"; -import { graphql, useFragment } from "react-relay"; import { LoadMore } from "@/components/LoadMore"; +import { usePaginator } from "@/hooks/usePaginator"; +import { Paginated } from "@/server/models/data"; +import { VariableCardData } from "@/server/variables/data"; import { VariableCard } from "./VariableCard"; -import { VariableList$key } from "@/__generated__/VariableList.graphql"; - -const Fragment = graphql` - fragment VariableList on VariableConnection { - edges { - node { - id - ...VariableCard - } - } - pageInfo { - hasNextPage - } - } -`; - type Props = { - connectionRef: VariableList$key; - loadNext(count: number): unknown; + page: Paginated; }; -export const VariableList: FC = ({ connectionRef, loadNext }) => { - const connection = useFragment(Fragment, connectionRef); +export const VariableList: FC = ({ page: initialPage }) => { + const page = usePaginator(initialPage); - return ( + return page.items.length === 0 ? ( +
No variables found.
+ ) : (
- {connection.edges.map((edge) => ( - + {page.items.map((variable) => ( + ))}
- {connection.pageInfo.hasNextPage && } + {page.loadNext && }
); }; From 34154c4aea18a8f342338dd20bc50b94f28800b2 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Wed, 27 Nov 2024 21:44:31 -0300 Subject: [PATCH 19/68] refactor choose-username flow, reimplement with RSC --- packages/hub/src/app/new/model/page.tsx | 2 +- .../choose-username/ChooseUsername.tsx | 53 ++++++------------- .../src/app/settings/choose-username/page.tsx | 11 ++-- packages/hub/src/auth.ts | 8 ++- .../components/layout/RootLayout/PageMenu.tsx | 11 +++- .../RootLayout/useForceChooseUsername.ts | 19 ++++--- .../hub/src/graphql/mutations/setUsername.ts | 47 ---------------- packages/hub/src/graphql/schema.ts | 1 - packages/hub/src/server/ai/analytics/index.ts | 2 +- packages/hub/src/server/ai/data.ts | 2 +- packages/hub/src/server/users/actions.ts | 53 +++++++++++++++++++ .../server/{userHelpers.ts => users/auth.ts} | 2 +- packages/hub/src/server/utils.ts | 5 ++ 13 files changed, 114 insertions(+), 102 deletions(-) delete mode 100644 packages/hub/src/graphql/mutations/setUsername.ts create mode 100644 packages/hub/src/server/users/actions.ts rename packages/hub/src/server/{userHelpers.ts => users/auth.ts} (96%) create mode 100644 packages/hub/src/server/utils.ts diff --git a/packages/hub/src/app/new/model/page.tsx b/packages/hub/src/app/new/model/page.tsx index 616c86b872..5d92f2d7e5 100644 --- a/packages/hub/src/app/new/model/page.tsx +++ b/packages/hub/src/app/new/model/page.tsx @@ -5,7 +5,7 @@ import { z } from "zod"; import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; import { SelectGroupOption } from "@/components/SelectGroup"; import { getCurrentEnvironment } from "@/relay/environment"; -import { getSessionUserOrRedirect } from "@/server/userHelpers"; +import { getSessionUserOrRedirect } from "@/server/users/auth"; import { NewModel } from "./NewModel"; diff --git a/packages/hub/src/app/settings/choose-username/ChooseUsername.tsx b/packages/hub/src/app/settings/choose-username/ChooseUsername.tsx index 4634cb1da7..82168dc8da 100644 --- a/packages/hub/src/app/settings/choose-username/ChooseUsername.tsx +++ b/packages/hub/src/app/settings/choose-username/ChooseUsername.tsx @@ -1,56 +1,35 @@ "use client"; -import { ChooseUsernameMutation } from "@gen/ChooseUsernameMutation.graphql"; -import { Session } from "next-auth"; import { useRouter } from "next/navigation"; import { FC } from "react"; -import { FormProvider } from "react-hook-form"; -import { graphql } from "relay-runtime"; +import { FormProvider, useForm } from "react-hook-form"; import { Button } from "@quri/ui"; import { SlugFormField } from "@/components/ui/SlugFormField"; -import { useMutationForm } from "@/hooks/useMutationForm"; +import { setUsername } from "@/server/users/actions"; -export const ChooseUsername: FC<{ session: Session | null }> = ({ - session, -}) => { +export const ChooseUsername: FC = () => { const router = useRouter(); - if (session?.user.username) { - router.replace("/"); - } type FormShape = { username: string; }; - const { form, onSubmit, inFlight } = useMutationForm< - FormShape, - ChooseUsernameMutation, - "Me" - >({ - mode: "onChange", - mutation: graphql` - mutation ChooseUsernameMutation($username: String!) { - result: setUsername(username: $username) { - __typename - ... on BaseError { - message - } - ... on Me { - email - } - } - } - `, - expectedTypename: "Me", - formDataToVariables: (data) => ({ username: data.username }), - onCompleted: () => { - router.refresh(); - }, - blockOnSuccess: true, + const form = useForm(); + + const onSubmit = form.handleSubmit(async (data) => { + const result = await setUsername(data); + if (result.ok) { + router.replace("/"); + } else { + form.setError("username", { message: result.error }); + } }); - const disabled = inFlight || !form.formState.isValid; + const disabled = + form.formState.isSubmitting || + form.formState.isSubmitSuccessful || + !form.formState.isValid; return (
diff --git a/packages/hub/src/app/settings/choose-username/page.tsx b/packages/hub/src/app/settings/choose-username/page.tsx index cce33da869..b00d29ae4b 100644 --- a/packages/hub/src/app/settings/choose-username/page.tsx +++ b/packages/hub/src/app/settings/choose-username/page.tsx @@ -1,12 +1,17 @@ import { Metadata } from "next"; +import { redirect } from "next/navigation"; -import { auth } from "@/auth"; +import { getSessionUserOrRedirect } from "@/server/users/auth"; import { ChooseUsername } from "./ChooseUsername"; export default async function OuterChooseUsernamePage() { - const session = await auth(); - return ; + const sessionUser = await getSessionUserOrRedirect(); + if (sessionUser.username) { + redirect("/"); + } + + return ; } export const metadata: Metadata = { diff --git a/packages/hub/src/auth.ts b/packages/hub/src/auth.ts index 13c85ae3e6..31d04ee46d 100644 --- a/packages/hub/src/auth.ts +++ b/packages/hub/src/auth.ts @@ -5,6 +5,7 @@ import NextAuth, { NextAuthConfig } from "next-auth"; import EmailProvider from "next-auth/providers/email"; import GithubProvider from "next-auth/providers/github"; import { Provider } from "next-auth/providers/index"; +import { cache } from "react"; import { indexUserId } from "@/graphql/helpers/searchHelpers"; import { prisma } from "@/prisma"; @@ -62,4 +63,9 @@ function buildAuthConfig(): NextAuthConfig { return config; } -export const { auth, handlers, signIn, signOut } = NextAuth(buildAuthConfig()); +const nextAuth = NextAuth(buildAuthConfig()); +export const { handlers, signIn, signOut } = nextAuth; + +// current next-auth v5 beta doesn't cache the session, unsure if intentionally +// note: this is React builtin cache, so it's per-request +export const auth = cache(nextAuth.auth); diff --git a/packages/hub/src/components/layout/RootLayout/PageMenu.tsx b/packages/hub/src/components/layout/RootLayout/PageMenu.tsx index 7a70cf15bd..144f05f7c7 100644 --- a/packages/hub/src/components/layout/RootLayout/PageMenu.tsx +++ b/packages/hub/src/components/layout/RootLayout/PageMenu.tsx @@ -1,6 +1,6 @@ "use client"; import { Session } from "next-auth"; -import { signIn } from "next-auth/react"; +import { signIn, signOut } from "next-auth/react"; import { FC, useState } from "react"; import { useFragment, useLazyLoadQuery } from "react-relay"; import { graphql } from "relay-runtime"; @@ -8,6 +8,7 @@ import { graphql } from "relay-runtime"; import { BoltIcon, BookOpenIcon, + Button, DotsHorizontalIcon, Dropdown, DropdownMenu, @@ -165,7 +166,7 @@ export const PageMenu: FC<{ session: Session | null }> = ({ session }) => { // TODO - if redirecting, return a custom menu; right now we render the // confused version where "New Model" button is visible, but "Sign In" button // is visible too - useForceChooseUsername(session); + const { shouldChoose } = useForceChooseUsername(session); const queryRef = useLazyLoadQuery( graphql` @@ -176,6 +177,12 @@ export const PageMenu: FC<{ session: Session | null }> = ({ session }) => { { signedIn: !!session } ); + if (shouldChoose) { + return ( + + ); + } + return ( <>
diff --git a/packages/hub/src/components/layout/RootLayout/useForceChooseUsername.ts b/packages/hub/src/components/layout/RootLayout/useForceChooseUsername.ts index 3f9f7c47f3..cc0d4d5df1 100644 --- a/packages/hub/src/components/layout/RootLayout/useForceChooseUsername.ts +++ b/packages/hub/src/components/layout/RootLayout/useForceChooseUsername.ts @@ -1,5 +1,6 @@ import { Session } from "next-auth"; import { usePathname, useRouter } from "next/navigation"; +import { useEffect } from "react"; import { chooseUsernameRoute } from "@/routes"; @@ -7,11 +8,15 @@ export function useForceChooseUsername(session: Session | null) { const pathname = usePathname(); const router = useRouter(); - if ( - session?.user && - !session?.user.username && - !pathname.includes(chooseUsernameRoute()) - ) { - router.push(chooseUsernameRoute()); - } + const shouldChoose = session?.user && !session.user.username; + const shouldRedirect = + shouldChoose && !pathname.includes(chooseUsernameRoute()); + + useEffect(() => { + if (shouldRedirect) { + router.push(chooseUsernameRoute()); + } + }, [shouldRedirect]); + + return { shouldRedirect, shouldChoose }; } diff --git a/packages/hub/src/graphql/mutations/setUsername.ts b/packages/hub/src/graphql/mutations/setUsername.ts deleted file mode 100644 index 4a1ab0ad0d..0000000000 --- a/packages/hub/src/graphql/mutations/setUsername.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { ZodError } from "zod"; - -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { Me } from "../types/Me"; -import { validateSlug } from "../utils"; - -builder.mutationField("setUsername", (t) => - t.withAuth({ signedIn: true }).field({ - type: Me, - args: { - username: t.arg.string({ required: true, validate: validateSlug }), - }, - errors: { types: [ZodError] }, - async resolve(_, args, { session }) { - if (session.user.username) { - throw new Error("Username is already set"); - } - - const existingOwner = await prisma.owner.count({ - where: { slug: args.username }, - }); - if (existingOwner) { - throw new Error(`Username ${args.username} is not available`); - } - - await prisma.user.update({ - where: { - email: session.user.email, - }, - data: { - asOwner: { - create: { slug: args.username }, - }, - }, - }); - - // I tried to call getSession() here to get a fresh session, but it didn't work; - // I suspect the reason is Next.js fetch() cache. - return { - ...session.user, - username: args.username, - }; - }, - }) -); diff --git a/packages/hub/src/graphql/schema.ts b/packages/hub/src/graphql/schema.ts index 6a9c395ef4..7faadd4984 100644 --- a/packages/hub/src/graphql/schema.ts +++ b/packages/hub/src/graphql/schema.ts @@ -34,7 +34,6 @@ import "./mutations/addUserToGroup"; import "./mutations/inviteUserToGroup"; import "./mutations/moveModel"; import "./mutations/reactToGroupInvite"; -import "./mutations/setUsername"; import "./mutations/updateGroupInviteRole"; import "./mutations/updateMembershipRole"; import "./mutations/updateModelPrivacy"; diff --git a/packages/hub/src/server/ai/analytics/index.ts b/packages/hub/src/server/ai/analytics/index.ts index eb5ab480a2..756295c346 100644 --- a/packages/hub/src/server/ai/analytics/index.ts +++ b/packages/hub/src/server/ai/analytics/index.ts @@ -7,7 +7,7 @@ import { CodeArtifact, Workflow } from "@quri/squiggle-ai/server"; import { prisma } from "@/prisma"; import { getAiCodec } from "@/server/ai/utils"; import { v2WorkflowDataSchema } from "@/server/ai/v2_0"; -import { checkRootUser } from "@/server/userHelpers"; +import { checkRootUser } from "@/server/users/auth"; async function loadWorkflows() { await checkRootUser(); diff --git a/packages/hub/src/server/ai/data.ts b/packages/hub/src/server/ai/data.ts index b5c83172eb..21b227ee50 100644 --- a/packages/hub/src/server/ai/data.ts +++ b/packages/hub/src/server/ai/data.ts @@ -2,7 +2,7 @@ import "server-only"; import { prisma } from "@/prisma"; -import { getSessionUserOrRedirect } from "../userHelpers"; +import { getSessionUserOrRedirect } from "../users/auth"; import { decodeDbWorkflowToClientWorkflow } from "./storage"; export async function loadWorkflows({ diff --git a/packages/hub/src/server/users/actions.ts b/packages/hub/src/server/users/actions.ts new file mode 100644 index 0000000000..8ddf977ead --- /dev/null +++ b/packages/hub/src/server/users/actions.ts @@ -0,0 +1,53 @@ +"use server"; + +import { z } from "zod"; + +import { auth } from "@/auth"; +import { prisma } from "@/prisma"; + +const schema = z.object({ + username: z.string().min(1), +}); + +type SetUsernameResult = + | { + ok: true; + } + | { + ok: false; + error: string; + }; + +export async function setUsername( + formData: unknown +): Promise { + const session = await auth(); + if (!session?.user.email) { + throw new Error("Not signed in"); + } + if (session.user.username) { + return { ok: false, error: "Username is already set" }; + } + + const args = schema.parse(formData); + + const existingOwner = await prisma.owner.count({ + where: { slug: args.username }, + }); + if (existingOwner) { + return { ok: false, error: `Username ${args.username} is not available` }; + } + + await prisma.user.update({ + where: { + email: session.user.email, + }, + data: { + asOwner: { + create: { slug: args.username }, + }, + }, + }); + + return { ok: true }; +} diff --git a/packages/hub/src/server/userHelpers.ts b/packages/hub/src/server/users/auth.ts similarity index 96% rename from packages/hub/src/server/userHelpers.ts rename to packages/hub/src/server/users/auth.ts index 812ee52334..08e17d1e9a 100644 --- a/packages/hub/src/server/userHelpers.ts +++ b/packages/hub/src/server/users/auth.ts @@ -10,7 +10,7 @@ import { redirect } from "next/navigation"; import { isRootEmail, isSignedIn } from "@/graphql/helpers/userHelpers"; import { prisma } from "@/prisma"; -import { auth } from "../auth"; +import { auth } from "../../auth"; export async function getSessionUserOrRedirect() { const session = await auth(); diff --git a/packages/hub/src/server/utils.ts b/packages/hub/src/server/utils.ts new file mode 100644 index 0000000000..dd89e69815 --- /dev/null +++ b/packages/hub/src/server/utils.ts @@ -0,0 +1,5 @@ +import { z } from "zod"; + +export const zSlug = z.string().regex(/^\w[\w\-]*$/, { + message: "Must be alphanumerical", +}); From 82c498fa6112b4f1d867d36a93dca6004abbaaa7 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Wed, 27 Nov 2024 21:53:10 -0300 Subject: [PATCH 20/68] avoid graphql in root layout --- .../layout/RootLayout/MyGroupsMenu.tsx | 36 +++++----------- .../components/layout/RootLayout/PageMenu.tsx | 42 +++++-------------- .../components/layout/RootLayout/index.tsx | 7 +++- packages/hub/src/server/groups/data.ts | 2 +- 4 files changed, 27 insertions(+), 60 deletions(-) diff --git a/packages/hub/src/components/layout/RootLayout/MyGroupsMenu.tsx b/packages/hub/src/components/layout/RootLayout/MyGroupsMenu.tsx index 938caae839..e5e6e2e499 100644 --- a/packages/hub/src/components/layout/RootLayout/MyGroupsMenu.tsx +++ b/packages/hub/src/components/layout/RootLayout/MyGroupsMenu.tsx @@ -1,46 +1,30 @@ import { FC } from "react"; -import { graphql, useFragment } from "react-relay"; import { DropdownMenuHeader, GroupIcon, PlusIcon } from "@quri/ui"; import { DropdownMenuNextLinkItem } from "@/components/ui/DropdownMenuNextLinkItem"; import { groupRoute, newGroupRoute } from "@/routes"; - -import { MyGroupsMenu$key } from "@/__generated__/MyGroupsMenu.graphql"; +import { GroupCardData } from "@/server/groups/data"; +import { Paginated } from "@/server/models/data"; type Props = { - groupsRef: MyGroupsMenu$key; + groups: Paginated; close: () => void; }; -export const MyGroupsMenu: FC = ({ groupsRef, close }) => { - const groups = useFragment( - graphql` - fragment MyGroupsMenu on Query { - result: groups(input: { myOnly: true }) { - edges { - node { - id - slug - } - } - } - } - `, - groupsRef - ); +export const MyGroupsMenu: FC = ({ groups, close }) => { return ( <> My Groups - {groups.result.edges.length ? ( + {groups.items.length ? ( <> - {groups.result.edges.map((edge) => ( + {groups.items.map((group) => ( ))} @@ -52,7 +36,7 @@ export const MyGroupsMenu: FC = ({ groupsRef, close }) => { title="New Group" close={close} /> - {/* TODO: "...show all" link is hasNextPage is true */} + {/* TODO: "...show all" link is loadNext is true */} ); }; diff --git a/packages/hub/src/components/layout/RootLayout/PageMenu.tsx b/packages/hub/src/components/layout/RootLayout/PageMenu.tsx index 144f05f7c7..66255914cd 100644 --- a/packages/hub/src/components/layout/RootLayout/PageMenu.tsx +++ b/packages/hub/src/components/layout/RootLayout/PageMenu.tsx @@ -2,8 +2,6 @@ import { Session } from "next-auth"; import { signIn, signOut } from "next-auth/react"; import { FC, useState } from "react"; -import { useFragment, useLazyLoadQuery } from "react-relay"; -import { graphql } from "relay-runtime"; import { BoltIcon, @@ -21,6 +19,8 @@ import { import { SQUIGGLE_DOCS_URL } from "@/lib/common"; import { aboutRoute, aiRoute, newModelRoute } from "@/routes"; +import { GroupCardData } from "@/server/groups/data"; +import { Paginated } from "@/server/models/data"; import { GlobalSearch } from "../../GlobalSearch"; import { DesktopUserControls } from "./DesktopUserControls"; @@ -30,9 +30,6 @@ import { MenuLinkModeProps, PageMenuLink } from "./PageMenuLink"; import { useForceChooseUsername } from "./useForceChooseUsername"; import { UserControlsMenu } from "./UserControlsMenu"; -import { PageMenu$key } from "@/__generated__/PageMenu.graphql"; -import { PageMenuQuery } from "@/__generated__/PageMenuQuery.graphql"; - const AboutMenuLink: FC = (props) => { return ; }; @@ -63,20 +60,12 @@ const NewModelMenuLink: FC = (props) => { ); }; -const fragment = graphql` - fragment PageMenu on Query - @argumentDefinitions(signedIn: { type: "Boolean!" }) { - ...MyGroupsMenu @include(if: $signedIn) - } -`; - type MenuProps = { - queryRef: PageMenu$key; + groups: Paginated; session: Session | null; }; -const DesktopMenu: FC = ({ queryRef, session }) => { - const menu = useFragment(fragment, queryRef); +const DesktopMenu: FC = ({ groups, session }) => { return (
@@ -88,7 +77,7 @@ const DesktopMenu: FC = ({ queryRef, session }) => { ( - + )} > @@ -102,9 +91,7 @@ const DesktopMenu: FC = ({ queryRef, session }) => { ); }; -const MobileMenu: FC = ({ queryRef, session }) => { - const menu = useFragment(fragment, queryRef); - +const MobileMenu: FC = ({ groups, session }) => { const username = session?.user?.username; const [open, setOpen] = useState(false); @@ -136,7 +123,7 @@ const MobileMenu: FC = ({ queryRef, session }) => { {username ? ( <> - + = ({ queryRef, session }) => { ); }; -export const PageMenu: FC<{ session: Session | null }> = ({ session }) => { +export const PageMenu: FC = ({ session, groups }) => { // TODO - if redirecting, return a custom menu; right now we render the // confused version where "New Model" button is visible, but "Sign In" button // is visible too const { shouldChoose } = useForceChooseUsername(session); - const queryRef = useLazyLoadQuery( - graphql` - query PageMenuQuery($signedIn: Boolean!) { - ...PageMenu @arguments(signedIn: $signedIn) - } - `, - { signedIn: !!session } - ); - if (shouldChoose) { return ( @@ -186,10 +164,10 @@ export const PageMenu: FC<{ session: Session | null }> = ({ session }) => { return ( <>
- +
- +
); diff --git a/packages/hub/src/components/layout/RootLayout/index.tsx b/packages/hub/src/components/layout/RootLayout/index.tsx index b1487f69f6..2ad046f957 100644 --- a/packages/hub/src/components/layout/RootLayout/index.tsx +++ b/packages/hub/src/components/layout/RootLayout/index.tsx @@ -2,6 +2,7 @@ import { FC, PropsWithChildren, Suspense } from "react"; import { auth } from "@/auth"; import { Link } from "@/components/ui/Link"; +import { loadGroupCards } from "@/server/groups/data"; import { ReactRoot } from "../../ReactRoot"; import { PageFooterIfNecessary } from "./PageFooterIfNecessary"; @@ -12,8 +13,12 @@ const WrappedPageMenu: FC = async () => { // ``, sequentially. We could select all relevant session data // through GraphQL, or avoid GraphQL queries altogether. const session = await auth(); + const username = session?.user?.username; + const groups = username + ? await loadGroupCards({ username: session?.user?.username }) + : { items: [] }; - return ; + return ; }; const InnerRootLayout: FC = ({ children }) => { diff --git a/packages/hub/src/server/groups/data.ts b/packages/hub/src/server/groups/data.ts index 4839e9685f..801c27433d 100644 --- a/packages/hub/src/server/groups/data.ts +++ b/packages/hub/src/server/groups/data.ts @@ -14,7 +14,7 @@ export async function hasGroupMembership(groupSlug: string) { return false; } - const group = await prisma.group.findFirstOrThrow({ + const group = await prisma.group.findFirst({ select: { id: true }, where: { asOwner: { slug: groupSlug }, From ce38465840969e0f0f823f24ea97ad50606757ec Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Wed, 27 Nov 2024 23:23:44 -0300 Subject: [PATCH 21/68] MoveModelAction server action; ServerActionModalAction --- .../models/[owner]/[slug]/MoveModelAction.tsx | 58 +++------ .../components/ui/ServerActionModalAction.tsx | 113 ++++++++++++++++++ .../hub/src/graphql/mutations/moveModel.ts | 42 ------- packages/hub/src/graphql/schema.ts | 1 - packages/hub/src/hooks/useServerActionForm.ts | 52 ++++++++ .../loadModelCardAction.ts} | 4 +- .../server/models/actions/moveModelAction.ts | 39 ++++++ packages/hub/src/server/users/auth.ts | 8 +- packages/hub/src/server/utils.ts | 12 ++ .../src/squiggle/components/ImportTooltip.tsx | 2 +- 10 files changed, 242 insertions(+), 89 deletions(-) create mode 100644 packages/hub/src/components/ui/ServerActionModalAction.tsx delete mode 100644 packages/hub/src/graphql/mutations/moveModel.ts create mode 100644 packages/hub/src/hooks/useServerActionForm.ts rename packages/hub/src/server/models/{actions.ts => actions/loadModelCardAction.ts} (65%) create mode 100644 packages/hub/src/server/models/actions/moveModelAction.ts diff --git a/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx index 630ea7048f..580d897697 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx @@ -6,35 +6,13 @@ import { graphql } from "relay-runtime"; import { RightArrowIcon } from "@quri/ui"; import { SelectOwner, SelectOwnerOption } from "@/components/SelectOwner"; -import { MutationModalAction } from "@/components/ui/MutationModalAction"; +import { ServerActionModalAction } from "@/components/ui/ServerActionModalAction"; import { modelRoute } from "@/routes"; +import { moveModelAction } from "@/server/models/actions/moveModelAction"; import { draftUtils, modelToDraftLocator } from "./SquiggleSnippetDraftDialog"; import { MoveModelAction$key } from "@/__generated__/MoveModelAction.graphql"; -import { MoveModelActionMutation } from "@/__generated__/MoveModelActionMutation.graphql"; - -const Mutation = graphql` - mutation MoveModelActionMutation($input: MutationMoveModelInput!) { - result: moveModel(input: $input) { - __typename - ... on BaseError { - message - } - ... on MoveModelResult { - model { - id - slug - owner { - __typename - id - slug - } - } - } - } - } -`; type FormShape = { owner: SelectOwnerOption }; @@ -61,27 +39,20 @@ export const MoveModelAction: FC = ({ model: modelKey, close }) => { const router = useRouter(); return ( - - mutation={Mutation} - expectedTypename="MoveModelResult" - formDataToVariables={(data) => ({ - input: { - oldOwner: model.owner.slug, - newOwner: data.owner.slug, - slug: model.slug, - }, - })} - initialFocus="owner" + + title="Change Owner" + modalTitle={`Change owner for ${model.owner.slug}/${model.slug}`} + submitText="Save" defaultValues={{ // __typename from fragment is string, while SelectOwner requires 'User' | 'Group' union, // so we have to explicitly recast owner: model.owner as SelectOwnerOption, }} - submitText="Save" - close={close} - title="Change Owner" - icon={RightArrowIcon} - modalTitle={`Change owner for ${model.owner.slug}/${model.slug}`} + formDataToVariables={(data) => ({ + oldOwner: model.owner.slug, + newOwner: data.owner.slug, + slug: model.slug, + })} onCompleted={({ model: newModel }) => { draftUtils.rename( modelToDraftLocator(model), @@ -91,6 +62,11 @@ export const MoveModelAction: FC = ({ model: modelKey, close }) => { modelRoute({ owner: newModel.owner.slug, slug: newModel.slug }) ); }} + icon={RightArrowIcon} + action={moveModelAction} + close={close} + initialFocus="owner" + blockOnSuccess > {() => (
@@ -100,6 +76,6 @@ export const MoveModelAction: FC = ({ model: modelKey, close }) => { name="owner" label="New owner" myOnly />
)} - + ); }; diff --git a/packages/hub/src/components/ui/ServerActionModalAction.tsx b/packages/hub/src/components/ui/ServerActionModalAction.tsx new file mode 100644 index 0000000000..b29dbd414a --- /dev/null +++ b/packages/hub/src/components/ui/ServerActionModalAction.tsx @@ -0,0 +1,113 @@ +import { FC, PropsWithChildren, ReactNode } from "react"; +import { FieldPath, FieldValues } from "react-hook-form"; + +import { DropdownMenuModalActionItem, IconProps } from "@quri/ui"; + +import { FormModal } from "@/components/ui/FormModal"; +import { useServerActionForm } from "@/hooks/useServerActionForm"; + +type CommonProps< + TFormShape extends FieldValues, + ActionVariables, + ActionResult, +> = Pick< + Parameters< + typeof useServerActionForm + >[0], + | "formDataToVariables" + | "defaultValues" + | "action" + | "onCompleted" + | "blockOnSuccess" +> & { + initialFocus?: FieldPath; + submitText: string; + close: () => void; +}; + +function ServerActionFormModal< + TFormShape extends FieldValues, + const ActionVariables, + const ActionResult, +>({ + formDataToVariables, + initialFocus, + defaultValues, + submitText, + action, + onCompleted, + close, + title, + children, +}: PropsWithChildren> & { + title: string; +}): ReactNode { + const { form, onSubmit, inFlight } = useServerActionForm< + TFormShape, + ActionVariables, + ActionResult + >({ + mode: "onChange", + defaultValues, + action, + formDataToVariables, + async onCompleted(data) { + onCompleted?.(data); + close(); + }, + }); + + return ( + + {children} + + ); +} + +export function ServerActionModalAction< + TFormShape extends FieldValues, + const Action extends (input: any) => Promise, +>({ + modalTitle, + title, + icon, + children, + ...modalProps +}: CommonProps< + TFormShape, + Parameters[0], + Awaited> +> & { + modalTitle: string; + title: string; + icon?: FC; + children: () => ReactNode; +}): ReactNode { + return ( + ( + [0], + Awaited> + > + // Note that we pass the same `close` that's responsible for closing the dropdown. + {...modalProps} + title={modalTitle} + > + {children()} + + )} + /> + ); +} diff --git a/packages/hub/src/graphql/mutations/moveModel.ts b/packages/hub/src/graphql/mutations/moveModel.ts deleted file mode 100644 index 78e5d9ad9a..0000000000 --- a/packages/hub/src/graphql/mutations/moveModel.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ZodError } from "zod"; - -import { prisma } from "@/prisma"; - -import { builder } from "../builder"; -import { NotFoundError } from "../errors/NotFoundError"; -import { getWriteableModel } from "../helpers/modelHelpers"; -import { getWriteableOwnerBySlug } from "../helpers/ownerHelpers"; -import { Model } from "../types/Model"; -import { validateSlug } from "../utils"; - -builder.mutationField("moveModel", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("MoveModelResult", { - fields: (t) => ({ - model: t.field({ type: Model }), - }), - }), - errors: { types: [NotFoundError, ZodError] }, - input: { - oldOwner: t.input.string({ required: true, validate: validateSlug }), - newOwner: t.input.string({ required: true, validate: validateSlug }), - slug: t.input.string({ required: true, validate: validateSlug }), - }, - resolve: async (_, { input }, { session }) => { - let model = await getWriteableModel({ - owner: input.oldOwner, - slug: input.slug, - session, - }); - - const newOwner = await getWriteableOwnerBySlug(session, input.newOwner); - - model = await prisma.model.update({ - where: { id: model.id }, - data: { ownerId: newOwner.id }, - }); - - return { model }; - }, - }) -); diff --git a/packages/hub/src/graphql/schema.ts b/packages/hub/src/graphql/schema.ts index 7faadd4984..0ee9cb7d10 100644 --- a/packages/hub/src/graphql/schema.ts +++ b/packages/hub/src/graphql/schema.ts @@ -32,7 +32,6 @@ import "./mutations/deleteRelativeValuesDefinition"; import "./mutations/deleteReusableGroupInviteToken"; import "./mutations/addUserToGroup"; import "./mutations/inviteUserToGroup"; -import "./mutations/moveModel"; import "./mutations/reactToGroupInvite"; import "./mutations/updateGroupInviteRole"; import "./mutations/updateMembershipRole"; diff --git a/packages/hub/src/hooks/useServerActionForm.ts b/packages/hub/src/hooks/useServerActionForm.ts new file mode 100644 index 0000000000..b31fc84a25 --- /dev/null +++ b/packages/hub/src/hooks/useServerActionForm.ts @@ -0,0 +1,52 @@ +import { BaseSyntheticEvent, useCallback } from "react"; +import { FieldValues, useForm, UseFormProps } from "react-hook-form"; + +/** + * This hook ties together `useForm` and server actions. + * + * See also: + * - `` if your form is available through a Dropdown menu + * + * All generic type parameters to this function default to `never`, so you'll have to set them explicitly to pass type checks. + */ +export function useServerActionForm< + FormShape extends FieldValues = never, + ActionVariables = never, + ActionResult = never, +>({ + defaultValues, + mode, + action, + onCompleted, + formDataToVariables, + blockOnSuccess, +}: { + // This is unfortunately not strictly type-safe: if you return extra variables that are not needed for mutation, TypeScript won't complain. + // See also: https://stackoverflow.com/questions/72111571/typescript-exact-return-type-of-function + // This could be solved by converting the return type to generic, but I expect that the lack of partial type parameters in TypeScript + // would get in the way, so I won't even try. + formDataToVariables: (data: FormShape) => ActionVariables; + action: (input: ActionVariables) => Promise; + onCompleted?: (result: ActionResult) => void | Promise; + blockOnSuccess?: boolean; +} & Pick, "defaultValues" | "mode">) { + const form = useForm({ defaultValues, mode }); + + const onSubmit = useCallback( + (event?: BaseSyntheticEvent) => + form.handleSubmit(async (formData) => { + // TODO - transition? + const result = await action(formDataToVariables(formData)); + onCompleted?.(result); + })(event), + [form, formDataToVariables, onCompleted, action] + ); + + return { + form, + onSubmit, + inFlight: blockOnSuccess + ? form.formState.isSubmitting || form.formState.isSubmitSuccessful + : form.formState.isSubmitting, + }; +} diff --git a/packages/hub/src/server/models/actions.ts b/packages/hub/src/server/models/actions/loadModelCardAction.ts similarity index 65% rename from packages/hub/src/server/models/actions.ts rename to packages/hub/src/server/models/actions/loadModelCardAction.ts index 2c21e4225d..be2fdad1f2 100644 --- a/packages/hub/src/server/models/actions.ts +++ b/packages/hub/src/server/models/actions/loadModelCardAction.ts @@ -1,7 +1,7 @@ "use server"; -import { loadModelCard, ModelCardData } from "./data"; +import { loadModelCard, ModelCardData } from "../data"; -// used in ImportTooltip +// data-fetching action, used in ImportTooltip export async function loadModelCardAction({ owner, slug, diff --git a/packages/hub/src/server/models/actions/moveModelAction.ts b/packages/hub/src/server/models/actions/moveModelAction.ts new file mode 100644 index 0000000000..880a4785b1 --- /dev/null +++ b/packages/hub/src/server/models/actions/moveModelAction.ts @@ -0,0 +1,39 @@ +"use server"; + +import { z } from "zod"; + +import { getWriteableModel } from "@/graphql/helpers/modelHelpers"; +import { getWriteableOwnerBySlug } from "@/graphql/helpers/ownerHelpers"; +import { prisma } from "@/prisma"; +import { getSessionOrRedirect } from "@/server/users/auth"; +import { makeServerAction, zSlug } from "@/server/utils"; + +export const moveModelAction = makeServerAction( + z.object({ + oldOwner: zSlug, + newOwner: zSlug, + slug: zSlug, + }), + async (input) => { + const session = await getSessionOrRedirect(); + + let model = await getWriteableModel({ + owner: input.oldOwner, + slug: input.slug, + session, + }); + + const newOwner = await getWriteableOwnerBySlug(session, input.newOwner); + + const newModel = await prisma.model.update({ + where: { id: model.id }, + data: { ownerId: newOwner.id }, + select: { + slug: true, + owner: true, + }, + }); + + return { model: newModel }; + } +); diff --git a/packages/hub/src/server/users/auth.ts b/packages/hub/src/server/users/auth.ts index 08e17d1e9a..7a83e52973 100644 --- a/packages/hub/src/server/users/auth.ts +++ b/packages/hub/src/server/users/auth.ts @@ -12,13 +12,17 @@ import { prisma } from "@/prisma"; import { auth } from "../../auth"; -export async function getSessionUserOrRedirect() { +export async function getSessionOrRedirect() { const session = await auth(); if (!isSignedIn(session)) { redirect("/api/auth/signin"); // TODO - callbackUrl } - return session.user; + return session; +} + +export async function getSessionUserOrRedirect() { + return (await getSessionOrRedirect()).user; } export async function checkRootUser() { diff --git a/packages/hub/src/server/utils.ts b/packages/hub/src/server/utils.ts index dd89e69815..f7077260f3 100644 --- a/packages/hub/src/server/utils.ts +++ b/packages/hub/src/server/utils.ts @@ -3,3 +3,15 @@ import { z } from "zod"; export const zSlug = z.string().regex(/^\w[\w\-]*$/, { message: "Must be alphanumerical", }); + +export function makeServerAction( + schema: z.ZodType, + handler: (input: T) => Promise +) { + return async ( + data: T // data type is unknown but we will validate it immediately + ) => { + const input = schema.parse(data); + return handler(input); + }; +} diff --git a/packages/hub/src/squiggle/components/ImportTooltip.tsx b/packages/hub/src/squiggle/components/ImportTooltip.tsx index 4459db0ac1..97b3b85061 100644 --- a/packages/hub/src/squiggle/components/ImportTooltip.tsx +++ b/packages/hub/src/squiggle/components/ImportTooltip.tsx @@ -3,7 +3,7 @@ import { FC, useEffect, useState } from "react"; import Skeleton from "react-loading-skeleton"; import { ModelCard } from "@/models/components/ModelCard"; -import { loadModelCardAction } from "@/server/models/actions"; +import { loadModelCardAction } from "@/server/models/actions/loadModelCardAction"; import { ModelCardData } from "@/server/models/data"; import { parseSourceId } from "./linker"; From 456f3c726bd5949b938e7d255179af4c179d306e Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Wed, 27 Nov 2024 23:34:47 -0300 Subject: [PATCH 22/68] rewrite updateModelSlug --- .../[owner]/[slug]/UpdateModelSlugAction.tsx | 45 ++++--------------- .../src/graphql/mutations/updateModelSlug.ts | 40 ----------------- packages/hub/src/graphql/schema.ts | 1 - packages/hub/src/hooks/useServerActionForm.ts | 12 ++++- .../models/actions/updateModelSlugAction.ts | 36 +++++++++++++++ 5 files changed, 54 insertions(+), 80 deletions(-) delete mode 100644 packages/hub/src/graphql/mutations/updateModelSlug.ts create mode 100644 packages/hub/src/server/models/actions/updateModelSlugAction.ts diff --git a/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx index 0d7d339ee9..032d7f0033 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx @@ -5,36 +5,14 @@ import { graphql } from "relay-runtime"; import { EditIcon } from "@quri/ui"; -import { MutationModalAction } from "@/components/ui/MutationModalAction"; +import { ServerActionModalAction } from "@/components/ui/ServerActionModalAction"; import { SlugFormField } from "@/components/ui/SlugFormField"; import { modelRoute } from "@/routes"; +import { updateModelSlugAction } from "@/server/models/actions/updateModelSlugAction"; import { draftUtils, modelToDraftLocator } from "./SquiggleSnippetDraftDialog"; import { UpdateModelSlugAction$key } from "@/__generated__/UpdateModelSlugAction.graphql"; -import { UpdateModelSlugActionMutation } from "@/__generated__/UpdateModelSlugActionMutation.graphql"; - -const Mutation = graphql` - mutation UpdateModelSlugActionMutation( - $input: MutationUpdateModelSlugInput! - ) { - result: updateModelSlug(input: $input) { - __typename - ... on BaseError { - message - } - ... on UpdateModelSlugResult { - model { - id - slug - owner { - slug - } - } - } - } - } -`; type Props = { model: UpdateModelSlugAction$key; @@ -64,21 +42,14 @@ export const UpdateModelSlugAction: FC = ({ const router = useRouter(); return ( - + title="Rename" icon={EditIcon} - mutation={Mutation} - expectedTypename="UpdateModelSlugResult" + action={updateModelSlugAction} formDataToVariables={(data) => ({ - input: { - owner: model.owner.slug, - oldSlug: model.slug, - newSlug: data.slug, - }, + owner: model.owner.slug, + oldSlug: model.slug, + newSlug: data.slug, })} onCompleted={({ model: newModel }) => { draftUtils.rename( @@ -102,6 +73,6 @@ export const UpdateModelSlugAction: FC = ({ name="slug" label="New slug" />
)} - + ); }; diff --git a/packages/hub/src/graphql/mutations/updateModelSlug.ts b/packages/hub/src/graphql/mutations/updateModelSlug.ts deleted file mode 100644 index 8c2494d876..0000000000 --- a/packages/hub/src/graphql/mutations/updateModelSlug.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { getWriteableModel } from "../helpers/modelHelpers"; -import { Model } from "../types/Model"; - -builder.mutationField("updateModelSlug", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("UpdateModelSlugResult", { - fields: (t) => ({ - model: t.field({ type: Model }), - }), - }), - errors: {}, - input: { - owner: t.input.string({ required: true }), - oldSlug: t.input.string({ required: true }), - newSlug: t.input.string({ - required: true, - validate: { - regex: /^\w[\w\-]*$/, - }, - }), - }, - resolve: async (_, { input }, { session }) => { - let model = await getWriteableModel({ - owner: input.owner, - slug: input.oldSlug, - session, - }); - - model = await prisma.model.update({ - where: { id: model.id }, - data: { slug: input.newSlug }, - }); - - return { model }; - }, - }) -); diff --git a/packages/hub/src/graphql/schema.ts b/packages/hub/src/graphql/schema.ts index 0ee9cb7d10..a2d2646ddf 100644 --- a/packages/hub/src/graphql/schema.ts +++ b/packages/hub/src/graphql/schema.ts @@ -36,7 +36,6 @@ import "./mutations/reactToGroupInvite"; import "./mutations/updateGroupInviteRole"; import "./mutations/updateMembershipRole"; import "./mutations/updateModelPrivacy"; -import "./mutations/updateModelSlug"; import "./mutations/updateRelativeValuesDefinition"; import "./mutations/updateSquiggleSnippetModel"; import "./mutations/validateReusableGroupInviteToken"; diff --git a/packages/hub/src/hooks/useServerActionForm.ts b/packages/hub/src/hooks/useServerActionForm.ts index b31fc84a25..6dea572cf8 100644 --- a/packages/hub/src/hooks/useServerActionForm.ts +++ b/packages/hub/src/hooks/useServerActionForm.ts @@ -1,6 +1,8 @@ import { BaseSyntheticEvent, useCallback } from "react"; import { FieldValues, useForm, UseFormProps } from "react-hook-form"; +import { useToast } from "@quri/ui"; + /** * This hook ties together `useForm` and server actions. * @@ -32,12 +34,18 @@ export function useServerActionForm< } & Pick, "defaultValues" | "mode">) { const form = useForm({ defaultValues, mode }); + const toast = useToast(); + const onSubmit = useCallback( (event?: BaseSyntheticEvent) => form.handleSubmit(async (formData) => { // TODO - transition? - const result = await action(formDataToVariables(formData)); - onCompleted?.(result); + try { + const result = await action(formDataToVariables(formData)); + onCompleted?.(result); + } catch (error) { + toast(String(error), "error"); + } })(event), [form, formDataToVariables, onCompleted, action] ); diff --git a/packages/hub/src/server/models/actions/updateModelSlugAction.ts b/packages/hub/src/server/models/actions/updateModelSlugAction.ts new file mode 100644 index 0000000000..12ff1b9de6 --- /dev/null +++ b/packages/hub/src/server/models/actions/updateModelSlugAction.ts @@ -0,0 +1,36 @@ +"use server"; + +import { z } from "zod"; + +import { getWriteableModel } from "@/graphql/helpers/modelHelpers"; +import { prisma } from "@/prisma"; +import { getSessionOrRedirect } from "@/server/users/auth"; +import { makeServerAction, zSlug } from "@/server/utils"; + +export const updateModelSlugAction = makeServerAction( + z.object({ + owner: zSlug, + oldSlug: zSlug, + newSlug: zSlug, + }), + async (input) => { + const session = await getSessionOrRedirect(); + + const model = await getWriteableModel({ + owner: input.owner, + slug: input.oldSlug, + session, + }); + + const newModel = await prisma.model.update({ + where: { id: model.id }, + data: { slug: input.newSlug }, + select: { + slug: true, + owner: true, + }, + }); + + return { model: newModel }; + } +); From 64844e54c5a8c5158d88e9a9c6963194a05162b3 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 28 Nov 2024 00:24:15 -0300 Subject: [PATCH 23/68] rewrite SelectOwner, SelectUser, SelectGroup --- packages/hub/src/app/api/find-owners/route.ts | 24 ++++ .../[slug]/members/AddUserToGroupAction.tsx | 2 +- packages/hub/src/components/SelectGroup.tsx | 54 +++----- packages/hub/src/components/SelectOwner.tsx | 71 +++------- packages/hub/src/components/SelectUser.tsx | 59 +++----- packages/hub/src/graphql/queries/users.ts | 41 ------ packages/hub/src/graphql/schema.ts | 1 - .../server/models/actions/moveModelAction.ts | 2 +- packages/hub/src/server/owners/data.ts | 131 ++++++++++++++++++ 9 files changed, 217 insertions(+), 168 deletions(-) create mode 100644 packages/hub/src/app/api/find-owners/route.ts delete mode 100644 packages/hub/src/graphql/queries/users.ts create mode 100644 packages/hub/src/server/owners/data.ts diff --git a/packages/hub/src/app/api/find-owners/route.ts b/packages/hub/src/app/api/find-owners/route.ts new file mode 100644 index 0000000000..ba41288cea --- /dev/null +++ b/packages/hub/src/app/api/find-owners/route.ts @@ -0,0 +1,24 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; + +import { findOwnersForSelect } from "@/server/owners/data"; + +// We're not calling this as a server actions because it'd be too slow (server actions are sequential). +// TODO: it'd be good to use tRPC for this. +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + + const { search, mode } = z + .object({ + search: z.string(), + mode: z.enum(["all", "all-users", "all-groups", "my", "my-groups"]), + }) + .parse(Object.fromEntries(searchParams.entries())); + + return Response.json( + await findOwnersForSelect({ + search, + mode, + }) + ); +} diff --git a/packages/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx b/packages/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx index 42ac97c2a6..0314466896 100644 --- a/packages/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx +++ b/packages/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx @@ -63,7 +63,7 @@ export const AddUserToGroupAction: FC = ({ groupRef, close }) => { formDataToVariables={(data) => ({ input: { group: group.slug, - username: data.user.username, + username: data.user.slug, role: data.role, }, connections: [ diff --git a/packages/hub/src/components/SelectGroup.tsx b/packages/hub/src/components/SelectGroup.tsx index b630bfd2bc..3b99b16770 100644 --- a/packages/hub/src/components/SelectGroup.tsx +++ b/packages/hub/src/components/SelectGroup.tsx @@ -1,30 +1,13 @@ "use client"; import { FieldPathByValue, FieldValues } from "react-hook-form"; -import { useRelayEnvironment } from "react-relay"; -import { fetchQuery, graphql } from "relay-runtime"; +import { z } from "zod"; import { SelectFormField } from "@quri/ui"; -import { - SelectGroupQuery, - SelectGroupQuery$data, -} from "@/__generated__/SelectGroupQuery.graphql"; - -const Query = graphql` - query SelectGroupQuery($input: GroupsQueryInput!) { - groups(input: $input) { - edges { - node { - id - slug - } - } - } - } -`; - -export type SelectGroupOption = - SelectGroupQuery$data["groups"]["edges"][number]["node"]; +export type SelectGroupOption = { + id: string; + slug: string; +}; export function SelectGroup< TValues extends FieldValues, @@ -45,23 +28,26 @@ export function SelectGroup< required?: boolean; myOnly?: boolean; }) { - const environment = useRelayEnvironment(); - const loadOptions = async ( inputValue: string ): Promise => { - const result = await fetchQuery(environment, Query, { - input: { - slugContains: inputValue, - myOnly, - }, - }).toPromise(); + const result = await fetch( + `/api/find-owners?${new URLSearchParams({ + search: inputValue, + mode: myOnly ? "my-groups" : "all-groups", + })}` + ).then((r) => r.json()); - if (!result) { - return []; - } + const data = z + .array( + z.object({ + id: z.string(), + slug: z.string(), + }) + ) + .parse(result); - return result.groups.edges.map((edge) => edge.node); + return data; }; return ( diff --git a/packages/hub/src/components/SelectOwner.tsx b/packages/hub/src/components/SelectOwner.tsx index 3d27c81045..964e76e6f2 100644 --- a/packages/hub/src/components/SelectOwner.tsx +++ b/packages/hub/src/components/SelectOwner.tsx @@ -1,47 +1,12 @@ "use client"; import { FC } from "react"; import { FieldPathByValue, FieldValues } from "react-hook-form"; -import { useRelayEnvironment } from "react-relay"; -import { fetchQuery, graphql } from "relay-runtime"; +import { z } from "zod"; import { SelectFormField } from "@quri/ui"; import { ownerIcon } from "@/lib/ownerIcon"; -import { SelectOwnerQuery } from "@/__generated__/SelectOwnerQuery.graphql"; - -const Query = graphql` - query SelectOwnerQuery($search: String!, $myOnly: Boolean!) { - me @include(if: $myOnly) { - asUser { - __typename - id - slug - } - } - users(input: { usernameContains: $search }) @skip(if: $myOnly) { - edges { - node { - __typename - id - slug - } - } - } - groups(input: { slugContains: $search, myOnly: $myOnly }) { - edges { - node { - __typename - id - slug - } - } - } - } -`; - -// Note: we can't wrap this in a fragment because it's not possible to call `useFragment` -// in SelectFormField callbacks. export type SelectOwnerOption = { __typename: "User" | "Group"; id: string; @@ -75,29 +40,27 @@ export function SelectOwner< required?: boolean; myOnly?: boolean; }) { - const environment = useRelayEnvironment(); - const loadOptions = async ( inputValue: string ): Promise => { - const result = await fetchQuery(environment, Query, { - search: inputValue, - myOnly, - }).toPromise(); + const result = await fetch( + `/api/find-owners?${new URLSearchParams({ + search: inputValue, + mode: myOnly ? "my" : "all", + })}` + ).then((r) => r.json()); - if (!result) { - return []; - } + const data = z + .array( + z.object({ + __typename: z.enum(["User", "Group"]), + id: z.string(), + slug: z.string(), + }) + ) + .parse(result); - const options: SelectOwnerOption[] = []; - if (result.me) { - options.push(result.me.asUser); - } - if (result.users) { - options.push(...(result.users.edges.map((edge) => edge.node) ?? [])); - } - options.push(...result.groups.edges.map((edge) => edge.node)); - return options; + return data; }; return ( diff --git a/packages/hub/src/components/SelectUser.tsx b/packages/hub/src/components/SelectUser.tsx index 3e20e64a8f..c22a6da359 100644 --- a/packages/hub/src/components/SelectUser.tsx +++ b/packages/hub/src/components/SelectUser.tsx @@ -1,30 +1,13 @@ "use client"; import { FieldPathByValue, FieldValues } from "react-hook-form"; -import { useRelayEnvironment } from "react-relay"; -import { fetchQuery, graphql } from "relay-runtime"; +import { z } from "zod"; import { SelectFormField } from "@quri/ui"; -import { - SelectUserQuery, - SelectUserQuery$data, -} from "@/__generated__/SelectUserQuery.graphql"; - -const Query = graphql` - query SelectUserQuery($input: UsersQueryInput!) { - users(input: $input) { - edges { - node { - id - username - } - } - } - } -`; - -export type SelectUserOption = - SelectUserQuery$data["users"]["edges"][number]["node"]; +export type SelectUserOption = { + id: string; + slug: string; +}; export function SelectUser({ name, @@ -35,22 +18,26 @@ export function SelectUser({ label?: string; required?: boolean; }) { - const environment = useRelayEnvironment(); - const loadOptions = async ( inputValue: string ): Promise => { - const result = await fetchQuery(environment, Query, { - input: { - usernameContains: inputValue, - }, - }).toPromise(); - - if (!result) { - return []; - } - - return result.users.edges.map((edge) => edge.node); + const result = await fetch( + `/api/find-owners?${new URLSearchParams({ + search: inputValue, + mode: "all-users", + })}` + ).then((r) => r.json()); + + const data = z + .array( + z.object({ + id: z.string(), + slug: z.string(), + }) + ) + .parse(result); + + return data; }; return ( @@ -60,7 +47,7 @@ export function SelectUser({ required={required} async loadOptions={loadOptions} - renderOption={(user) => user.username} + renderOption={(user) => user.slug} /> ); } diff --git a/packages/hub/src/graphql/queries/users.ts b/packages/hub/src/graphql/queries/users.ts deleted file mode 100644 index 35620d1620..0000000000 --- a/packages/hub/src/graphql/queries/users.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Prisma } from "@prisma/client"; - -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { User } from "../types/User"; - -const UsersQueryInput = builder.inputType("UsersQueryInput", { - fields: (t) => ({ - usernameContains: t.string(), - }), -}); - -builder.queryField("users", (t) => - t.prismaConnection({ - type: User, - cursor: "id", - args: { - input: t.arg({ type: UsersQueryInput }), - }, - resolve: async (query, _, { input }) => { - const where: Prisma.UserWhereInput = { - ownerId: { not: null }, - }; - - if (input?.usernameContains) { - where.asOwner = { - slug: { - contains: input.usernameContains, - mode: "insensitive", - }, - }; - } - - return await prisma.user.findMany({ - ...query, - where, - }); - }, - }) -); diff --git a/packages/hub/src/graphql/schema.ts b/packages/hub/src/graphql/schema.ts index a2d2646ddf..d806af2979 100644 --- a/packages/hub/src/graphql/schema.ts +++ b/packages/hub/src/graphql/schema.ts @@ -15,7 +15,6 @@ import "./queries/relativeValuesDefinitions"; import "./queries/runSquiggle"; import "./queries/search"; import "./queries/userByUsername"; -import "./queries/users"; import "./mutations/acceptReusableGroupInviteToken"; import "./mutations/adminUpdateModelVersion"; import "./mutations/adminRebuildSearchIndex"; diff --git a/packages/hub/src/server/models/actions/moveModelAction.ts b/packages/hub/src/server/models/actions/moveModelAction.ts index 880a4785b1..eccb32d081 100644 --- a/packages/hub/src/server/models/actions/moveModelAction.ts +++ b/packages/hub/src/server/models/actions/moveModelAction.ts @@ -17,7 +17,7 @@ export const moveModelAction = makeServerAction( async (input) => { const session = await getSessionOrRedirect(); - let model = await getWriteableModel({ + const model = await getWriteableModel({ owner: input.oldOwner, slug: input.slug, session, diff --git a/packages/hub/src/server/owners/data.ts b/packages/hub/src/server/owners/data.ts new file mode 100644 index 0000000000..ef017bc8bf --- /dev/null +++ b/packages/hub/src/server/owners/data.ts @@ -0,0 +1,131 @@ +import "server-only"; + +import { auth } from "@/auth"; +import { prisma } from "@/prisma"; + +type OwnerForSelect = { + __typename: "User" | "Group"; + id: string; + slug: string; +}; + +// See also: app/api/find-owners/route.ts +export async function findOwnersForSelect(params: { + search: string; + // "my" means "me and my groups" + mode: "all" | "all-users" | "all-groups" | "my" | "my-groups"; +}): Promise { + const result: OwnerForSelect[] = []; + const session = await auth(); + + const selectUsers = params.mode === "all-users" || params.mode === "all"; + + const selectGroups = + params.mode === "all" || + params.mode === "all-groups" || + params.mode === "my-groups" || + params.mode === "my"; + + const myOnly = params.mode === "my" || params.mode === "my-groups"; + + if (myOnly && !session?.user.email) { + return []; + } + + if ( + (selectUsers || params.mode === "my") && + session?.user.id && + session?.user?.username && + (!params.search || session.user.username.match(params.search)) + ) { + result.push({ + __typename: "User" as const, + id: session.user.id, + slug: session.user.username ?? "", + }); + } + + if (selectUsers) { + const rows = await prisma.user.findMany({ + where: { + ownerId: { not: null }, + ...(params.search && { + asOwner: { + slug: { + contains: params.search, + mode: "insensitive", + }, + }, + }), + }, + select: { + id: true, + asOwner: { + select: { + slug: true, + }, + }, + }, + take: 20, + }); + + for (const row of rows) { + if (!row.asOwner) { + continue; + } + if (row.id === session?.user.id) { + // already added above + continue; + } + + result.push({ + __typename: "User" as const, + id: row.id, + slug: row.asOwner.slug, + }); + } + } + + if (selectGroups) { + const rows = await prisma.group.findMany({ + where: { + ...(params.search && { + asOwner: { + slug: { + contains: params.search, + mode: "insensitive", + }, + }, + }), + ...(myOnly + ? { + memberships: { + some: { + user: { email: session!.user.email }, + }, + }, + } + : {}), + }, + select: { + id: true, + asOwner: { + select: { + slug: true, + }, + }, + }, + take: 20, + }); + + result.push( + ...rows.map((row) => ({ + __typename: "Group" as const, + id: row.id, + slug: row.asOwner?.slug ?? "", + })) + ); + } + + return result; +} From e4c2ab443262f207a4cdf88c7174d93ae106052b Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 28 Nov 2024 00:32:46 -0300 Subject: [PATCH 24/68] select groups directly on /new/model --- packages/hub/schema.graphql | 44 ------------------------- packages/hub/src/app/new/model/page.tsx | 37 ++------------------- packages/hub/src/server/groups/data.ts | 17 +++++++--- 3 files changed, 15 insertions(+), 83 deletions(-) diff --git a/packages/hub/schema.graphql b/packages/hub/schema.graphql index 0f375deeca..418f9fa79e 100644 --- a/packages/hub/schema.graphql +++ b/packages/hub/schema.graphql @@ -236,10 +236,6 @@ type ModelsByVersion { version: String! } -type MoveModelResult { - model: Model! -} - type Mutation { acceptReusableGroupInviteToken(input: MutationAcceptReusableGroupInviteTokenInput!): MutationAcceptReusableGroupInviteTokenResult! addUserToGroup(input: MutationAddUserToGroupInput!): MutationAddUserToGroupResult! @@ -269,13 +265,10 @@ type Mutation { """Disable a reusable invite token for a group.""" deleteReusableGroupInviteToken(input: MutationDeleteReusableGroupInviteTokenInput!): MutationDeleteReusableGroupInviteTokenResult! inviteUserToGroup(input: MutationInviteUserToGroupInput!): MutationInviteUserToGroupResult! - moveModel(input: MutationMoveModelInput!): MutationMoveModelResult! reactToGroupInvite(input: MutationReactToGroupInviteInput!): MutationReactToGroupInviteResult! - setUsername(username: String!): MutationSetUsernameResult! updateGroupInviteRole(input: MutationUpdateGroupInviteRoleInput!): MutationUpdateGroupInviteRoleResult! updateMembershipRole(input: MutationUpdateMembershipRoleInput!): MutationUpdateMembershipRoleResult! updateModelPrivacy(input: MutationUpdateModelPrivacyInput!): MutationUpdateModelPrivacyResult! - updateModelSlug(input: MutationUpdateModelSlugInput!): MutationUpdateModelSlugResult! updateRelativeValuesDefinition(input: MutationUpdateRelativeValuesDefinitionInput!): MutationUpdateRelativeValuesDefinitionResult! updateSquiggleSnippetModel(input: MutationUpdateSquiggleSnippetModelInput!): MutationUpdateSquiggleSnippetModelResult! validateReusableGroupInviteToken(input: MutationValidateReusableGroupInviteTokenInput!): MutationValidateReusableGroupInviteTokenResult! @@ -403,14 +396,6 @@ input MutationInviteUserToGroupInput { union MutationInviteUserToGroupResult = BaseError | InviteUserToGroupResult | ValidationError -input MutationMoveModelInput { - newOwner: String! - oldOwner: String! - slug: String! -} - -union MutationMoveModelResult = BaseError | MoveModelResult | NotFoundError | ValidationError - input MutationReactToGroupInviteInput { action: GroupInviteReaction! inviteId: String! @@ -418,8 +403,6 @@ input MutationReactToGroupInviteInput { union MutationReactToGroupInviteResult = BaseError | ReactToGroupInviteResult -union MutationSetUsernameResult = BaseError | Me | ValidationError - input MutationUpdateGroupInviteRoleInput { inviteId: String! role: MembershipRole! @@ -443,14 +426,6 @@ input MutationUpdateModelPrivacyInput { union MutationUpdateModelPrivacyResult = BaseError | UpdateModelPrivacyResult -input MutationUpdateModelSlugInput { - newSlug: String! - oldSlug: String! - owner: String! -} - -union MutationUpdateModelSlugResult = BaseError | UpdateModelSlugResult - input MutationUpdateRelativeValuesDefinitionInput { clusters: [RelativeValuesClusterInput!]! items: [RelativeValuesItemInput!]! @@ -516,7 +491,6 @@ type Query { runSquiggle(code: String!, seed: String): SquiggleOutput! search(after: String, before: String, first: Int, last: Int, text: String!): QuerySearchResult! userByUsername(username: String!): QueryUserByUsernameResult! - users(after: String, before: String, first: Int, input: UsersQueryInput, last: Int): QueryUsersConnection! variable(input: QueryVariableInput!): QueryVariableResult! variables(after: String, before: String, first: Int, input: VariableQueryInput, last: Int): VariableConnection! } @@ -546,16 +520,6 @@ union QuerySearchResult = BaseError | QuerySearchConnection union QueryUserByUsernameResult = BaseError | NotFoundError | User -type QueryUsersConnection { - edges: [QueryUsersConnectionEdge!]! - pageInfo: PageInfo! -} - -type QueryUsersConnectionEdge { - cursor: String! - node: User! -} - input QueryVariableInput { owner: String! slug: String! @@ -712,10 +676,6 @@ type UpdateModelPrivacyResult { model: Model! } -type UpdateModelSlugResult { - model: Model! -} - type UpdateRelativeValuesDefinitionResult { definition: RelativeValuesDefinition! } @@ -760,10 +720,6 @@ type UserGroupMembershipEdge { node: UserGroupMembership! } -input UsersQueryInput { - usernameContains: String -} - type ValidateReusableGroupInviteTokenResult { ok: Boolean! } diff --git a/packages/hub/src/app/new/model/page.tsx b/packages/hub/src/app/new/model/page.tsx index 5d92f2d7e5..bde7b119c8 100644 --- a/packages/hub/src/app/new/model/page.tsx +++ b/packages/hub/src/app/new/model/page.tsx @@ -1,16 +1,12 @@ import { Metadata } from "next"; -import { fetchQuery, graphql } from "relay-runtime"; import { z } from "zod"; import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; -import { SelectGroupOption } from "@/components/SelectGroup"; -import { getCurrentEnvironment } from "@/relay/environment"; +import { getMyGroup } from "@/server/groups/data"; import { getSessionUserOrRedirect } from "@/server/users/auth"; import { NewModel } from "./NewModel"; -import { pageNewModelQuery } from "@/__generated__/pageNewModelQuery.graphql"; - export default async function OuterNewModelPage({ searchParams, }: { @@ -25,36 +21,7 @@ export default async function OuterNewModelPage({ .optional() .parse((await searchParams)["group"]); - const environment = getCurrentEnvironment(); - - let group: SelectGroupOption | null = null; - - if (groupSlug) { - const result = await fetchQuery( - environment, - graphql` - query pageNewModelQuery($groupSlug: String!) { - group(slug: $groupSlug) { - ... on Group { - id - slug - myMembership { - id - } - } - } - } - `, - { groupSlug } - ).toPromise(); - - if (result?.group?.id && result?.group?.slug) { - group = { - id: result.group.id, - slug: result.group.slug, - }; - } - } + const group = groupSlug ? await getMyGroup(groupSlug) : null; return ( diff --git a/packages/hub/src/server/groups/data.ts b/packages/hub/src/server/groups/data.ts index 801c27433d..73c6bb217a 100644 --- a/packages/hub/src/server/groups/data.ts +++ b/packages/hub/src/server/groups/data.ts @@ -7,15 +7,17 @@ import { prisma } from "@/prisma"; import { Paginated } from "../models/data"; -export async function hasGroupMembership(groupSlug: string) { +export async function getMyGroup( + groupSlug: string +): Promise { const session = await auth(); const userId = session?.user.id; if (!userId) { - return false; + return null; } const group = await prisma.group.findFirst({ - select: { id: true }, + select: groupCardSelect, where: { asOwner: { slug: groupSlug }, memberships: { @@ -23,7 +25,14 @@ export async function hasGroupMembership(groupSlug: string) { }, }, }); - return !!group; + if (!group) { + return null; + } + return dbGroupToGroupCard(group); +} + +export async function hasGroupMembership(groupSlug: string): boolean { + return !!(await getMyGroup(groupSlug)); } const groupCardSelect = { From 83f63dddc2f7332a0961b94bd998ddaebbfb9ed2 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 28 Nov 2024 00:49:56 -0300 Subject: [PATCH 25/68] convert createSquiggleSnippetModel --- packages/hub/src/app/new/model/NewModel.tsx | 48 ++------ packages/hub/src/components/SelectGroup.tsx | 4 + .../components/ui/ServerActionModalAction.tsx | 30 ++--- .../mutations/createSquiggleSnippetModel.ts | 104 ------------------ packages/hub/src/graphql/schema.ts | 1 - packages/hub/src/hooks/useServerActionForm.ts | 9 +- .../createSquiggleSnippetModelAction.ts | 92 ++++++++++++++++ 7 files changed, 120 insertions(+), 168 deletions(-) delete mode 100644 packages/hub/src/graphql/mutations/createSquiggleSnippetModel.ts create mode 100644 packages/hub/src/server/models/actions/createSquiggleSnippetModelAction.ts diff --git a/packages/hub/src/app/new/model/NewModel.tsx b/packages/hub/src/app/new/model/NewModel.tsx index 2c01d4469b..9797b593e7 100644 --- a/packages/hub/src/app/new/model/NewModel.tsx +++ b/packages/hub/src/app/new/model/NewModel.tsx @@ -2,7 +2,6 @@ import { useRouter } from "next/navigation"; import { FC, useState } from "react"; import { FormProvider } from "react-hook-form"; -import { graphql } from "relay-runtime"; import { generateSeed } from "@quri/squiggle-lang"; import { Button, CheckboxFormField } from "@quri/ui"; @@ -11,10 +10,9 @@ import { defaultSquiggleVersion } from "@quri/versioned-squiggle-components"; import { SelectGroup, SelectGroupOption } from "@/components/SelectGroup"; import { H1 } from "@/components/ui/Headers"; import { SlugFormField } from "@/components/ui/SlugFormField"; -import { useMutationForm } from "@/hooks/useMutationForm"; +import { useServerActionForm } from "@/hooks/useServerActionForm"; import { modelRoute } from "@/routes"; - -import { NewModelMutation } from "@/__generated__/NewModelMutation.graphql"; +import { createSquiggleSnippetModelAction } from "@/server/models/actions/createSquiggleSnippetModelAction"; const defaultCode = `/* Describe your code here @@ -36,10 +34,9 @@ export const NewModel: FC<{ initialGroup: SelectGroupOption | null }> = ({ const router = useRouter(); - const { form, onSubmit, inFlight } = useMutationForm< + const { form, onSubmit, inFlight } = useServerActionForm< FormShape, - NewModelMutation, - "CreateSquiggleSnippetModelResult" + typeof createSquiggleSnippetModelAction >({ mode: "onChange", defaultValues: { @@ -47,38 +44,15 @@ export const NewModel: FC<{ initialGroup: SelectGroupOption | null }> = ({ group, isPrivate: false, }, - mutation: graphql` - mutation NewModelMutation( - $input: MutationCreateSquiggleSnippetModelInput! - ) { - result: createSquiggleSnippetModel(input: $input) { - __typename - ... on BaseError { - message - } - ... on CreateSquiggleSnippetModelResult { - model { - id - slug - owner { - slug - } - } - } - } - } - `, - expectedTypename: "CreateSquiggleSnippetModelResult", blockOnSuccess: true, + action: createSquiggleSnippetModelAction, formDataToVariables: (data) => ({ - input: { - slug: data.slug ?? "", // shouldn't happen but satisfies Typescript - groupSlug: data.group?.slug, - isPrivate: data.isPrivate, - code: defaultCode, - version: defaultSquiggleVersion, - seed: generateSeed(), - }, + slug: data.slug ?? "", // shouldn't happen but satisfies Typescript + groupSlug: data.group?.slug, + isPrivate: data.isPrivate, + code: defaultCode, + version: defaultSquiggleVersion, + seed: generateSeed(), }), onCompleted: (result) => { router.push( diff --git a/packages/hub/src/components/SelectGroup.tsx b/packages/hub/src/components/SelectGroup.tsx index 3b99b16770..0bc8cec181 100644 --- a/packages/hub/src/components/SelectGroup.tsx +++ b/packages/hub/src/components/SelectGroup.tsx @@ -31,6 +31,10 @@ export function SelectGroup< const loadOptions = async ( inputValue: string ): Promise => { + if (typeof window === "undefined") { + return []; + } + const result = await fetch( `/api/find-owners?${new URLSearchParams({ search: inputValue, diff --git a/packages/hub/src/components/ui/ServerActionModalAction.tsx b/packages/hub/src/components/ui/ServerActionModalAction.tsx index b29dbd414a..6788888845 100644 --- a/packages/hub/src/components/ui/ServerActionModalAction.tsx +++ b/packages/hub/src/components/ui/ServerActionModalAction.tsx @@ -8,12 +8,9 @@ import { useServerActionForm } from "@/hooks/useServerActionForm"; type CommonProps< TFormShape extends FieldValues, - ActionVariables, - ActionResult, + Action extends (input: any) => Promise, > = Pick< - Parameters< - typeof useServerActionForm - >[0], + Parameters>[0], | "formDataToVariables" | "defaultValues" | "action" @@ -27,8 +24,7 @@ type CommonProps< function ServerActionFormModal< TFormShape extends FieldValues, - const ActionVariables, - const ActionResult, + Action extends (input: any) => Promise, >({ formDataToVariables, initialFocus, @@ -39,14 +35,10 @@ function ServerActionFormModal< close, title, children, -}: PropsWithChildren> & { +}: PropsWithChildren> & { title: string; }): ReactNode { - const { form, onSubmit, inFlight } = useServerActionForm< - TFormShape, - ActionVariables, - ActionResult - >({ + const { form, onSubmit, inFlight } = useServerActionForm({ mode: "onChange", defaultValues, action, @@ -81,11 +73,7 @@ export function ServerActionModalAction< icon, children, ...modalProps -}: CommonProps< - TFormShape, - Parameters[0], - Awaited> -> & { +}: CommonProps & { modalTitle: string; title: string; icon?: FC; @@ -96,11 +84,7 @@ export function ServerActionModalAction< title={title} icon={icon} render={() => ( - [0], - Awaited> - > + // Note that we pass the same `close` that's responsible for closing the dropdown. {...modalProps} title={modalTitle} diff --git a/packages/hub/src/graphql/mutations/createSquiggleSnippetModel.ts b/packages/hub/src/graphql/mutations/createSquiggleSnippetModel.ts deleted file mode 100644 index 45381a4709..0000000000 --- a/packages/hub/src/graphql/mutations/createSquiggleSnippetModel.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { ZodError } from "zod"; - -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { rethrowOnConstraint } from "../errors/common"; -import { getWriteableOwner } from "../helpers/ownerHelpers"; -import { indexModelId } from "../helpers/searchHelpers"; -import { getSelf } from "../helpers/userHelpers"; -import { Model } from "../types/Model"; -import { validateSlug } from "../utils"; - -builder.mutationField("createSquiggleSnippetModel", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("CreateSquiggleSnippetModelResult", { - fields: (t) => ({ - model: t.field({ type: Model }), - }), - }), - errors: { types: [ZodError] }, - input: { - groupSlug: t.input.string({ - validate: validateSlug, - description: - "Optional, if not set, model will be created on current user's account", - }), - code: t.input.string({ - required: true, - description: "Squiggle source code", - }), - version: t.input.string({ required: true }), - slug: t.input.string({ - required: true, - validate: validateSlug, - }), - isPrivate: t.input.boolean({ - description: "Defaults to false", - }), - seed: t.input.string({ - required: true, - description: "A unique seed, used for calculation", - }), - }, - resolve: async (_, { input }, { session }) => { - const model = await prisma.$transaction(async (tx) => { - const owner = await getWriteableOwner(session, input.groupSlug); - - // nested create is not possible here; - // similar problem is described here: https://github.com/prisma/prisma/discussions/14937, - // seems to be caused by multiple Model -> ModelRevision relations - const model = await rethrowOnConstraint( - () => - tx.model.create({ - data: { - slug: input.slug, - ownerId: owner.id, - isPrivate: input.isPrivate ?? false, - }, - }), - { - target: ["slug", "ownerId"], - error: `Model ${input.slug} already exists on this account`, - } - ); - - const self = await getSelf(session); - - const revision = await tx.modelRevision.create({ - data: { - squiggleSnippet: { - create: { - code: input.code, - version: input.version, - seed: input.seed, - }, - }, - author: { - connect: { id: self.id }, - }, - contentType: "SquiggleSnippet", - model: { - connect: { - id: model.id, - }, - }, - }, - }); - - return await tx.model.update({ - where: { - id: model.id, - }, - data: { - currentRevisionId: revision.id, - }, - }); - }); - - await indexModelId(model.id); - - return { model }; - }, - }) -); diff --git a/packages/hub/src/graphql/schema.ts b/packages/hub/src/graphql/schema.ts index d806af2979..f89e2b9db7 100644 --- a/packages/hub/src/graphql/schema.ts +++ b/packages/hub/src/graphql/schema.ts @@ -24,7 +24,6 @@ import "./mutations/clearRelativeValuesCache"; import "./mutations/createGroup"; import "./mutations/createRelativeValuesDefinition"; import "./mutations/createReusableGroupInviteToken"; -import "./mutations/createSquiggleSnippetModel"; import "./mutations/deleteMembership"; import "./mutations/deleteModel"; import "./mutations/deleteRelativeValuesDefinition"; diff --git a/packages/hub/src/hooks/useServerActionForm.ts b/packages/hub/src/hooks/useServerActionForm.ts index 6dea572cf8..b244014545 100644 --- a/packages/hub/src/hooks/useServerActionForm.ts +++ b/packages/hub/src/hooks/useServerActionForm.ts @@ -13,8 +13,9 @@ import { useToast } from "@quri/ui"; */ export function useServerActionForm< FormShape extends FieldValues = never, - ActionVariables = never, - ActionResult = never, + const Action extends (input: any) => Promise = never, + ActionVariables = Parameters[0], + ActionResult = Awaited>, >({ defaultValues, mode, @@ -45,9 +46,11 @@ export function useServerActionForm< onCompleted?.(result); } catch (error) { toast(String(error), "error"); + // important to rethrow; otherwise form will have `isSubmitting` set to true, which can make it disabled if `blockOnSuccess` is enabled + throw error; } })(event), - [form, formDataToVariables, onCompleted, action] + [form, formDataToVariables, onCompleted, action, toast] ); return { diff --git a/packages/hub/src/server/models/actions/createSquiggleSnippetModelAction.ts b/packages/hub/src/server/models/actions/createSquiggleSnippetModelAction.ts new file mode 100644 index 0000000000..d9aad210e6 --- /dev/null +++ b/packages/hub/src/server/models/actions/createSquiggleSnippetModelAction.ts @@ -0,0 +1,92 @@ +"use server"; + +import { z } from "zod"; + +import { rethrowOnConstraint } from "@/graphql/errors/common"; +import { getWriteableOwner } from "@/graphql/helpers/ownerHelpers"; +import { indexModelId } from "@/graphql/helpers/searchHelpers"; +import { getSelf } from "@/graphql/helpers/userHelpers"; +import { prisma } from "@/prisma"; +import { getSessionOrRedirect } from "@/server/users/auth"; +import { makeServerAction, zSlug } from "@/server/utils"; + +export const createSquiggleSnippetModelAction = makeServerAction( + z.object({ + groupSlug: zSlug.optional(), + code: z.string(), + version: z.string(), + slug: zSlug, + isPrivate: z.boolean().optional(), + seed: z.string(), + }), + async (input) => { + const session = await getSessionOrRedirect(); + + const model = await prisma.$transaction(async (tx) => { + const owner = await getWriteableOwner(session, input.groupSlug); + + // nested create is not possible here; + // similar problem is described here: https://github.com/prisma/prisma/discussions/14937, + // seems to be caused by multiple Model -> ModelRevision relations + const model = await rethrowOnConstraint( + () => + tx.model.create({ + data: { + slug: input.slug, + ownerId: owner.id, + isPrivate: input.isPrivate ?? false, + }, + }), + { + target: ["slug", "ownerId"], + error: `Model ${input.slug} already exists on this account`, + } + ); + + const self = await getSelf(session); + + const revision = await tx.modelRevision.create({ + data: { + squiggleSnippet: { + create: { + code: input.code, + version: input.version, + seed: input.seed, + }, + }, + author: { + connect: { id: self.id }, + }, + contentType: "SquiggleSnippet", + model: { + connect: { + id: model.id, + }, + }, + }, + }); + + return await tx.model.update({ + where: { + id: model.id, + }, + data: { + currentRevisionId: revision.id, + }, + select: { + id: true, + slug: true, + owner: { + select: { + slug: true, + }, + }, + }, + }); + }); + + await indexModelId(model.id); + + return { model }; + } +); From cef9e9ee3d1b40c6d4b41c33e3c066ebf2c86a1d Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 28 Nov 2024 00:52:28 -0300 Subject: [PATCH 26/68] type fix --- packages/hub/src/server/groups/data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/hub/src/server/groups/data.ts b/packages/hub/src/server/groups/data.ts index 73c6bb217a..36c93992c1 100644 --- a/packages/hub/src/server/groups/data.ts +++ b/packages/hub/src/server/groups/data.ts @@ -31,7 +31,7 @@ export async function getMyGroup( return dbGroupToGroupCard(group); } -export async function hasGroupMembership(groupSlug: string): boolean { +export async function hasGroupMembership(groupSlug: string): Promise { return !!(await getMyGroup(groupSlug)); } From 4209d738d1003dff2c560cf74d11829c7d290d46 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 28 Nov 2024 13:18:06 -0300 Subject: [PATCH 27/68] ModelLayout uses RSC --- .../[owner]/[slug]/DeleteModelAction.tsx | 22 +--- .../[owner]/[slug]/ModelAccessControls.tsx | 110 +++++++----------- .../app/models/[owner]/[slug]/ModelLayout.tsx | 94 ++++----------- .../[owner]/[slug]/ModelSettingsButton.tsx | 19 +-- .../models/[owner]/[slug]/MoveModelAction.tsx | 23 +--- .../[owner]/[slug]/UpdateModelSlugAction.tsx | 26 +---- .../src/app/models/[owner]/[slug]/layout.tsx | 22 ++-- ...elUrlCasing.ts => useFixModelUrlCasing.ts} | 18 +-- packages/hub/src/components/EntityLayout.tsx | 3 +- .../graphql/mutations/updateModelPrivacy.ts | 35 ------ .../mutations/updateSquiggleSnippetModel.ts | 4 + packages/hub/src/graphql/schema.ts | 1 - .../actions/updateModelPrivacyAction.ts | 36 ++++++ packages/hub/src/server/models/data.ts | 38 +++++- packages/hub/src/server/models/utils.ts | 29 +++++ .../Dropdown/DropdownMenuActionItem.tsx | 3 +- 16 files changed, 199 insertions(+), 284 deletions(-) rename packages/hub/src/app/models/[owner]/[slug]/{FixModelUrlCasing.ts => useFixModelUrlCasing.ts} (53%) delete mode 100644 packages/hub/src/graphql/mutations/updateModelPrivacy.ts create mode 100644 packages/hub/src/server/models/actions/updateModelPrivacyAction.ts create mode 100644 packages/hub/src/server/models/utils.ts diff --git a/packages/hub/src/app/models/[owner]/[slug]/DeleteModelAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/DeleteModelAction.tsx index b20eb7730c..f3043071b0 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/DeleteModelAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/DeleteModelAction.tsx @@ -1,13 +1,13 @@ import { useRouter } from "next/navigation"; import { FC, useCallback } from "react"; -import { useFragment, useMutation } from "react-relay"; +import { useMutation } from "react-relay"; import { graphql } from "relay-runtime"; import { DropdownMenuAsyncActionItem, TrashIcon, useToast } from "@quri/ui"; import { ownerRoute } from "@/routes"; +import { ModelCardData } from "@/server/models/data"; -import { DeleteModelAction$key } from "@/__generated__/DeleteModelAction.graphql"; import { DeleteModelActionMutation } from "@/__generated__/DeleteModelActionMutation.graphql"; const Mutation = graphql` @@ -22,25 +22,11 @@ const Mutation = graphql` `; type Props = { - model: DeleteModelAction$key; + model: ModelCardData; close(): void; }; -export const DeleteModelAction: FC = ({ model: modelKey, close }) => { - const model = useFragment( - graphql` - fragment DeleteModelAction on Model { - slug - owner { - __typename - id - slug - } - } - `, - modelKey - ); - +export const DeleteModelAction: FC = ({ model, close }) => { const router = useRouter(); const [mutation] = useMutation(Mutation); diff --git a/packages/hub/src/app/models/[owner]/[slug]/ModelAccessControls.tsx b/packages/hub/src/app/models/[owner]/[slug]/ModelAccessControls.tsx index 5697bc180c..66e841d9ad 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/ModelAccessControls.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/ModelAccessControls.tsx @@ -1,112 +1,84 @@ "use client"; import { clsx } from "clsx"; -import { FC } from "react"; -import { graphql, useFragment } from "react-relay"; +import { FC, useEffect, useState, useTransition } from "react"; import { Dropdown, DropdownMenu, - DropdownMenuAsyncActionItem, + DropdownMenuActionItem, GlobeIcon, LockIcon, + RefreshIcon, } from "@quri/ui"; -import { useAsyncMutation } from "@/hooks/useAsyncMutation"; - -import { ModelAccessControls$key } from "@/__generated__/ModelAccessControls.graphql"; -import { ModelAccessControlsMutation } from "@/__generated__/ModelAccessControlsMutation.graphql"; - -export const Fragment = graphql` - fragment ModelAccessControls on Model { - id - slug - isPrivate - isEditable - owner { - slug - } - } -`; +import { updateModelPrivacyAction } from "@/server/models/actions/updateModelPrivacyAction"; +import { ModelCardData } from "@/server/models/data"; function getIconComponent(isPrivate: boolean) { return isPrivate ? LockIcon : GlobeIcon; } -export const Mutation = graphql` - mutation ModelAccessControlsMutation( - $input: MutationUpdateModelPrivacyInput! - ) { - result: updateModelPrivacy(input: $input) { - __typename - ... on BaseError { - message - } - ... on UpdateModelPrivacyResult { - model { - id - isPrivate - } - } - } - } -`; - -export const UpdateModelPrivacyAction: FC<{ - modelRef: ModelAccessControls$key; - close(): void; -}> = ({ modelRef, close }) => { - const model = useFragment(Fragment, modelRef); - const [runMutation] = useAsyncMutation({ - mutation: Mutation, - expectedTypename: "UpdateModelPrivacyResult", - }); - - const act = () => - runMutation({ - variables: { - input: { - owner: model.owner.slug, - slug: model.slug, - isPrivate: !model.isPrivate, - }, - }, +const UpdatePrivacyAction: FC<{ + model: ModelCardData; + close: () => void; +}> = ({ model, close }) => { + const [initialIsPrivate] = useState(model.isPrivate); + const [isPending, startTransition] = useTransition(); + const act = () => { + startTransition(async () => { + await updateModelPrivacyAction({ + owner: model.owner.slug, + slug: model.slug, + isPrivate: !model.isPrivate, + }); }); + }; + + // We can't just call `close()` in the transition; server action finishes before it sends back the revalidated UI. + // This is an ugly workaround; see also: https://github.com/vercel/next.js/discussions/53206 + // Discussion in QURI Slack: https://quri.slack.com/archives/C059EEU0HMM/p1732810277978719 + useEffect(() => { + if (model.isPrivate !== initialIsPrivate) { + close(); + } + }, [model.isPrivate, initialIsPrivate, close]); return ( - ); }; -export const ModelAccessControls: FC<{ modelRef: ModelAccessControls$key }> = ({ - modelRef, -}) => { - const model = useFragment(Fragment, modelRef); +export const ModelAccessControls: FC<{ + model: ModelCardData; + isEditable: boolean; +}> = ({ model, isEditable }) => { + const { isPrivate } = model; - const Icon = getIconComponent(model.isPrivate); + const Icon = getIconComponent(isPrivate); const body = ( // TODO: copy-pasted from CacheMenu from relative-values, extract to or something
- {model.isPrivate ? "Private" : "Public"} + {isPrivate ? "Private" : "Public"}
); - return model.isEditable ? ( + return isEditable ? ( ( - + )} > diff --git a/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx b/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx index f0e05c6b7f..ac39677619 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx @@ -1,84 +1,31 @@ "use client"; import { FC, PropsWithChildren } from "react"; -import { graphql } from "relay-runtime"; import { CodeBracketSquareIcon, RectangleStackIcon, ShareIcon } from "@quri/ui"; import { EntityLayout } from "@/components/EntityLayout"; import { EntityTab } from "@/components/ui/EntityTab"; -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; import { totalImportLength, type VariableRevision, VariablesDropdown, } from "@/lib/VariablesDropdown"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; import { modelRevisionsRoute, modelRoute } from "@/routes"; +import { ModelCardData } from "@/server/models/data"; +import { getExportedVariableNames } from "@/server/models/utils"; -import { useFixModelUrlCasing } from "./FixModelUrlCasing"; import { ModelAccessControls } from "./ModelAccessControls"; import { ModelEntityNodes } from "./ModelEntityNodes"; import { ModelSettingsButton } from "./ModelSettingsButton"; - -import { ModelLayoutQuery } from "@/__generated__/ModelLayoutQuery.graphql"; - -// Note that we have to do two GraphQL queries on most model pages: one for layout.tsx, and one for page.tsx. -const Query = graphql` - query ModelLayoutQuery($input: QueryModelInput!) { - result: model(input: $input) { - __typename - ... on BaseError { - message - } - ... on NotFoundError { - message - } - ... on Model { - id - slug - isEditable - owner { - __typename - slug - } - ...FixModelUrlCasing - ...ModelAccessControls - ...ModelSettingsButton - variables { - id - variableName - currentRevision { - variableType - title - } - } - currentRevision { - id - exportNames - relativeValuesExports { - id - variableName - definition { - slug - } - } - } - } - } - } -`; +import { useFixModelUrlCasing } from "./useFixModelUrlCasing"; export const ModelLayout: FC< PropsWithChildren<{ - query: SerializablePreloadedQuery; + model: ModelCardData; + isEditable: boolean; }> -> = ({ query, children }) => { - const [{ result }] = usePageQuery(Query, query); - - const model = extractFromGraphqlErrorUnion(result, "Model"); - +> = ({ model, isEditable, children }) => { useFixModelUrlCasing(model); const modelUrl = modelRoute({ owner: model.owner.slug, slug: model.slug }); @@ -87,19 +34,20 @@ export const ModelLayout: FC< slug: model.slug, }); - const variableRevisions: VariableRevision[] = - model.currentRevision.exportNames.map((name) => { - const matchingVariable = model.variables.find( - (e) => e.variableName === name - ); + const variableRevisions: VariableRevision[] = getExportedVariableNames( + model.currentRevision.squiggleSnippet?.code ?? "" + ).map((name) => { + const matchingVariable = model.variables.find( + (e) => e.variableName === name + ); - return { - variableName: name, - variableType: - matchingVariable?.currentRevision?.variableType || undefined, - title: matchingVariable?.currentRevision?.title || undefined, - }; - }); + return { + variableName: name, + variableType: + matchingVariable?.currentRevision?.variableType || undefined, + title: matchingVariable?.currentRevision?.title || undefined, + }; + }); const relativeValuesExports = model.currentRevision.relativeValuesExports.map( ({ variableName, definition: { slug } }) => ({ @@ -117,7 +65,7 @@ export const ModelLayout: FC< } isFluid={true} - headerLeft={} + headerLeft={} headerRight={ - {model.isEditable ? : null} + {isEditable ? : null} } > diff --git a/packages/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx b/packages/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx index d2b683a8e3..7a96dbb209 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx @@ -1,31 +1,18 @@ "use client"; import { FC } from "react"; -import { graphql, useFragment } from "react-relay"; import { Cog8ToothIcon, Dropdown, DropdownMenu } from "@quri/ui"; import { EntityTab } from "@/components/ui/EntityTab"; +import { ModelCardData } from "@/server/models/data"; import { DeleteModelAction } from "./DeleteModelAction"; import { MoveModelAction } from "./MoveModelAction"; import { UpdateModelSlugAction } from "./UpdateModelSlugAction"; -import { ModelSettingsButton$key } from "@/__generated__/ModelSettingsButton.graphql"; - export const ModelSettingsButton: FC<{ - model: ModelSettingsButton$key; -}> = ({ model: modelKey }) => { - const model = useFragment( - graphql` - fragment ModelSettingsButton on Model { - ...UpdateModelSlugAction - ...MoveModelAction - ...DeleteModelAction - } - `, - modelKey - ); - + model: ModelCardData; +}> = ({ model }) => { return ( ( diff --git a/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx index 580d897697..b3f1501839 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx @@ -1,7 +1,5 @@ import { useRouter } from "next/navigation"; import { FC } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; import { RightArrowIcon } from "@quri/ui"; @@ -9,33 +7,18 @@ import { SelectOwner, SelectOwnerOption } from "@/components/SelectOwner"; import { ServerActionModalAction } from "@/components/ui/ServerActionModalAction"; import { modelRoute } from "@/routes"; import { moveModelAction } from "@/server/models/actions/moveModelAction"; +import { ModelCardData } from "@/server/models/data"; import { draftUtils, modelToDraftLocator } from "./SquiggleSnippetDraftDialog"; -import { MoveModelAction$key } from "@/__generated__/MoveModelAction.graphql"; - type FormShape = { owner: SelectOwnerOption }; type Props = { - model: MoveModelAction$key; + model: ModelCardData; close(): void; }; -export const MoveModelAction: FC = ({ model: modelKey, close }) => { - const model = useFragment( - graphql` - fragment MoveModelAction on Model { - slug - owner { - __typename - id - slug - } - } - `, - modelKey - ); - +export const MoveModelAction: FC = ({ model, close }) => { const router = useRouter(); return ( diff --git a/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx index 032d7f0033..d704af5c77 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx @@ -1,7 +1,5 @@ import { useRouter } from "next/navigation"; import { FC } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; import { EditIcon } from "@quri/ui"; @@ -9,36 +7,18 @@ import { ServerActionModalAction } from "@/components/ui/ServerActionModalAction import { SlugFormField } from "@/components/ui/SlugFormField"; import { modelRoute } from "@/routes"; import { updateModelSlugAction } from "@/server/models/actions/updateModelSlugAction"; +import { ModelCardData } from "@/server/models/data"; import { draftUtils, modelToDraftLocator } from "./SquiggleSnippetDraftDialog"; -import { UpdateModelSlugAction$key } from "@/__generated__/UpdateModelSlugAction.graphql"; - type Props = { - model: UpdateModelSlugAction$key; + model: ModelCardData; close(): void; }; type FormShape = { slug: string }; -export const UpdateModelSlugAction: FC = ({ - model: modelKey, - close, -}) => { - const model = useFragment( - graphql` - fragment UpdateModelSlugAction on Model { - slug - owner { - __typename - id - slug - } - } - `, - modelKey - ); - +export const UpdateModelSlugAction: FC = ({ model, close }) => { const router = useRouter(); return ( diff --git a/packages/hub/src/app/models/[owner]/[slug]/layout.tsx b/packages/hub/src/app/models/[owner]/[slug]/layout.tsx index d327cf1680..d656bf9d7d 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/layout.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/layout.tsx @@ -1,26 +1,30 @@ import { Metadata } from "next"; +import { notFound } from "next/navigation"; import { PropsWithChildren, Suspense } from "react"; -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { isModelEditable, loadModelCard } from "@/server/models/data"; import { FallbackModelLayout } from "./FallbackLayout"; import { ModelLayout } from "./ModelLayout"; -import ModelLayoutQueryNode, { - ModelLayoutQuery, -} from "@/__generated__/ModelLayoutQuery.graphql"; - type Props = PropsWithChildren<{ params: Promise<{ owner: string; slug: string }>; }>; async function LoadedLayout({ params, children }: Props) { const { owner, slug } = await params; - const query = await loadPageQuery(ModelLayoutQueryNode, { - input: { owner, slug }, - }); + const model = await loadModelCard({ owner, slug }); + if (!model) { + notFound(); + } + + const isEditable = await isModelEditable(model); - return {children}; + return ( + + {children} + + ); } export default async function Layout({ params, children }: Props) { diff --git a/packages/hub/src/app/models/[owner]/[slug]/FixModelUrlCasing.ts b/packages/hub/src/app/models/[owner]/[slug]/useFixModelUrlCasing.ts similarity index 53% rename from packages/hub/src/app/models/[owner]/[slug]/FixModelUrlCasing.ts rename to packages/hub/src/app/models/[owner]/[slug]/useFixModelUrlCasing.ts index 3472a4eb84..c0bc0f19f5 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/FixModelUrlCasing.ts +++ b/packages/hub/src/app/models/[owner]/[slug]/useFixModelUrlCasing.ts @@ -1,25 +1,11 @@ import { usePathname, useRouter } from "next/navigation"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; import { patchModelRoute } from "@/routes"; +import { ModelCardData } from "@/server/models/data"; -import { FixModelUrlCasing$key } from "@/__generated__/FixModelUrlCasing.graphql"; - -export const FixModelUrlCasingFragment = graphql` - fragment FixModelUrlCasing on Model { - id - slug - owner { - slug - } - } -`; - -export function useFixModelUrlCasing(modelRef: FixModelUrlCasing$key) { +export function useFixModelUrlCasing(model: ModelCardData) { const router = useRouter(); const pathname = usePathname(); - const model = useFragment(FixModelUrlCasingFragment, modelRef); const patchedPathname = patchModelRoute({ pathname, diff --git a/packages/hub/src/components/EntityLayout.tsx b/packages/hub/src/components/EntityLayout.tsx index dc5e6404b7..b94ccd0e69 100644 --- a/packages/hub/src/components/EntityLayout.tsx +++ b/packages/hub/src/components/EntityLayout.tsx @@ -1,8 +1,7 @@ -"use client"; import { clsx } from "clsx"; import { FC, ReactNode } from "react"; -import { EntityNode } from "./EntityInfo"; +import { type EntityNode } from "./EntityInfo"; export type { EntityNode }; diff --git a/packages/hub/src/graphql/mutations/updateModelPrivacy.ts b/packages/hub/src/graphql/mutations/updateModelPrivacy.ts deleted file mode 100644 index fddde8f499..0000000000 --- a/packages/hub/src/graphql/mutations/updateModelPrivacy.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { getWriteableModel } from "../helpers/modelHelpers"; -import { Model } from "../types/Model"; - -builder.mutationField("updateModelPrivacy", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("UpdateModelPrivacyResult", { - fields: (t) => ({ - model: t.field({ type: Model }), - }), - }), - errors: {}, - input: { - owner: t.input.string({ required: true }), - slug: t.input.string({ required: true }), - isPrivate: t.input.boolean({ required: true }), - }, - resolve: async (_, { input }, { session }) => { - let model = await getWriteableModel({ - slug: input.slug, - owner: input.owner, - session, - }); - - model = await prisma.model.update({ - where: { id: model.id }, - data: { isPrivate: input.isPrivate }, - }); - - return { model }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/updateSquiggleSnippetModel.ts b/packages/hub/src/graphql/mutations/updateSquiggleSnippetModel.ts index a35d0ec4a8..20217210ea 100644 --- a/packages/hub/src/graphql/mutations/updateSquiggleSnippetModel.ts +++ b/packages/hub/src/graphql/mutations/updateSquiggleSnippetModel.ts @@ -1,9 +1,11 @@ import { RelativeValuesDefinition } from "@prisma/client"; +import { revalidatePath } from "next/cache"; import { squiggleVersions } from "@quri/versioned-squiggle-components"; import { builder } from "@/graphql/builder"; import { prisma } from "@/prisma"; +import { modelRoute } from "@/routes"; import { getWriteableModel } from "../helpers/modelHelpers"; import { getSelf } from "../helpers/userHelpers"; @@ -177,6 +179,8 @@ builder.mutationField("updateSquiggleSnippetModel", (t) => return updatedModel; }); + revalidatePath(modelRoute({ owner: input.owner, slug: input.slug })); + return { model }; }, }) diff --git a/packages/hub/src/graphql/schema.ts b/packages/hub/src/graphql/schema.ts index f89e2b9db7..b90e18c8a0 100644 --- a/packages/hub/src/graphql/schema.ts +++ b/packages/hub/src/graphql/schema.ts @@ -33,7 +33,6 @@ import "./mutations/inviteUserToGroup"; import "./mutations/reactToGroupInvite"; import "./mutations/updateGroupInviteRole"; import "./mutations/updateMembershipRole"; -import "./mutations/updateModelPrivacy"; import "./mutations/updateRelativeValuesDefinition"; import "./mutations/updateSquiggleSnippetModel"; import "./mutations/validateReusableGroupInviteToken"; diff --git a/packages/hub/src/server/models/actions/updateModelPrivacyAction.ts b/packages/hub/src/server/models/actions/updateModelPrivacyAction.ts new file mode 100644 index 0000000000..4795a7394d --- /dev/null +++ b/packages/hub/src/server/models/actions/updateModelPrivacyAction.ts @@ -0,0 +1,36 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { z } from "zod"; + +import { getWriteableModel } from "@/graphql/helpers/modelHelpers"; +import { prisma } from "@/prisma"; +import { modelRoute } from "@/routes"; +import { getSessionOrRedirect } from "@/server/users/auth"; +import { makeServerAction, zSlug } from "@/server/utils"; + +export const updateModelPrivacyAction = makeServerAction( + z.object({ + owner: zSlug, + slug: zSlug, + isPrivate: z.boolean(), + }), + async (input) => { + const session = await getSessionOrRedirect(); + + const model = await getWriteableModel({ + slug: input.slug, + owner: input.owner, + session, + }); + + const newModel = await prisma.model.update({ + where: { id: model.id }, + data: { isPrivate: input.isPrivate }, + select: { id: true, isPrivate: true }, + }); + + revalidatePath(modelRoute({ owner: input.owner, slug: input.slug })); + return { isPrivate: newModel.isPrivate }; + } +); diff --git a/packages/hub/src/server/models/data.ts b/packages/hub/src/server/models/data.ts index 7d99c87347..9ba983d51f 100644 --- a/packages/hub/src/server/models/data.ts +++ b/packages/hub/src/server/models/data.ts @@ -46,12 +46,17 @@ function dbModelToModelCard(dbModel: DbModelCard) { check(dbModel); const ownerToGraphqlCompatible = (owner: { + id: string; slug: string; user: { id: string } | null; group: { id: string } | null; }) => { const __typename = owner.user ? "User" : "Group"; - return { slug: owner.slug, __typename }; + return { + id: owner.id, + slug: owner.slug, + __typename, + }; }; return { @@ -66,6 +71,7 @@ const modelCardSelect = { updatedAt: true, owner: { select: { + id: true, slug: true, user: { select: { id: true }, @@ -198,3 +204,33 @@ export async function loadModelCard({ return dbModelToModelCard(dbModel); } + +export async function isModelEditable(model: ModelCardData): Promise { + const session = await auth(); + if (!session?.user.email) { + return false; + } + return Boolean( + await prisma.owner.count({ + where: { + id: model.owner.id, + OR: [ + { + user: { email: session.user.email }, + }, + { + group: { + memberships: { + some: { + user: { + email: session.user.email, + }, + }, + }, + }, + }, + ], + }, + }) + ); +} diff --git a/packages/hub/src/server/models/utils.ts b/packages/hub/src/server/models/utils.ts new file mode 100644 index 0000000000..456ed42baa --- /dev/null +++ b/packages/hub/src/server/models/utils.ts @@ -0,0 +1,29 @@ +// TODO - should use versioned-components +import { ASTNode, parse } from "@quri/squiggle-lang"; + +function astToVariableNames(ast: ASTNode): string[] { + const exportedVariableNames: string[] = []; + + if (ast.kind === "Program") { + ast.statements.forEach((statement) => { + if ( + (statement.kind === "LetStatement" || + statement.kind === "DefunStatement") && + statement.exported + ) { + exportedVariableNames.push(statement.variable.value); + } + }); + } + + return exportedVariableNames; +} + +export function getExportedVariableNames(code: string): string[] { + const ast = parse(code); + if (ast.ok) { + return astToVariableNames(ast.value); + } else { + return []; + } +} diff --git a/packages/ui/src/components/Dropdown/DropdownMenuActionItem.tsx b/packages/ui/src/components/Dropdown/DropdownMenuActionItem.tsx index a214586461..a461cfd801 100644 --- a/packages/ui/src/components/Dropdown/DropdownMenuActionItem.tsx +++ b/packages/ui/src/components/Dropdown/DropdownMenuActionItem.tsx @@ -14,10 +14,11 @@ export const DropdownMenuActionItem: FC = ({ title, icon, onClick, + acting, }) => { return (
- +
); }; From 55563c699966d1fe1d42ba53de9528752d8c4ec7 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 28 Nov 2024 14:25:39 -0300 Subject: [PATCH 28/68] /status RSC --- packages/hub/src/app/status/StatusPage.tsx | 49 ------------------- packages/hub/src/app/status/layout.tsx | 15 ++++++ packages/hub/src/app/status/page.tsx | 30 +++++++----- .../src/graphql/queries/globalStatistics.ts | 27 ---------- packages/hub/src/graphql/schema.ts | 1 - packages/hub/src/server/globalStatistics.ts | 16 ++++++ 6 files changed, 50 insertions(+), 88 deletions(-) delete mode 100644 packages/hub/src/app/status/StatusPage.tsx create mode 100644 packages/hub/src/app/status/layout.tsx delete mode 100644 packages/hub/src/graphql/queries/globalStatistics.ts create mode 100644 packages/hub/src/server/globalStatistics.ts diff --git a/packages/hub/src/app/status/StatusPage.tsx b/packages/hub/src/app/status/StatusPage.tsx deleted file mode 100644 index 3a37d1005d..0000000000 --- a/packages/hub/src/app/status/StatusPage.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"use client"; - -import { FC } from "react"; -import { graphql } from "relay-runtime"; - -import { H1 } from "@/components/ui/Headers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; - -import { StatusPageQuery } from "@/__generated__/StatusPageQuery.graphql"; - -const Query = graphql` - query StatusPageQuery { - globalStatistics { - users - models - relativeValuesDefinitions - } - } -`; - -const StatRow: FC<{ name: string; value: number }> = ({ name, value }) => ( - - {name} - {value} - -); - -export const StatusPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [{ globalStatistics: stats }] = usePageQuery(Query, query); - - return ( -
-

Global statistics

- - - - - - -
-
- ); -}; diff --git a/packages/hub/src/app/status/layout.tsx b/packages/hub/src/app/status/layout.tsx new file mode 100644 index 0000000000..fc9d59e792 --- /dev/null +++ b/packages/hub/src/app/status/layout.tsx @@ -0,0 +1,15 @@ +import { PropsWithChildren } from "react"; + +import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; +import { H1 } from "@/components/ui/Headers"; + +export default function StatusLayout({ children }: PropsWithChildren) { + return ( + +
+

Global statistics

+ {children} +
+
+ ); +} diff --git a/packages/hub/src/app/status/page.tsx b/packages/hub/src/app/status/page.tsx index 1742dbc63c..9b978591a8 100644 --- a/packages/hub/src/app/status/page.tsx +++ b/packages/hub/src/app/status/page.tsx @@ -1,21 +1,29 @@ import { Metadata } from "next"; +import { FC } from "react"; -import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { getGlobalStatistics } from "@/server/globalStatistics"; -import { StatusPage } from "./StatusPage"; - -import QueryNode, { - StatusPageQuery, -} from "@/__generated__/StatusPageQuery.graphql"; +const StatRow: FC<{ name: string; value: number }> = ({ name, value }) => ( + + {name} + {value} + +); export default async function OuterFrontPage() { - const query = await loadPageQuery(QueryNode, {}); + const stats = await getGlobalStatistics(); return ( - - - + + + + + + +
); } diff --git a/packages/hub/src/graphql/queries/globalStatistics.ts b/packages/hub/src/graphql/queries/globalStatistics.ts deleted file mode 100644 index c9279304dd..0000000000 --- a/packages/hub/src/graphql/queries/globalStatistics.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -const GlobalStatistics = builder.simpleObject("GlobalStatistics", {}, (t) => ({ - users: t.int({ - async resolve() { - return await prisma.user.count(); - }, - }), - models: t.int({ - async resolve() { - return await prisma.model.count(); - }, - }), - relativeValuesDefinitions: t.int({ - async resolve() { - return await prisma.relativeValuesDefinition.count(); - }, - }), -})); - -builder.queryField("globalStatistics", (t) => - t.field({ - type: GlobalStatistics, - resolve: () => ({}), - }) -); diff --git a/packages/hub/src/graphql/schema.ts b/packages/hub/src/graphql/schema.ts index b90e18c8a0..a0f149f812 100644 --- a/packages/hub/src/graphql/schema.ts +++ b/packages/hub/src/graphql/schema.ts @@ -1,7 +1,6 @@ import "./errors/BaseError"; import "./errors/NotFoundError"; import "./errors/ValidationError"; -import "./queries/globalStatistics"; import "./queries/group"; import "./queries/groups"; import "./queries/me"; diff --git a/packages/hub/src/server/globalStatistics.ts b/packages/hub/src/server/globalStatistics.ts new file mode 100644 index 0000000000..d51cf2ca1c --- /dev/null +++ b/packages/hub/src/server/globalStatistics.ts @@ -0,0 +1,16 @@ +import "server-only"; + +import { prisma } from "@/prisma"; + +export async function getGlobalStatistics() { + const userCount = await prisma.user.count(); + const modelCount = await prisma.model.count(); + const relativeValuesDefinitionCount = + await prisma.relativeValuesDefinition.count(); + + return { + users: userCount, + models: modelCount, + relativeValuesDefinitions: relativeValuesDefinitionCount, + }; +} From d7cb0c909cb39c7627438c8901b874399023ef89 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 28 Nov 2024 14:56:13 -0300 Subject: [PATCH 29/68] rewrite model/revisions, start DTO refactorings --- packages/hub/src/app/(frontpage)/page.tsx | 2 +- packages/hub/src/app/groups/[slug]/page.tsx | 2 +- .../[owner]/[slug]/DeleteModelAction.tsx | 4 +- .../[owner]/[slug]/ModelAccessControls.tsx | 6 +- .../app/models/[owner]/[slug]/ModelLayout.tsx | 4 +- .../[owner]/[slug]/ModelSettingsButton.tsx | 4 +- .../models/[owner]/[slug]/MoveModelAction.tsx | 4 +- .../[owner]/[slug]/UpdateModelSlugAction.tsx | 4 +- .../src/app/models/[owner]/[slug]/layout.tsx | 3 +- .../[slug]/revisions/ModelRevisionsList.tsx | 118 +++------------- .../models/[owner]/[slug]/revisions/page.tsx | 19 +-- .../[owner]/[slug]/useFixModelUrlCasing.ts | 4 +- .../hub/src/app/users/[username]/page.tsx | 2 +- .../layout/RootLayout/MyGroupsMenu.tsx | 2 +- .../components/layout/RootLayout/PageMenu.tsx | 2 +- .../hub/src/groups/components/GroupList.tsx | 2 +- packages/hub/src/hooks/usePaginator.ts | 2 +- .../hub/src/models/components/ModelCard.tsx | 4 +- .../hub/src/models/components/ModelList.tsx | 5 +- .../RelativeValuesDefinitionList.tsx | 2 +- packages/hub/src/server/groups/data.ts | 2 +- .../models/actions/loadModelCardAction.ts | 4 +- .../hub/src/server/models/data/authHelpers.ts | 32 +++++ .../server/models/{data.ts => data/card.ts} | 76 ++--------- .../hub/src/server/models/data/helpers.ts | 35 +++++ .../hub/src/server/models/data/revisions.ts | 129 ++++++++++++++++++ .../hub/src/server/relative-values/data.ts | 2 +- packages/hub/src/server/types.ts | 4 + packages/hub/src/server/variables/data.ts | 3 +- .../src/squiggle/components/ImportTooltip.tsx | 4 +- .../src/variables/components/VariableList.tsx | 2 +- 31 files changed, 274 insertions(+), 214 deletions(-) create mode 100644 packages/hub/src/server/models/data/authHelpers.ts rename packages/hub/src/server/models/{data.ts => data/card.ts} (65%) create mode 100644 packages/hub/src/server/models/data/helpers.ts create mode 100644 packages/hub/src/server/models/data/revisions.ts create mode 100644 packages/hub/src/server/types.ts diff --git a/packages/hub/src/app/(frontpage)/page.tsx b/packages/hub/src/app/(frontpage)/page.tsx index a992ec27f6..32f08efd89 100644 --- a/packages/hub/src/app/(frontpage)/page.tsx +++ b/packages/hub/src/app/(frontpage)/page.tsx @@ -1,5 +1,5 @@ import { ModelList } from "@/models/components/ModelList"; -import { loadModelCards } from "@/server/models/data"; +import { loadModelCards } from "@/server/models/data/card"; export default async function FrontPage() { const page = await loadModelCards(); diff --git a/packages/hub/src/app/groups/[slug]/page.tsx b/packages/hub/src/app/groups/[slug]/page.tsx index b14dedac0f..2dde6f6029 100644 --- a/packages/hub/src/app/groups/[slug]/page.tsx +++ b/packages/hub/src/app/groups/[slug]/page.tsx @@ -1,6 +1,6 @@ import { ModelList } from "@/models/components/ModelList"; import { hasGroupMembership } from "@/server/groups/data"; -import { loadModelCards } from "@/server/models/data"; +import { loadModelCards } from "@/server/models/data/card"; type Props = { params: Promise<{ slug: string }>; diff --git a/packages/hub/src/app/models/[owner]/[slug]/DeleteModelAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/DeleteModelAction.tsx index f3043071b0..4fdb7059d8 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/DeleteModelAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/DeleteModelAction.tsx @@ -6,7 +6,7 @@ import { graphql } from "relay-runtime"; import { DropdownMenuAsyncActionItem, TrashIcon, useToast } from "@quri/ui"; import { ownerRoute } from "@/routes"; -import { ModelCardData } from "@/server/models/data"; +import { ModelCardDTO } from "@/server/models/data/card"; import { DeleteModelActionMutation } from "@/__generated__/DeleteModelActionMutation.graphql"; @@ -22,7 +22,7 @@ const Mutation = graphql` `; type Props = { - model: ModelCardData; + model: ModelCardDTO; close(): void; }; diff --git a/packages/hub/src/app/models/[owner]/[slug]/ModelAccessControls.tsx b/packages/hub/src/app/models/[owner]/[slug]/ModelAccessControls.tsx index 66e841d9ad..b132a953e3 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/ModelAccessControls.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/ModelAccessControls.tsx @@ -12,14 +12,14 @@ import { } from "@quri/ui"; import { updateModelPrivacyAction } from "@/server/models/actions/updateModelPrivacyAction"; -import { ModelCardData } from "@/server/models/data"; +import { ModelCardDTO } from "@/server/models/data/card"; function getIconComponent(isPrivate: boolean) { return isPrivate ? LockIcon : GlobeIcon; } const UpdatePrivacyAction: FC<{ - model: ModelCardData; + model: ModelCardDTO; close: () => void; }> = ({ model, close }) => { const [initialIsPrivate] = useState(model.isPrivate); @@ -54,7 +54,7 @@ const UpdatePrivacyAction: FC<{ }; export const ModelAccessControls: FC<{ - model: ModelCardData; + model: ModelCardDTO; isEditable: boolean; }> = ({ model, isEditable }) => { const { isPrivate } = model; diff --git a/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx b/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx index ac39677619..645ec03052 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx @@ -12,7 +12,7 @@ import { VariablesDropdown, } from "@/lib/VariablesDropdown"; import { modelRevisionsRoute, modelRoute } from "@/routes"; -import { ModelCardData } from "@/server/models/data"; +import { ModelCardDTO } from "@/server/models/data/card"; import { getExportedVariableNames } from "@/server/models/utils"; import { ModelAccessControls } from "./ModelAccessControls"; @@ -22,7 +22,7 @@ import { useFixModelUrlCasing } from "./useFixModelUrlCasing"; export const ModelLayout: FC< PropsWithChildren<{ - model: ModelCardData; + model: ModelCardDTO; isEditable: boolean; }> > = ({ model, isEditable, children }) => { diff --git a/packages/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx b/packages/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx index 7a96dbb209..ad81797d2b 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx @@ -4,14 +4,14 @@ import { FC } from "react"; import { Cog8ToothIcon, Dropdown, DropdownMenu } from "@quri/ui"; import { EntityTab } from "@/components/ui/EntityTab"; -import { ModelCardData } from "@/server/models/data"; +import { ModelCardDTO } from "@/server/models/data/card"; import { DeleteModelAction } from "./DeleteModelAction"; import { MoveModelAction } from "./MoveModelAction"; import { UpdateModelSlugAction } from "./UpdateModelSlugAction"; export const ModelSettingsButton: FC<{ - model: ModelCardData; + model: ModelCardDTO; }> = ({ model }) => { return ( = ({ modelRef, revisionRef }) => { - const revision = useFragment( - graphql` - fragment ModelRevisionsList_revision on ModelRevision { - id - createdAtTimestamp - buildStatus - author { - username - } - comment - variableRevisions { - id - } - lastBuild { - errors - runSeconds - } - } - `, - revisionRef - ); - - const model = useFragment( - graphql` - fragment ModelRevisionsList_model on Model { - id - slug - owner { - slug - } - } - `, - modelRef - ); - + model: ModelCardDTO; + revision: ModelRevisionDTO; +}> = ({ model, revision }) => { return (
@@ -67,7 +26,7 @@ const ModelRevisionItem: FC<{ revisionId: revision.id, })} > - {format(new Date(revision.createdAtTimestamp), commonDateFormat)} + {format(revision.createdAt, commonDateFormat)} {revision.author ? ( <> @@ -94,66 +53,23 @@ const ModelRevisionItem: FC<{ }; export const ModelRevisionsList: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [{ model: result }] = usePageQuery( - graphql` - query ModelRevisionsListQuery($input: QueryModelInput!) { - model(input: $input) { - __typename - ... on Model { - id - ...ModelRevisionsList_model - ...ModelRevisionsList - } - } - } - `, - query - ); - - const modelRef = extractFromGraphqlErrorUnion(result, "Model"); - - const { - data: { revisions }, - loadNext, - } = usePaginationFragment( - graphql` - fragment ModelRevisionsList on Model - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 20 } - ) - @refetchable(queryName: "ModelRevisionsListPaginationQuery") { - revisions(first: $count, after: $cursor) - @connection(key: "ModelRevisionsList_revisions") { - edges { - node { - id - ...ModelRevisionsList_revision - } - } - pageInfo { - hasNextPage - } - } - } - `, - modelRef - ); + page: Paginated; + model: ModelCardDTO; +}> = ({ page: initialPage, model }) => { + const { items: revisions, loadNext } = usePaginator(initialPage); return (
- {revisions.edges.map((edge) => ( + {revisions.map((revision) => ( ))}
- {revisions.pageInfo.hasNextPage && } + {loadNext && }
); }; diff --git a/packages/hub/src/app/models/[owner]/[slug]/revisions/page.tsx b/packages/hub/src/app/models/[owner]/[slug]/revisions/page.tsx index 4a8f114767..fcade354bc 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/revisions/page.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/revisions/page.tsx @@ -1,10 +1,9 @@ -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { notFound } from "next/navigation"; -import { ModelRevisionsList } from "./ModelRevisionsList"; +import { loadModelCard } from "@/server/models/data/card"; +import { loadModelRevisions } from "@/server/models/data/revisions"; -import QueryNode, { - ModelRevisionsListQuery, -} from "@/__generated__/ModelRevisionsListQuery.graphql"; +import { ModelRevisionsList } from "./ModelRevisionsList"; export default async function ModelPage({ params, @@ -12,9 +11,11 @@ export default async function ModelPage({ params: Promise<{ owner: string; slug: string }>; }) { const { owner, slug } = await params; - const query = await loadPageQuery(QueryNode, { - input: { owner, slug }, - }); + const page = await loadModelRevisions({ owner, slug }); + const model = await loadModelCard({ owner, slug }); + if (!model) { + notFound(); + } - return ; + return ; } diff --git a/packages/hub/src/app/models/[owner]/[slug]/useFixModelUrlCasing.ts b/packages/hub/src/app/models/[owner]/[slug]/useFixModelUrlCasing.ts index c0bc0f19f5..a2441096c0 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/useFixModelUrlCasing.ts +++ b/packages/hub/src/app/models/[owner]/[slug]/useFixModelUrlCasing.ts @@ -1,9 +1,9 @@ import { usePathname, useRouter } from "next/navigation"; import { patchModelRoute } from "@/routes"; -import { ModelCardData } from "@/server/models/data"; +import { ModelCardDTO } from "@/server/models/data/card"; -export function useFixModelUrlCasing(model: ModelCardData) { +export function useFixModelUrlCasing(model: ModelCardDTO) { const router = useRouter(); const pathname = usePathname(); diff --git a/packages/hub/src/app/users/[username]/page.tsx b/packages/hub/src/app/users/[username]/page.tsx index 3d9c7e1af4..ee5c78b43b 100644 --- a/packages/hub/src/app/users/[username]/page.tsx +++ b/packages/hub/src/app/users/[username]/page.tsx @@ -1,7 +1,7 @@ import { Metadata } from "next"; import { ModelList } from "@/models/components/ModelList"; -import { loadModelCards } from "@/server/models/data"; +import { loadModelCards } from "@/server/models/data/card"; type Props = { params: Promise<{ username: string }>; diff --git a/packages/hub/src/components/layout/RootLayout/MyGroupsMenu.tsx b/packages/hub/src/components/layout/RootLayout/MyGroupsMenu.tsx index e5e6e2e499..be46d56047 100644 --- a/packages/hub/src/components/layout/RootLayout/MyGroupsMenu.tsx +++ b/packages/hub/src/components/layout/RootLayout/MyGroupsMenu.tsx @@ -5,7 +5,7 @@ import { DropdownMenuHeader, GroupIcon, PlusIcon } from "@quri/ui"; import { DropdownMenuNextLinkItem } from "@/components/ui/DropdownMenuNextLinkItem"; import { groupRoute, newGroupRoute } from "@/routes"; import { GroupCardData } from "@/server/groups/data"; -import { Paginated } from "@/server/models/data"; +import { Paginated } from "@/server/types"; type Props = { groups: Paginated; diff --git a/packages/hub/src/components/layout/RootLayout/PageMenu.tsx b/packages/hub/src/components/layout/RootLayout/PageMenu.tsx index 66255914cd..51a8a425e6 100644 --- a/packages/hub/src/components/layout/RootLayout/PageMenu.tsx +++ b/packages/hub/src/components/layout/RootLayout/PageMenu.tsx @@ -20,7 +20,7 @@ import { import { SQUIGGLE_DOCS_URL } from "@/lib/common"; import { aboutRoute, aiRoute, newModelRoute } from "@/routes"; import { GroupCardData } from "@/server/groups/data"; -import { Paginated } from "@/server/models/data"; +import { Paginated } from "@/server/types"; import { GlobalSearch } from "../../GlobalSearch"; import { DesktopUserControls } from "./DesktopUserControls"; diff --git a/packages/hub/src/groups/components/GroupList.tsx b/packages/hub/src/groups/components/GroupList.tsx index a7b658833f..853b8102b1 100644 --- a/packages/hub/src/groups/components/GroupList.tsx +++ b/packages/hub/src/groups/components/GroupList.tsx @@ -4,7 +4,7 @@ import { FC } from "react"; import { LoadMore } from "@/components/LoadMore"; import { usePaginator } from "@/hooks/usePaginator"; import { GroupCardData } from "@/server/groups/data"; -import { Paginated } from "@/server/models/data"; +import { Paginated } from "@/server/types"; import { GroupCard } from "./GroupCard"; diff --git a/packages/hub/src/hooks/usePaginator.ts b/packages/hub/src/hooks/usePaginator.ts index 6461931de0..f217e0b0ef 100644 --- a/packages/hub/src/hooks/usePaginator.ts +++ b/packages/hub/src/hooks/usePaginator.ts @@ -1,6 +1,6 @@ import { useState } from "react"; -import { Paginated } from "@/server/models/data"; +import { Paginated } from "@/server/types"; export function usePaginator(initialPage: Paginated): { items: T[]; diff --git a/packages/hub/src/models/components/ModelCard.tsx b/packages/hub/src/models/components/ModelCard.tsx index 06fde66522..983f84a73e 100644 --- a/packages/hub/src/models/components/ModelCard.tsx +++ b/packages/hub/src/models/components/ModelCard.tsx @@ -17,10 +17,10 @@ import { VariablesDropdown, } from "@/lib/VariablesDropdown"; import { modelRoute, ownerRoute } from "@/routes"; -import { ModelCardData } from "@/server/models/data"; +import { ModelCardDTO } from "@/server/models/data/card"; type Props = { - model: ModelCardData; + model: ModelCardDTO; showOwner?: boolean; }; diff --git a/packages/hub/src/models/components/ModelList.tsx b/packages/hub/src/models/components/ModelList.tsx index 15a33ab954..bb6f66b09b 100644 --- a/packages/hub/src/models/components/ModelList.tsx +++ b/packages/hub/src/models/components/ModelList.tsx @@ -3,12 +3,13 @@ import { FC } from "react"; import { LoadMore } from "@/components/LoadMore"; import { usePaginator } from "@/hooks/usePaginator"; -import { ModelCardData, Paginated } from "@/server/models/data"; +import { ModelCardDTO } from "@/server/models/data/card"; +import { Paginated } from "@/server/types"; import { ModelCard } from "./ModelCard"; type Props = { - page: Paginated; + page: Paginated; showOwner?: boolean; }; diff --git a/packages/hub/src/relative-values/components/RelativeValuesDefinitionList.tsx b/packages/hub/src/relative-values/components/RelativeValuesDefinitionList.tsx index 7e1f5175d4..4d7410d2c4 100644 --- a/packages/hub/src/relative-values/components/RelativeValuesDefinitionList.tsx +++ b/packages/hub/src/relative-values/components/RelativeValuesDefinitionList.tsx @@ -4,8 +4,8 @@ import { FC } from "react"; import { LoadMore } from "@/components/LoadMore"; import { usePaginator } from "@/hooks/usePaginator"; -import { Paginated } from "@/server/models/data"; import { RelativeValuesDefinitionCardData } from "@/server/relative-values/data"; +import { Paginated } from "@/server/types"; import { RelativeValuesDefinitionCard } from "./RelativeValuesDefinitionCard"; diff --git a/packages/hub/src/server/groups/data.ts b/packages/hub/src/server/groups/data.ts index 36c93992c1..988b9ceed0 100644 --- a/packages/hub/src/server/groups/data.ts +++ b/packages/hub/src/server/groups/data.ts @@ -5,7 +5,7 @@ import { Prisma } from "@prisma/client"; import { auth } from "@/auth"; import { prisma } from "@/prisma"; -import { Paginated } from "../models/data"; +import { Paginated } from "../types"; export async function getMyGroup( groupSlug: string diff --git a/packages/hub/src/server/models/actions/loadModelCardAction.ts b/packages/hub/src/server/models/actions/loadModelCardAction.ts index be2fdad1f2..d4e55f1154 100644 --- a/packages/hub/src/server/models/actions/loadModelCardAction.ts +++ b/packages/hub/src/server/models/actions/loadModelCardAction.ts @@ -1,5 +1,5 @@ "use server"; -import { loadModelCard, ModelCardData } from "../data"; +import { loadModelCard, ModelCardDTO } from "../data/card"; // data-fetching action, used in ImportTooltip export async function loadModelCardAction({ @@ -8,6 +8,6 @@ export async function loadModelCardAction({ }: { owner: string; slug: string; -}): Promise { +}): Promise { return loadModelCard({ owner, slug }); } diff --git a/packages/hub/src/server/models/data/authHelpers.ts b/packages/hub/src/server/models/data/authHelpers.ts new file mode 100644 index 0000000000..843b7258ee --- /dev/null +++ b/packages/hub/src/server/models/data/authHelpers.ts @@ -0,0 +1,32 @@ +import "server-only"; + +import { Prisma } from "@prisma/client"; + +import { auth } from "@/auth"; + +export async function modelWhereHasAccess(): Promise { + const session = await auth(); + + const orParts: Prisma.ModelWhereInput[] = [{ isPrivate: false }]; + if (session) { + orParts.push({ + owner: { + OR: [ + { + user: { email: session.user.email }, + }, + { + group: { + memberships: { + some: { + user: { email: session.user.email }, + }, + }, + }, + }, + ], + }, + }); + } + return orParts; +} diff --git a/packages/hub/src/server/models/data.ts b/packages/hub/src/server/models/data/card.ts similarity index 65% rename from packages/hub/src/server/models/data.ts rename to packages/hub/src/server/models/data/card.ts index 9ba983d51f..e91bc9f785 100644 --- a/packages/hub/src/server/models/data.ts +++ b/packages/hub/src/server/models/data/card.ts @@ -2,37 +2,12 @@ import "server-only"; import { Prisma } from "@prisma/client"; -import { auth } from "@/auth"; import { prisma } from "@/prisma"; -// duplicates code in graphql/helpers/modelHelpers.ts -export async function modelWhereHasAccess(): Promise { - const session = await auth(); - const orParts: Prisma.ModelWhereInput[] = [{ isPrivate: false }]; - if (session) { - orParts.push({ - owner: { - OR: [ - { - user: { email: session.user.email }, - }, - { - group: { - memberships: { - some: { - user: { email: session.user.email }, - }, - }, - }, - }, - ], - }, - }); - } - return orParts; -} +import { Paginated } from "../../types"; +import { modelWhereHasAccess } from "./authHelpers"; -function dbModelToModelCard(dbModel: DbModelCard) { +function toDTO(dbModel: DbModelCard) { function check(model: DbModelCard): asserts model is Omit< DbModelCard, "currentRevision" @@ -134,12 +109,7 @@ type DbModelCard = NonNullable< > >; -export type ModelCardData = ReturnType; - -export type Paginated = { - items: T[]; - loadMore?: (limit: number) => Promise>; -}; +export type ModelCardDTO = ReturnType; export async function loadModelCards( params: { @@ -147,7 +117,7 @@ export async function loadModelCards( cursor?: string; limit?: number; } = {} -): Promise> { +): Promise> { const limit = params.limit ?? 20; const dbModels = await prisma.model.findMany({ @@ -167,7 +137,7 @@ export async function loadModelCards( take: limit + 1, }); - const models = dbModels.map(dbModelToModelCard); + const models = dbModels.map(toDTO); const nextCursor = models[models.length - 1]?.id; @@ -188,7 +158,7 @@ export async function loadModelCard({ }: { owner: string; slug: string; -}): Promise { +}): Promise { const dbModel = await prisma.model.findFirst({ select: modelCardSelect, where: { @@ -202,35 +172,5 @@ export async function loadModelCard({ return null; } - return dbModelToModelCard(dbModel); -} - -export async function isModelEditable(model: ModelCardData): Promise { - const session = await auth(); - if (!session?.user.email) { - return false; - } - return Boolean( - await prisma.owner.count({ - where: { - id: model.owner.id, - OR: [ - { - user: { email: session.user.email }, - }, - { - group: { - memberships: { - some: { - user: { - email: session.user.email, - }, - }, - }, - }, - }, - ], - }, - }) - ); + return toDTO(dbModel); } diff --git a/packages/hub/src/server/models/data/helpers.ts b/packages/hub/src/server/models/data/helpers.ts new file mode 100644 index 0000000000..610b61698c --- /dev/null +++ b/packages/hub/src/server/models/data/helpers.ts @@ -0,0 +1,35 @@ +import { auth } from "@/auth"; +import { prisma } from "@/prisma"; + +import { ModelCardDTO } from "./card"; + +export async function isModelEditable(model: ModelCardDTO): Promise { + const session = await auth(); + if (!session?.user.email) { + return false; + } + + return Boolean( + await prisma.owner.count({ + where: { + id: model.owner.id, + OR: [ + { + user: { email: session.user.email }, + }, + { + group: { + memberships: { + some: { + user: { + email: session.user.email, + }, + }, + }, + }, + }, + ], + }, + }) + ); +} diff --git a/packages/hub/src/server/models/data/revisions.ts b/packages/hub/src/server/models/data/revisions.ts new file mode 100644 index 0000000000..e471705b82 --- /dev/null +++ b/packages/hub/src/server/models/data/revisions.ts @@ -0,0 +1,129 @@ +import { Prisma } from "@prisma/client"; + +import { prisma } from "@/prisma"; +import { Paginated } from "@/server/types"; + +import { modelWhereHasAccess } from "./authHelpers"; + +const select = { + id: true, + createdAt: true, + author: { + select: { asOwner: { select: { slug: true } } }, + }, + comment: true, + variableRevisions: { + select: { + id: true, + }, + }, + // used for `buildStatus` and `lastBuild` + builds: { + select: { + errors: true, + runSeconds: true, + }, + orderBy: { + createdAt: "desc", + }, + take: 1, + }, + model: { + select: { + currentRevisionId: true, + }, + }, +} satisfies Prisma.ModelRevisionSelect; + +type BuildStatus = "Success" | "Failure" | "Pending" | "Skipped"; + +export type DbModelRevision = NonNullable< + Awaited< + ReturnType> + > +>; + +type DbModelRevisionBuild = DbModelRevision["builds"][number]; + +type ModelRevisionBuildDTO = { + runSeconds: number; + errors: string[]; +}; + +export type ModelRevisionDTO = { + id: string; + createdAt: Date; + author?: { username: string }; + comment?: string; + variableRevisions: { id: string }[]; + buildStatus: BuildStatus; + lastBuild?: ModelRevisionBuildDTO; +}; + +function buildToDTO(build: DbModelRevisionBuild): ModelRevisionBuildDTO { + return { + runSeconds: build.runSeconds, + errors: build.errors, + }; +} + +function revisionToDTO(dbRevision: DbModelRevision): ModelRevisionDTO { + const lastBuild = dbRevision.builds[0]; + let buildStatus: BuildStatus = "Pending"; + if (lastBuild) { + const errors = lastBuild.errors.filter((e) => e !== ""); + buildStatus = errors.length === 0 ? "Success" : "Failure"; + } else if (dbRevision.model.currentRevisionId !== dbRevision.id) { + buildStatus = "Skipped"; + } + + return { + id: dbRevision.id, + createdAt: dbRevision.createdAt, + author: dbRevision.author?.asOwner + ? { + username: dbRevision.author.asOwner?.slug, + } + : undefined, + comment: dbRevision.comment, + variableRevisions: dbRevision.variableRevisions, + buildStatus, + lastBuild: lastBuild ? buildToDTO(lastBuild) : undefined, + }; +} + +export async function loadModelRevisions(params: { + owner: string; + slug: string; + cursor?: string; + limit?: number; +}): Promise> { + const limit = params.limit ?? 20; + + const dbRevisions = await prisma.modelRevision.findMany({ + select, + where: { + model: { + slug: params.slug, + owner: { slug: params.owner }, + OR: await modelWhereHasAccess(), + }, + }, + cursor: params.cursor ? { id: params.cursor } : undefined, + take: limit + 1, + }); + + const nextCursor = dbRevisions[dbRevisions.length - 1]?.id; + + async function loadMore(limit: number) { + "use server"; + return loadModelRevisions({ ...params, cursor: nextCursor, limit }); + } + + const revisions = dbRevisions.map(revisionToDTO); + + return { + items: revisions.slice(0, limit), + loadMore: revisions.length > limit ? loadMore : undefined, + }; +} diff --git a/packages/hub/src/server/relative-values/data.ts b/packages/hub/src/server/relative-values/data.ts index 6a66e6429f..af760f2874 100644 --- a/packages/hub/src/server/relative-values/data.ts +++ b/packages/hub/src/server/relative-values/data.ts @@ -4,7 +4,7 @@ import { Prisma } from "@prisma/client"; import { prisma } from "@/prisma"; -import { Paginated } from "../models/data"; +import { Paginated } from "../types"; const definitionCardSelect = { id: true, diff --git a/packages/hub/src/server/types.ts b/packages/hub/src/server/types.ts new file mode 100644 index 0000000000..0966b174ea --- /dev/null +++ b/packages/hub/src/server/types.ts @@ -0,0 +1,4 @@ +export type Paginated = { + items: T[]; + loadMore?: (limit: number) => Promise>; +}; diff --git a/packages/hub/src/server/variables/data.ts b/packages/hub/src/server/variables/data.ts index 595b7e6d76..3e62731e6a 100644 --- a/packages/hub/src/server/variables/data.ts +++ b/packages/hub/src/server/variables/data.ts @@ -4,7 +4,8 @@ import { Prisma } from "@prisma/client"; import { prisma } from "@/prisma"; -import { modelWhereHasAccess, Paginated } from "../models/data"; +import { modelWhereHasAccess } from "../models/data/authHelpers"; +import { Paginated } from "../types"; const variableCardSelect = { id: true, diff --git a/packages/hub/src/squiggle/components/ImportTooltip.tsx b/packages/hub/src/squiggle/components/ImportTooltip.tsx index 97b3b85061..70b73f015c 100644 --- a/packages/hub/src/squiggle/components/ImportTooltip.tsx +++ b/packages/hub/src/squiggle/components/ImportTooltip.tsx @@ -4,7 +4,7 @@ import Skeleton from "react-loading-skeleton"; import { ModelCard } from "@/models/components/ModelCard"; import { loadModelCardAction } from "@/server/models/actions/loadModelCardAction"; -import { ModelCardData } from "@/server/models/data"; +import { ModelCardDTO } from "@/server/models/data/card"; import { parseSourceId } from "./linker"; @@ -15,7 +15,7 @@ type Props = { export const ImportTooltip: FC = ({ importId }) => { const { owner, slug } = parseSourceId(importId); - const [model, setModel] = useState( + const [model, setModel] = useState( "loading" ); diff --git a/packages/hub/src/variables/components/VariableList.tsx b/packages/hub/src/variables/components/VariableList.tsx index cd23da3428..6cb2dc35e9 100644 --- a/packages/hub/src/variables/components/VariableList.tsx +++ b/packages/hub/src/variables/components/VariableList.tsx @@ -3,7 +3,7 @@ import { FC } from "react"; import { LoadMore } from "@/components/LoadMore"; import { usePaginator } from "@/hooks/usePaginator"; -import { Paginated } from "@/server/models/data"; +import { Paginated } from "@/server/types"; import { VariableCardData } from "@/server/variables/data"; import { VariableCard } from "./VariableCard"; From ed05e2db4eaaf30a2ed3e38f4301271d86a02b47 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 28 Nov 2024 15:45:03 -0300 Subject: [PATCH 30/68] group page queries migrated --- .../hub/src/app/(frontpage)/groups/page.tsx | 2 +- .../hub/src/app/groups/[slug]/GroupLayout.tsx | 100 ---------------- .../hub/src/app/groups/[slug]/InviteForMe.tsx | 99 --------------- .../src/app/groups/[slug]/NewModelButton.tsx | 29 +++++ packages/hub/src/app/groups/[slug]/hooks.ts | 15 --- .../invite-link/AcceptGroupInvitePage.tsx | 34 +----- .../app/groups/[slug]/invite-link/page.tsx | 16 ++- packages/hub/src/app/groups/[slug]/layout.tsx | 52 ++++++-- packages/hub/src/app/groups/[slug]/page.tsx | 2 +- packages/hub/src/app/new/model/page.tsx | 2 +- .../src/app/users/[username]/groups/page.tsx | 2 +- .../layout/RootLayout/MyGroupsMenu.tsx | 4 +- .../components/layout/RootLayout/PageMenu.tsx | 4 +- .../components/layout/RootLayout/index.tsx | 2 +- .../src/graphql/mutations/addUserToGroup.ts | 1 - .../graphql/mutations/inviteUserToGroup.ts | 113 ------------------ .../graphql/mutations/reactToGroupInvite.ts | 78 ------------ packages/hub/src/graphql/schema.ts | 2 - .../hub/src/groups/components/GroupCard.tsx | 4 +- .../hub/src/groups/components/GroupList.tsx | 4 +- .../server/groups/{data.ts => data/card.ts} | 88 +++++++------- .../hub/src/server/groups/data/helpers.ts | 46 +++++++ 22 files changed, 195 insertions(+), 504 deletions(-) delete mode 100644 packages/hub/src/app/groups/[slug]/GroupLayout.tsx delete mode 100644 packages/hub/src/app/groups/[slug]/InviteForMe.tsx create mode 100644 packages/hub/src/app/groups/[slug]/NewModelButton.tsx delete mode 100644 packages/hub/src/graphql/mutations/inviteUserToGroup.ts delete mode 100644 packages/hub/src/graphql/mutations/reactToGroupInvite.ts rename packages/hub/src/server/groups/{data.ts => data/card.ts} (69%) create mode 100644 packages/hub/src/server/groups/data/helpers.ts diff --git a/packages/hub/src/app/(frontpage)/groups/page.tsx b/packages/hub/src/app/(frontpage)/groups/page.tsx index 030ebcfc5f..d9a0b62464 100644 --- a/packages/hub/src/app/(frontpage)/groups/page.tsx +++ b/packages/hub/src/app/(frontpage)/groups/page.tsx @@ -1,5 +1,5 @@ import { GroupList } from "@/groups/components/GroupList"; -import { loadGroupCards } from "@/server/groups/data"; +import { loadGroupCards } from "@/server/groups/data/card"; export default async function OuterGroupsPage() { const page = await loadGroupCards(); diff --git a/packages/hub/src/app/groups/[slug]/GroupLayout.tsx b/packages/hub/src/app/groups/[slug]/GroupLayout.tsx deleted file mode 100644 index 04c1ac5dfb..0000000000 --- a/packages/hub/src/app/groups/[slug]/GroupLayout.tsx +++ /dev/null @@ -1,100 +0,0 @@ -"use client"; -import { useRouter, useSelectedLayoutSegment } from "next/navigation"; -import { FC, PropsWithChildren } from "react"; -import { useSubscribeToInvalidationState } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { Button, GroupIcon, PlusIcon } from "@quri/ui"; - -import { H1 } from "@/components/ui/Headers"; -import { StyledTabLink } from "@/components/ui/StyledTabLink"; -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; -import { groupMembersRoute, groupRoute, newModelRoute } from "@/routes"; - -import { useIsGroupMember } from "./hooks"; -import { InviteForMe } from "./InviteForMe"; - -import { GroupLayoutQuery } from "@/__generated__/GroupLayoutQuery.graphql"; - -const Query = graphql` - query GroupLayoutQuery($slug: String!) { - result: group(slug: $slug) { - __typename - ... on BaseError { - message - } - ... on NotFoundError { - message - } - ... on Group { - id - slug - ...hooks_useIsGroupAdmin - ...hooks_useIsGroupMember - ...InviteForMe - } - } - } -`; - -const NewButton: FC<{ group: string }> = ({ group }) => { - const segment = useSelectedLayoutSegment(); - - let link = newModelRoute({ group }); - - const router = useRouter(); - - if (segment === "members" || segment === "invite-link") { - return null; - } - - return ( - - ); -}; - -export const GroupLayout: FC< - PropsWithChildren<{ - query: SerializablePreloadedQuery; - }> -> = ({ query, children }) => { - const [{ result }, { reload }] = usePageQuery(Query, query); - const group = extractFromGraphqlErrorUnion(result, "Group"); - - useSubscribeToInvalidationState([group.id], reload); - - const isMember = useIsGroupMember(group); - - return ( -
-

-
- - {group.slug} -
-

- -
- - - - - {isMember && } -
-
{children}
-
- ); -}; diff --git a/packages/hub/src/app/groups/[slug]/InviteForMe.tsx b/packages/hub/src/app/groups/[slug]/InviteForMe.tsx deleted file mode 100644 index c3bdef5202..0000000000 --- a/packages/hub/src/app/groups/[slug]/InviteForMe.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { FC } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { Card } from "@/components/ui/Card"; -import { MutationButton } from "@/components/ui/MutationButton"; - -import { InviteForMe$key } from "@/__generated__/InviteForMe.graphql"; -import { - GroupInviteReaction, - InviteForMeMutation, -} from "@/__generated__/InviteForMeMutation.graphql"; - -const Fragment = graphql` - fragment InviteForMe on Group { - id - inviteForMe { - id - role - } - } -`; - -const Mutation = graphql` - mutation InviteForMeMutation($input: MutationReactToGroupInviteInput!) { - result: reactToGroupInvite(input: $input) { - __typename - ... on BaseError { - message - } - ... on ReactToGroupInviteResult { - invite { - id - } - } - } - } -`; - -const InviteReactButton: FC<{ - inviteId: string; - groupId: string; - action: GroupInviteReaction; - title: string; - theme?: "default" | "primary"; -}> = ({ inviteId, groupId, action, title, theme }) => { - return ( - - mutation={Mutation} - variables={{ - input: { inviteId, action }, - }} - updater={(store) => { - // updating the invites connection is hard, let's just reload page data - store.get(groupId)?.invalidateRecord(); - }} - expectedTypename="ReactToGroupInviteResult" - title={title} - theme={theme} - > - ); -}; - -type Props = { - groupRef: InviteForMe$key; -}; - -export const InviteForMe: FC = ({ groupRef }) => { - const group = useFragment(Fragment, groupRef); - - if (!group.inviteForMe) { - return null; - } - - const inviteId = group.inviteForMe.id; - - return ( - -
-
{"You've been invited to this group."}
-
- - -
-
-
- ); -}; diff --git a/packages/hub/src/app/groups/[slug]/NewModelButton.tsx b/packages/hub/src/app/groups/[slug]/NewModelButton.tsx new file mode 100644 index 0000000000..a5910303b0 --- /dev/null +++ b/packages/hub/src/app/groups/[slug]/NewModelButton.tsx @@ -0,0 +1,29 @@ +"use client"; +import { useRouter, useSelectedLayoutSegment } from "next/navigation"; +import { FC } from "react"; + +import { Button, PlusIcon } from "@quri/ui"; + +import { newModelRoute } from "@/routes"; + +// TODO - this could be a server component, if we had `` component +export const NewModelButton: FC<{ group: string }> = ({ group }) => { + const segment = useSelectedLayoutSegment(); + + const link = newModelRoute({ group }); + + const router = useRouter(); + + if (segment === "members" || segment === "invite-link") { + return null; + } + + return ( + + ); +}; diff --git a/packages/hub/src/app/groups/[slug]/hooks.ts b/packages/hub/src/app/groups/[slug]/hooks.ts index dbcd099949..6fc0fe7b3b 100644 --- a/packages/hub/src/app/groups/[slug]/hooks.ts +++ b/packages/hub/src/app/groups/[slug]/hooks.ts @@ -1,7 +1,6 @@ import { graphql, useFragment } from "react-relay"; import { hooks_useIsGroupAdmin$key } from "@/__generated__/hooks_useIsGroupAdmin.graphql"; -import { hooks_useIsGroupMember$key } from "@/__generated__/hooks_useIsGroupMember.graphql"; export function useIsGroupAdmin(groupRef: hooks_useIsGroupAdmin$key) { const { myMembership } = useFragment( @@ -17,17 +16,3 @@ export function useIsGroupAdmin(groupRef: hooks_useIsGroupAdmin$key) { ); return myMembership?.role === "Admin"; } - -export function useIsGroupMember(groupRef: hooks_useIsGroupMember$key) { - const { myMembership } = useFragment( - graphql` - fragment hooks_useIsGroupMember on Group { - myMembership { - id - } - } - `, - groupRef - ); - return Boolean(myMembership); -} diff --git a/packages/hub/src/app/groups/[slug]/invite-link/AcceptGroupInvitePage.tsx b/packages/hub/src/app/groups/[slug]/invite-link/AcceptGroupInvitePage.tsx index a5fea6fc49..164aa2f5de 100644 --- a/packages/hub/src/app/groups/[slug]/invite-link/AcceptGroupInvitePage.tsx +++ b/packages/hub/src/app/groups/[slug]/invite-link/AcceptGroupInvitePage.tsx @@ -1,5 +1,5 @@ "use client"; -import { redirect, useRouter, useSearchParams } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import { FC, useEffect } from "react"; import { graphql } from "relay-runtime"; @@ -7,41 +7,15 @@ import { useToast } from "@quri/ui"; import { MutationButton } from "@/components/ui/MutationButton"; import { useAsyncMutation } from "@/hooks/useAsyncMutation"; -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; import { groupRoute } from "@/routes"; - -import { useIsGroupMember } from "../hooks"; +import { GroupCardDTO } from "@/server/groups/data/card"; import { AcceptGroupInvitePage_ValidateMutation } from "@/__generated__/AcceptGroupInvitePage_ValidateMutation.graphql"; import { AcceptGroupInvitePageMutation } from "@/__generated__/AcceptGroupInvitePageMutation.graphql"; -import { AcceptGroupInvitePageQuery } from "@/__generated__/AcceptGroupInvitePageQuery.graphql"; export const AcceptGroupInvitePage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [{ result }] = usePageQuery( - graphql` - query AcceptGroupInvitePageQuery($slug: String!) { - result: group(slug: $slug) { - __typename - ... on Group { - slug - ...hooks_useIsGroupMember - } - } - } - `, - query - ); - const group = extractFromGraphqlErrorUnion(result, "Group"); - const isGroupMember = useIsGroupMember(group); - - if (isGroupMember) { - redirect(groupRoute({ slug: group.slug })); - } - + group: GroupCardDTO; +}> = ({ group }) => { const params = useSearchParams(); const inviteToken = params.get("token"); if (!inviteToken) { diff --git a/packages/hub/src/app/groups/[slug]/invite-link/page.tsx b/packages/hub/src/app/groups/[slug]/invite-link/page.tsx index 2a36e863c5..c2cd51d517 100644 --- a/packages/hub/src/app/groups/[slug]/invite-link/page.tsx +++ b/packages/hub/src/app/groups/[slug]/invite-link/page.tsx @@ -1,5 +1,10 @@ +import { notFound, redirect } from "next/navigation"; + import { WithAuth } from "@/components/WithAuth"; import { loadPageQuery } from "@/relay/loadPageQuery"; +import { groupRoute } from "@/routes"; +import { loadGroupCard } from "@/server/groups/data/card"; +import { hasGroupMembership } from "@/server/groups/data/helpers"; import { AcceptGroupInvitePage } from "./AcceptGroupInvitePage"; @@ -17,7 +22,16 @@ async function InnerPage({ params }: Props) { slug, }); - return ; + const group = await loadGroupCard(slug); + if (!group) { + notFound(); + } + const isMember = await hasGroupMembership(slug); + if (isMember) { + redirect(groupRoute({ slug: group.slug })); + } + + return ; } export default async function ({ params }: Props) { diff --git a/packages/hub/src/app/groups/[slug]/layout.tsx b/packages/hub/src/app/groups/[slug]/layout.tsx index a08fef4a4f..63100a3eef 100644 --- a/packages/hub/src/app/groups/[slug]/layout.tsx +++ b/packages/hub/src/app/groups/[slug]/layout.tsx @@ -1,28 +1,58 @@ import { Metadata } from "next"; +import { notFound } from "next/navigation"; import { PropsWithChildren } from "react"; -import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { GroupIcon } from "@quri/ui"; -import { GroupLayout } from "./GroupLayout"; +import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; +import { H1 } from "@/components/ui/Headers"; +import { + StyledTabLink, + StyledTabLinkList, +} from "@/components/ui/StyledTabLink"; +import { groupMembersRoute, groupRoute } from "@/routes"; +import { loadGroupCard } from "@/server/groups/data/card"; +import { hasGroupMembership } from "@/server/groups/data/helpers"; -import QueryNode, { - GroupLayoutQuery, -} from "@/__generated__/GroupLayoutQuery.graphql"; +import { NewModelButton } from "./NewModelButton"; type Props = PropsWithChildren<{ params: Promise<{ slug: string }>; }>; -export default async function OuterGroupLayout({ params, children }: Props) { +export default async function GroupLayout({ params, children }: Props) { const { slug } = await params; - const query = await loadPageQuery(QueryNode, { - slug, - }); + const group = await loadGroupCard(slug); + if (!group) { + notFound(); + } + + const isMember = await hasGroupMembership(slug); return ( - {children} +
+

+
+ + {group.slug} +
+

+
+ + + + + {isMember && } +
+
{children}
+
); } diff --git a/packages/hub/src/app/groups/[slug]/page.tsx b/packages/hub/src/app/groups/[slug]/page.tsx index 2dde6f6029..da68121152 100644 --- a/packages/hub/src/app/groups/[slug]/page.tsx +++ b/packages/hub/src/app/groups/[slug]/page.tsx @@ -1,5 +1,5 @@ import { ModelList } from "@/models/components/ModelList"; -import { hasGroupMembership } from "@/server/groups/data"; +import { hasGroupMembership } from "@/server/groups/data/helpers"; import { loadModelCards } from "@/server/models/data/card"; type Props = { diff --git a/packages/hub/src/app/new/model/page.tsx b/packages/hub/src/app/new/model/page.tsx index bde7b119c8..3571c009e3 100644 --- a/packages/hub/src/app/new/model/page.tsx +++ b/packages/hub/src/app/new/model/page.tsx @@ -2,7 +2,7 @@ import { Metadata } from "next"; import { z } from "zod"; import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; -import { getMyGroup } from "@/server/groups/data"; +import { getMyGroup } from "@/server/groups/data/card"; import { getSessionUserOrRedirect } from "@/server/users/auth"; import { NewModel } from "./NewModel"; diff --git a/packages/hub/src/app/users/[username]/groups/page.tsx b/packages/hub/src/app/users/[username]/groups/page.tsx index 515ea4fb32..7e1b12ef12 100644 --- a/packages/hub/src/app/users/[username]/groups/page.tsx +++ b/packages/hub/src/app/users/[username]/groups/page.tsx @@ -1,7 +1,7 @@ import { Metadata } from "next"; import { GroupList } from "@/groups/components/GroupList"; -import { loadGroupCards } from "@/server/groups/data"; +import { loadGroupCards } from "@/server/groups/data/card"; type Props = { params: Promise<{ username: string }>; diff --git a/packages/hub/src/components/layout/RootLayout/MyGroupsMenu.tsx b/packages/hub/src/components/layout/RootLayout/MyGroupsMenu.tsx index be46d56047..36eca80f47 100644 --- a/packages/hub/src/components/layout/RootLayout/MyGroupsMenu.tsx +++ b/packages/hub/src/components/layout/RootLayout/MyGroupsMenu.tsx @@ -4,11 +4,11 @@ import { DropdownMenuHeader, GroupIcon, PlusIcon } from "@quri/ui"; import { DropdownMenuNextLinkItem } from "@/components/ui/DropdownMenuNextLinkItem"; import { groupRoute, newGroupRoute } from "@/routes"; -import { GroupCardData } from "@/server/groups/data"; +import { GroupCardDTO } from "@/server/groups/data/card"; import { Paginated } from "@/server/types"; type Props = { - groups: Paginated; + groups: Paginated; close: () => void; }; diff --git a/packages/hub/src/components/layout/RootLayout/PageMenu.tsx b/packages/hub/src/components/layout/RootLayout/PageMenu.tsx index 51a8a425e6..e29d6cacec 100644 --- a/packages/hub/src/components/layout/RootLayout/PageMenu.tsx +++ b/packages/hub/src/components/layout/RootLayout/PageMenu.tsx @@ -19,7 +19,7 @@ import { import { SQUIGGLE_DOCS_URL } from "@/lib/common"; import { aboutRoute, aiRoute, newModelRoute } from "@/routes"; -import { GroupCardData } from "@/server/groups/data"; +import { GroupCardDTO } from "@/server/groups/data/card"; import { Paginated } from "@/server/types"; import { GlobalSearch } from "../../GlobalSearch"; @@ -61,7 +61,7 @@ const NewModelMenuLink: FC = (props) => { }; type MenuProps = { - groups: Paginated; + groups: Paginated; session: Session | null; }; diff --git a/packages/hub/src/components/layout/RootLayout/index.tsx b/packages/hub/src/components/layout/RootLayout/index.tsx index 2ad046f957..8f8484eb7b 100644 --- a/packages/hub/src/components/layout/RootLayout/index.tsx +++ b/packages/hub/src/components/layout/RootLayout/index.tsx @@ -2,7 +2,7 @@ import { FC, PropsWithChildren, Suspense } from "react"; import { auth } from "@/auth"; import { Link } from "@/components/ui/Link"; -import { loadGroupCards } from "@/server/groups/data"; +import { loadGroupCards } from "@/server/groups/data/card"; import { ReactRoot } from "../../ReactRoot"; import { PageFooterIfNecessary } from "./PageFooterIfNecessary"; diff --git a/packages/hub/src/graphql/mutations/addUserToGroup.ts b/packages/hub/src/graphql/mutations/addUserToGroup.ts index 94b7dfb339..1f7780ce35 100644 --- a/packages/hub/src/graphql/mutations/addUserToGroup.ts +++ b/packages/hub/src/graphql/mutations/addUserToGroup.ts @@ -6,7 +6,6 @@ import { builder } from "../builder"; import { MembershipRoleType, UserGroupMembership } from "../types/Group"; import { validateSlug } from "../utils"; -// Adapted from `inviteUserToGroup`. builder.mutationField("addUserToGroup", (t) => t.withAuth({ signedIn: true }).fieldWithInput({ type: builder.simpleObject("AddUserToGroupResult", { diff --git a/packages/hub/src/graphql/mutations/inviteUserToGroup.ts b/packages/hub/src/graphql/mutations/inviteUserToGroup.ts deleted file mode 100644 index 834e058e76..0000000000 --- a/packages/hub/src/graphql/mutations/inviteUserToGroup.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { ZodError } from "zod"; - -import { prisma } from "@/prisma"; - -import { builder } from "../builder"; -import { MembershipRoleType } from "../types/Group"; -import { GroupInvite } from "../types/GroupInvite"; -import { validateSlug } from "../utils"; - -builder.mutationField("inviteUserToGroup", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("InviteUserToGroupResult", { - fields: (t) => ({ - invite: t.field({ type: GroupInvite }), - }), - }), - errors: { types: [ZodError] }, - input: { - group: t.input.string({ required: true, validate: validateSlug }), - username: t.input.string({ required: true, validate: validateSlug }), - role: t.input.field({ - type: MembershipRoleType, - required: true, - }), - }, - resolve: async (_, { input }, { session }) => { - const invite = await prisma.$transaction(async (tx) => { - const groupOwner = await tx.owner.findUnique({ - where: { - slug: input.group, - }, - }); - if (!groupOwner) { - throw new Error(`Group ${input.group} not found`); - } - - const invitedUser = await tx.user.findFirst({ - where: { - asOwner: { - slug: input.username, - }, - }, - }); - - if (!invitedUser) { - throw new Error(`Invited user ${input.username} not found`); - } - - // We perform all checks one by one because that allows more precise error reporting. - // (It would be possible to check everything in one big query with clever nested `connect` checks.) - const isAdmin = await tx.group.count({ - where: { - ownerId: groupOwner.id, - memberships: { - some: { - user: { email: session.user.email }, - role: "Admin", - }, - }, - }, - }); - if (!isAdmin) { - throw new Error(`You're not an admin of ${input.group} group`); - } - - const alreadyAMember = await tx.group.count({ - where: { - ownerId: groupOwner.id, - memberships: { - some: { userId: invitedUser.id }, - }, - }, - }); - if (alreadyAMember) { - throw new Error( - `${input.username} is already a member of ${input.group}` - ); - } - - const hasPendingInvite = await tx.group.count({ - where: { - ownerId: groupOwner.id, - invites: { - some: { - userId: invitedUser.id, - status: "Pending", - }, - }, - }, - }); - if (hasPendingInvite) { - throw new Error( - `There's already a pending invite for ${input.username} to join ${input.group}` - ); - } - - return await tx.groupInvite.create({ - data: { - user: { - connect: { id: invitedUser.id }, - }, - group: { - connect: { ownerId: groupOwner.id }, - }, - role: input.role, - }, - }); - }); - - return { invite }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/reactToGroupInvite.ts b/packages/hub/src/graphql/mutations/reactToGroupInvite.ts deleted file mode 100644 index dfc296be4a..0000000000 --- a/packages/hub/src/graphql/mutations/reactToGroupInvite.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { prisma } from "@/prisma"; - -import { builder } from "../builder"; -import { UserGroupMembership } from "../types/Group"; -import { GroupInvite } from "../types/GroupInvite"; -import { decodeGlobalIdWithTypename } from "../utils"; - -export const InviteReaction = builder.enumType("GroupInviteReaction", { - values: ["Accept", "Decline"], -}); - -builder.mutationField("reactToGroupInvite", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("ReactToGroupInviteResult", { - fields: (t) => ({ - invite: t.field({ type: GroupInvite }), - membership: t.field({ type: UserGroupMembership, nullable: true }), - }), - }), - errors: {}, - input: { - inviteId: t.input.string({ required: true }), - action: t.input.field({ type: InviteReaction, required: true }), - }, - resolve: async (_, { input }, { session }) => { - // Note: doesn't support email invites yet (which are not implemented anyway) - const decodedInviteId = decodeGlobalIdWithTypename( - input.inviteId, - "UserGroupInvite" - ); - - const newStatus = - input.action === "Accept" - ? "Accepted" - : input.action === "Decline" - ? "Declined" - : ("" as never); - - const { invite, membership } = await prisma.$transaction(async (tx) => { - const invite = await tx.groupInvite.update({ - where: { - id: decodedInviteId, - user: { - email: session.user.email, - }, - status: "Pending", - }, - data: { - status: newStatus, - }, - }); - - const membership = - invite.status === "Accepted" - ? await tx.userGroupMembership.create({ - data: { - group: { - connect: { - id: invite.groupId, - }, - }, - user: { - connect: { - email: session.user.email, - }, - }, - role: invite.role, - }, - }) - : null; - - return { invite, membership }; - }); - - return { invite, membership }; - }, - }) -); diff --git a/packages/hub/src/graphql/schema.ts b/packages/hub/src/graphql/schema.ts index a0f149f812..355791933f 100644 --- a/packages/hub/src/graphql/schema.ts +++ b/packages/hub/src/graphql/schema.ts @@ -28,8 +28,6 @@ import "./mutations/deleteModel"; import "./mutations/deleteRelativeValuesDefinition"; import "./mutations/deleteReusableGroupInviteToken"; import "./mutations/addUserToGroup"; -import "./mutations/inviteUserToGroup"; -import "./mutations/reactToGroupInvite"; import "./mutations/updateGroupInviteRole"; import "./mutations/updateMembershipRole"; import "./mutations/updateRelativeValuesDefinition"; diff --git a/packages/hub/src/groups/components/GroupCard.tsx b/packages/hub/src/groups/components/GroupCard.tsx index 6195d49151..8a62220037 100644 --- a/packages/hub/src/groups/components/GroupCard.tsx +++ b/packages/hub/src/groups/components/GroupCard.tsx @@ -2,10 +2,10 @@ import { FC } from "react"; import { EntityCard, UpdatedStatus } from "@/components/EntityCard"; import { groupRoute } from "@/routes"; -import { GroupCardData } from "@/server/groups/data"; +import { GroupCardDTO } from "@/server/groups/data/card"; type Props = { - group: GroupCardData; + group: GroupCardDTO; }; export const GroupCard: FC = ({ group }) => { diff --git a/packages/hub/src/groups/components/GroupList.tsx b/packages/hub/src/groups/components/GroupList.tsx index 853b8102b1..1a1b9cd1fc 100644 --- a/packages/hub/src/groups/components/GroupList.tsx +++ b/packages/hub/src/groups/components/GroupList.tsx @@ -3,13 +3,13 @@ import { FC } from "react"; import { LoadMore } from "@/components/LoadMore"; import { usePaginator } from "@/hooks/usePaginator"; -import { GroupCardData } from "@/server/groups/data"; +import { GroupCardDTO } from "@/server/groups/data/card"; import { Paginated } from "@/server/types"; import { GroupCard } from "./GroupCard"; type Props = { - page: Paginated; + page: Paginated; }; export const GroupList: FC = ({ page: initialPage }) => { diff --git a/packages/hub/src/server/groups/data.ts b/packages/hub/src/server/groups/data/card.ts similarity index 69% rename from packages/hub/src/server/groups/data.ts rename to packages/hub/src/server/groups/data/card.ts index 988b9ceed0..322da4e42c 100644 --- a/packages/hub/src/server/groups/data.ts +++ b/packages/hub/src/server/groups/data/card.ts @@ -5,37 +5,9 @@ import { Prisma } from "@prisma/client"; import { auth } from "@/auth"; import { prisma } from "@/prisma"; -import { Paginated } from "../types"; +import { Paginated } from "../../types"; -export async function getMyGroup( - groupSlug: string -): Promise { - const session = await auth(); - const userId = session?.user.id; - if (!userId) { - return null; - } - - const group = await prisma.group.findFirst({ - select: groupCardSelect, - where: { - asOwner: { slug: groupSlug }, - memberships: { - some: { userId }, - }, - }, - }); - if (!group) { - return null; - } - return dbGroupToGroupCard(group); -} - -export async function hasGroupMembership(groupSlug: string): Promise { - return !!(await getMyGroup(groupSlug)); -} - -const groupCardSelect = { +const select = { id: true, asOwner: { select: { @@ -46,14 +18,16 @@ const groupCardSelect = { } satisfies Prisma.GroupSelect; type DbGroupCard = NonNullable< - Awaited< - ReturnType< - typeof prisma.group.findFirst<{ select: typeof groupCardSelect }> - > - > + Awaited>> >; -export function dbGroupToGroupCard(dbGroup: DbGroupCard) { +export type GroupCardDTO = { + id: string; + slug: string; + updatedAt: Date; +}; + +export function toDTO(dbGroup: DbGroupCard): GroupCardDTO { return { id: dbGroup.id, slug: dbGroup.asOwner.slug, @@ -61,19 +35,17 @@ export function dbGroupToGroupCard(dbGroup: DbGroupCard) { }; } -export type GroupCardData = ReturnType; - export async function loadGroupCards( params: { username?: string; cursor?: string; limit?: number; } = {} -): Promise> { +): Promise> { const limit = params.limit ?? 20; const dbGroups = await prisma.group.findMany({ - select: groupCardSelect, + select: select, orderBy: { updatedAt: "desc" }, cursor: params.cursor ? { id: params.cursor } : undefined, where: { @@ -90,7 +62,7 @@ export async function loadGroupCards( take: limit + 1, }); - const groups = dbGroups.map(dbGroupToGroupCard); + const groups = dbGroups.map(toDTO); const nextCursor = groups[groups.length - 1]?.id; @@ -104,3 +76,37 @@ export async function loadGroupCards( loadMore: groups.length > limit ? loadMore : undefined, }; } + +export async function loadGroupCard( + groupSlug: string +): Promise { + const group = await prisma.group.findFirst({ + select, + where: { asOwner: { slug: groupSlug } }, + }); + return group ? toDTO(group) : null; +} + +export async function getMyGroup( + groupSlug: string +): Promise { + const session = await auth(); + const userId = session?.user.id; + if (!userId) { + return null; + } + + const group = await prisma.group.findFirst({ + select, + where: { + asOwner: { slug: groupSlug }, + memberships: { + some: { userId }, + }, + }, + }); + if (!group) { + return null; + } + return toDTO(group); +} diff --git a/packages/hub/src/server/groups/data/helpers.ts b/packages/hub/src/server/groups/data/helpers.ts new file mode 100644 index 0000000000..5035e481ba --- /dev/null +++ b/packages/hub/src/server/groups/data/helpers.ts @@ -0,0 +1,46 @@ +import { MembershipRole } from "@prisma/client"; + +import { auth } from "@/auth"; +import { prisma } from "@/prisma"; + +import { getMyGroup } from "./card"; + +export async function hasGroupMembership(groupSlug: string): Promise { + // TODO - could be optimized + return !!(await getMyGroup(groupSlug)); +} + +export type GroupInviteDTO = { + id: string; + role: MembershipRole; +}; + +export async function loadInviteForMe( + groupSlug: string +): Promise { + const session = await auth(); + if (!session?.user.email) { + return null; + } + + const invite = await prisma.groupInvite.findFirst({ + select: { + id: true, + role: true, + }, + where: { + group: { asOwner: { slug: groupSlug } }, + user: { email: session.user.email }, + status: "Pending", + }, + }); + + if (!invite) { + return null; + } + + return { + id: invite.id, + role: invite.role, + }; +} From 9e767c6cd1bdea5788c72568752aaee307a80a48 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 28 Nov 2024 16:12:54 -0300 Subject: [PATCH 31/68] invite mutations -> actions --- .../invite-link/AcceptGroupInvitePage.tsx | 101 +++--------------- .../app/groups/[slug]/invite-link/page.tsx | 44 +++++--- .../src/components/ui/ServerActionButton.tsx | 31 ++++++ .../acceptReusableGroupInviteToken.ts | 59 ---------- .../validateReusableGroupInviteToken.ts | 33 ------ packages/hub/src/graphql/schema.ts | 2 - .../acceptReusableGroupInviteTokenAction.ts | 61 +++++++++++ .../hub/src/server/groups/data/helpers.ts | 22 ++++ 8 files changed, 159 insertions(+), 194 deletions(-) create mode 100644 packages/hub/src/components/ui/ServerActionButton.tsx delete mode 100644 packages/hub/src/graphql/mutations/acceptReusableGroupInviteToken.ts delete mode 100644 packages/hub/src/graphql/mutations/validateReusableGroupInviteToken.ts create mode 100644 packages/hub/src/server/groups/actions/acceptReusableGroupInviteTokenAction.ts diff --git a/packages/hub/src/app/groups/[slug]/invite-link/AcceptGroupInvitePage.tsx b/packages/hub/src/app/groups/[slug]/invite-link/AcceptGroupInvitePage.tsx index 164aa2f5de..88db42617b 100644 --- a/packages/hub/src/app/groups/[slug]/invite-link/AcceptGroupInvitePage.tsx +++ b/packages/hub/src/app/groups/[slug]/invite-link/AcceptGroupInvitePage.tsx @@ -1,108 +1,35 @@ "use client"; -import { useRouter, useSearchParams } from "next/navigation"; -import { FC, useEffect } from "react"; -import { graphql } from "relay-runtime"; +import { useRouter } from "next/navigation"; +import { FC } from "react"; import { useToast } from "@quri/ui"; -import { MutationButton } from "@/components/ui/MutationButton"; -import { useAsyncMutation } from "@/hooks/useAsyncMutation"; +import { ServerActionButton } from "@/components/ui/ServerActionButton"; import { groupRoute } from "@/routes"; +import { acceptReusableGroupInviteTokenAction } from "@/server/groups/actions/acceptReusableGroupInviteTokenAction"; import { GroupCardDTO } from "@/server/groups/data/card"; -import { AcceptGroupInvitePage_ValidateMutation } from "@/__generated__/AcceptGroupInvitePage_ValidateMutation.graphql"; -import { AcceptGroupInvitePageMutation } from "@/__generated__/AcceptGroupInvitePageMutation.graphql"; - export const AcceptGroupInvitePage: FC<{ group: GroupCardDTO; -}> = ({ group }) => { - const params = useSearchParams(); - const inviteToken = params.get("token"); - if (!inviteToken) { - throw new Error("Token is missing"); - } - - const [validateMutation] = useAsyncMutation< - AcceptGroupInvitePage_ValidateMutation, - "ValidateReusableGroupInviteTokenResult" - >({ - mutation: graphql` - mutation AcceptGroupInvitePage_ValidateMutation( - $input: MutationValidateReusableGroupInviteTokenInput! - ) { - result: validateReusableGroupInviteToken(input: $input) { - __typename - ... on BaseError { - message - } - ... on ValidateReusableGroupInviteTokenResult { - ok - } - } - } - `, - expectedTypename: "ValidateReusableGroupInviteTokenResult", - }); - + inviteToken: string; +}> = ({ group, inviteToken }) => { const toast = useToast(); const router = useRouter(); - useEffect(() => { - validateMutation({ - variables: { - input: { - groupSlug: group.slug, - inviteToken, - }, - }, - onCompleted({ ok }) { - if (!ok) { - toast("Invalid token", "error"); - router.replace(groupRoute({ slug: group.slug })); - } - }, - }); - }, []); - return (

{`You've been invited to join ${group.slug} group.`}

- - mutation={graphql` - mutation AcceptGroupInvitePageMutation( - $input: MutationAcceptReusableGroupInviteTokenInput! - ) { - result: acceptReusableGroupInviteToken(input: $input) { - __typename - ... on BaseError { - message - } - ... on AcceptReusableGroupInviteTokenResult { - __typename - membership { - group { - id - slug - ...hooks_useIsGroupMember - } - } - } - } - } - `} - expectedTypename="AcceptReusableGroupInviteTokenResult" - variables={{ - input: { + { + await acceptReusableGroupInviteTokenAction({ groupSlug: group.slug, inviteToken, - }, + }); + toast("Joined", "confirmation"); + router.push(groupRoute({ slug: group.slug })); }} - title="Join this group" - theme="primary" - onCompleted={() => toast("Joined", "confirmation")} />
); diff --git a/packages/hub/src/app/groups/[slug]/invite-link/page.tsx b/packages/hub/src/app/groups/[slug]/invite-link/page.tsx index c2cd51d517..26f56aca00 100644 --- a/packages/hub/src/app/groups/[slug]/invite-link/page.tsx +++ b/packages/hub/src/app/groups/[slug]/invite-link/page.tsx @@ -1,26 +1,29 @@ import { notFound, redirect } from "next/navigation"; +import { z } from "zod"; import { WithAuth } from "@/components/WithAuth"; -import { loadPageQuery } from "@/relay/loadPageQuery"; import { groupRoute } from "@/routes"; import { loadGroupCard } from "@/server/groups/data/card"; -import { hasGroupMembership } from "@/server/groups/data/helpers"; +import { + hasGroupMembership, + validateReusableGroupInviteToken, +} from "@/server/groups/data/helpers"; import { AcceptGroupInvitePage } from "./AcceptGroupInvitePage"; -import QueryNode, { - AcceptGroupInvitePageQuery, -} from "@/__generated__/AcceptGroupInvitePageQuery.graphql"; - type Props = { params: Promise<{ slug: string }>; + searchParams: Promise<{ token: string }>; }; -async function InnerPage({ params }: Props) { +async function InnerPage({ params, searchParams }: Props) { const { slug } = await params; - const query = await loadPageQuery(QueryNode, { - slug, - }); + + const { token: inviteToken } = z + .object({ + token: z.string(), + }) + .parse(await searchParams); const group = await loadGroupCard(slug); if (!group) { @@ -31,13 +34,28 @@ async function InnerPage({ params }: Props) { redirect(groupRoute({ slug: group.slug })); } - return ; + const isValidToken = await validateReusableGroupInviteToken({ + groupSlug: group.slug, + inviteToken, + }); + + if (!isValidToken) { + // FIXME - copy-pasted from ErrorBoudnary + return ( +
+
Error
+
Invalid invite token.
+
+ ); + } + + return ; } -export default async function ({ params }: Props) { +export default async function (props: Props) { return ( - + ); } diff --git a/packages/hub/src/components/ui/ServerActionButton.tsx b/packages/hub/src/components/ui/ServerActionButton.tsx new file mode 100644 index 0000000000..4665b3b155 --- /dev/null +++ b/packages/hub/src/components/ui/ServerActionButton.tsx @@ -0,0 +1,31 @@ +import { ReactNode, useActionState } from "react"; + +import { Button } from "@quri/ui"; + +/* + * Props for this component include: + * - some props that are passed to ` + + ); +} diff --git a/packages/hub/src/graphql/mutations/acceptReusableGroupInviteToken.ts b/packages/hub/src/graphql/mutations/acceptReusableGroupInviteToken.ts deleted file mode 100644 index ce8a0f730c..0000000000 --- a/packages/hub/src/graphql/mutations/acceptReusableGroupInviteToken.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { getMyMembership } from "../helpers/groupHelpers"; -import { UserGroupMembership } from "../types/Group"; - -builder.mutationField("acceptReusableGroupInviteToken", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("AcceptReusableGroupInviteTokenResult", { - fields: (t) => ({ - membership: t.field({ type: UserGroupMembership }), - }), - }), - errors: {}, - input: { - groupSlug: t.input.string({ required: true }), - inviteToken: t.input.string({ required: true }), - }, - resolve: async (_, { input }, { session }) => { - let group = await prisma.group.findFirstOrThrow({ - where: { - asOwner: { - slug: input.groupSlug, - }, - }, - }); - - if (group.reusableInviteToken !== input.inviteToken) { - throw new Error("Invalid token"); - } - - const myMembership = await getMyMembership({ - groupSlug: input.groupSlug, - session, - }); - if (myMembership) { - throw new Error("You're already a member of this group"); - } - - const membership = await prisma.userGroupMembership.create({ - data: { - group: { - connect: { - id: group.id, - }, - }, - user: { - connect: { - email: session.user.email, - }, - }, - role: "Member", - }, - }); - - return { membership }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/validateReusableGroupInviteToken.ts b/packages/hub/src/graphql/mutations/validateReusableGroupInviteToken.ts deleted file mode 100644 index 06e04e9d45..0000000000 --- a/packages/hub/src/graphql/mutations/validateReusableGroupInviteToken.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -// validate without accepting, for frontend checks -builder.mutationField("validateReusableGroupInviteToken", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("ValidateReusableGroupInviteTokenResult", { - fields: (t) => ({ - ok: t.boolean(), - }), - }), - errors: {}, - input: { - groupSlug: t.input.string({ required: true }), - inviteToken: t.input.string({ required: true }), - }, - resolve: async (_, { input }, { session }) => { - let group = await prisma.group.findFirstOrThrow({ - where: { - asOwner: { - slug: input.groupSlug, - }, - }, - }); - - if (group.reusableInviteToken !== input.inviteToken) { - return { ok: false }; - } - - return { ok: true }; - }, - }) -); diff --git a/packages/hub/src/graphql/schema.ts b/packages/hub/src/graphql/schema.ts index 355791933f..484f6d8b14 100644 --- a/packages/hub/src/graphql/schema.ts +++ b/packages/hub/src/graphql/schema.ts @@ -14,7 +14,6 @@ import "./queries/relativeValuesDefinitions"; import "./queries/runSquiggle"; import "./queries/search"; import "./queries/userByUsername"; -import "./mutations/acceptReusableGroupInviteToken"; import "./mutations/adminUpdateModelVersion"; import "./mutations/adminRebuildSearchIndex"; import "./mutations/buildRelativeValuesCache"; @@ -32,7 +31,6 @@ import "./mutations/updateGroupInviteRole"; import "./mutations/updateMembershipRole"; import "./mutations/updateRelativeValuesDefinition"; import "./mutations/updateSquiggleSnippetModel"; -import "./mutations/validateReusableGroupInviteToken"; import { builder } from "./builder"; diff --git a/packages/hub/src/server/groups/actions/acceptReusableGroupInviteTokenAction.ts b/packages/hub/src/server/groups/actions/acceptReusableGroupInviteTokenAction.ts new file mode 100644 index 0000000000..d091273314 --- /dev/null +++ b/packages/hub/src/server/groups/actions/acceptReusableGroupInviteTokenAction.ts @@ -0,0 +1,61 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { z } from "zod"; + +import { getMyMembership } from "@/graphql/helpers/groupHelpers"; +import { prisma } from "@/prisma"; +import { groupMembersRoute } from "@/routes"; +import { getSessionOrRedirect } from "@/server/users/auth"; +import { makeServerAction, zSlug } from "@/server/utils"; + +import { validateReusableGroupInviteToken } from "../data/helpers"; + +export const acceptReusableGroupInviteTokenAction = makeServerAction( + z.object({ + groupSlug: zSlug, + inviteToken: z.string(), + }), + async (input) => { + const session = await getSessionOrRedirect(); + + if (!(await validateReusableGroupInviteToken(input))) { + throw new Error("Invalid token"); + } + + const group = await prisma.group.findFirstOrThrow({ + select: { + id: true, + }, + where: { + asOwner: { slug: input.groupSlug }, + }, + }); + + const myMembership = await getMyMembership({ + groupSlug: input.groupSlug, + session, + }); + if (myMembership) { + throw new Error("You're already a member of this group"); + } + + await prisma.userGroupMembership.create({ + data: { + group: { + connect: { + id: group.id, + }, + }, + user: { + connect: { + email: session.user.email, + }, + }, + role: "Member", + }, + }); + + revalidatePath(groupMembersRoute({ slug: input.groupSlug })); + } +); diff --git a/packages/hub/src/server/groups/data/helpers.ts b/packages/hub/src/server/groups/data/helpers.ts index 5035e481ba..b205cc13fa 100644 --- a/packages/hub/src/server/groups/data/helpers.ts +++ b/packages/hub/src/server/groups/data/helpers.ts @@ -2,6 +2,7 @@ import { MembershipRole } from "@prisma/client"; import { auth } from "@/auth"; import { prisma } from "@/prisma"; +import { getSessionUserOrRedirect } from "@/server/users/auth"; import { getMyGroup } from "./card"; @@ -44,3 +45,24 @@ export async function loadInviteForMe( role: invite.role, }; } + +export async function validateReusableGroupInviteToken(input: { + groupSlug: string; + inviteToken: string; +}) { + await getSessionUserOrRedirect(); + + const group = await prisma.group.findFirstOrThrow({ + where: { + asOwner: { + slug: input.groupSlug, + }, + }, + }); + + if (group.reusableInviteToken !== input.inviteToken) { + return false; + } + + return true; +} From 2162e3166e1141a63a81aa65ce0a9182d03117b9 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 28 Nov 2024 16:24:55 -0300 Subject: [PATCH 32/68] migrate deleteModel --- .../[owner]/[slug]/DeleteModelAction.tsx | 51 +++++-------------- .../hub/src/graphql/helpers/modelHelpers.ts | 2 +- .../hub/src/graphql/mutations/deleteModel.tsx | 36 ------------- packages/hub/src/graphql/schema.ts | 1 - .../models/actions/deleteModelAction.ts | 28 ++++++++++ .../models/actions/loadModelCardAction.ts | 22 ++++---- 6 files changed, 55 insertions(+), 85 deletions(-) delete mode 100644 packages/hub/src/graphql/mutations/deleteModel.tsx create mode 100644 packages/hub/src/server/models/actions/deleteModelAction.ts diff --git a/packages/hub/src/app/models/[owner]/[slug]/DeleteModelAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/DeleteModelAction.tsx index 4fdb7059d8..58e9e91d0f 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/DeleteModelAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/DeleteModelAction.tsx @@ -1,26 +1,12 @@ import { useRouter } from "next/navigation"; -import { FC, useCallback } from "react"; -import { useMutation } from "react-relay"; -import { graphql } from "relay-runtime"; +import { FC } from "react"; import { DropdownMenuAsyncActionItem, TrashIcon, useToast } from "@quri/ui"; import { ownerRoute } from "@/routes"; +import { deleteModelAction } from "@/server/models/actions/deleteModelAction"; import { ModelCardDTO } from "@/server/models/data/card"; -import { DeleteModelActionMutation } from "@/__generated__/DeleteModelActionMutation.graphql"; - -const Mutation = graphql` - mutation DeleteModelActionMutation($input: MutationDeleteModelInput!) { - deleteModel(input: $input) { - __typename - ... on BaseError { - message - } - } - } -`; - type Props = { model: ModelCardDTO; close(): void; @@ -29,35 +15,24 @@ type Props = { export const DeleteModelAction: FC = ({ model, close }) => { const router = useRouter(); - const [mutation] = useMutation(Mutation); - const toast = useToast(); - const onClick = useCallback((): Promise => { - return new Promise((resolve) => { - mutation({ - variables: { input: { owner: model.owner.slug, slug: model.slug } }, - onCompleted(response) { - if (response.deleteModel.__typename === "BaseError") { - toast(response.deleteModel.message, "error"); - resolve(); - } else { - // TODO - this is risky, what if we add more error types to GraphQL schema? - router.push(ownerRoute(model.owner)); - } - }, - onError(e) { - toast(e.toString(), "error"); - resolve(); - }, + const act = async () => { + try { + await deleteModelAction({ + owner: model.owner.slug, + slug: model.slug, }); - }); - }, [mutation, model.owner, model.slug, router, toast]); + router.push(ownerRoute(model.owner)); + } catch (e) { + toast(String(e), "error"); + } + }; return ( diff --git a/packages/hub/src/graphql/helpers/modelHelpers.ts b/packages/hub/src/graphql/helpers/modelHelpers.ts index f32d5deab8..e7a59651a9 100644 --- a/packages/hub/src/graphql/helpers/modelHelpers.ts +++ b/packages/hub/src/graphql/helpers/modelHelpers.ts @@ -38,7 +38,7 @@ export async function getWriteableModel({ slug, include, }: { - session: Session; + session: Session; // FIXME - SignedInSession? owner: string; slug: string; include?: Prisma.ModelInclude; diff --git a/packages/hub/src/graphql/mutations/deleteModel.tsx b/packages/hub/src/graphql/mutations/deleteModel.tsx deleted file mode 100644 index d903c53818..0000000000 --- a/packages/hub/src/graphql/mutations/deleteModel.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { ZodError } from "zod"; - -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { NotFoundError } from "../errors/NotFoundError"; -import { getWriteableModel } from "../helpers/modelHelpers"; -import { validateSlug } from "../utils"; - -builder.mutationField("deleteModel", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("DeleteModelResult", { - fields: (t) => ({ - ok: t.boolean(), - }), - }), - input: { - owner: t.input.string({ required: true, validate: validateSlug }), - slug: t.input.string({ required: true, validate: validateSlug }), - }, - errors: { types: [ZodError, NotFoundError] }, - async resolve(_, { input }, { session }) { - const model = await getWriteableModel({ - slug: input.slug, - owner: input.owner, - session, - }); - - await prisma.model.delete({ - where: { id: model.id }, - }); - - return { ok: true }; - }, - }) -); diff --git a/packages/hub/src/graphql/schema.ts b/packages/hub/src/graphql/schema.ts index 484f6d8b14..3bf95f7e97 100644 --- a/packages/hub/src/graphql/schema.ts +++ b/packages/hub/src/graphql/schema.ts @@ -23,7 +23,6 @@ import "./mutations/createGroup"; import "./mutations/createRelativeValuesDefinition"; import "./mutations/createReusableGroupInviteToken"; import "./mutations/deleteMembership"; -import "./mutations/deleteModel"; import "./mutations/deleteRelativeValuesDefinition"; import "./mutations/deleteReusableGroupInviteToken"; import "./mutations/addUserToGroup"; diff --git a/packages/hub/src/server/models/actions/deleteModelAction.ts b/packages/hub/src/server/models/actions/deleteModelAction.ts new file mode 100644 index 0000000000..e218090b02 --- /dev/null +++ b/packages/hub/src/server/models/actions/deleteModelAction.ts @@ -0,0 +1,28 @@ +"use server"; + +import { z } from "zod"; + +import { getWriteableModel } from "@/graphql/helpers/modelHelpers"; +import { prisma } from "@/prisma"; +import { getSessionOrRedirect } from "@/server/users/auth"; +import { makeServerAction, zSlug } from "@/server/utils"; + +export const deleteModelAction = makeServerAction( + z.object({ + owner: zSlug, + slug: zSlug, + }), + async (input) => { + const session = await getSessionOrRedirect(); + + const model = await getWriteableModel({ + slug: input.slug, + owner: input.owner, + session, + }); + + await prisma.model.delete({ + where: { id: model.id }, + }); + } +); diff --git a/packages/hub/src/server/models/actions/loadModelCardAction.ts b/packages/hub/src/server/models/actions/loadModelCardAction.ts index d4e55f1154..b5b7b80a2a 100644 --- a/packages/hub/src/server/models/actions/loadModelCardAction.ts +++ b/packages/hub/src/server/models/actions/loadModelCardAction.ts @@ -1,13 +1,17 @@ "use server"; +import { z } from "zod"; + +import { makeServerAction, zSlug } from "@/server/utils"; + import { loadModelCard, ModelCardDTO } from "../data/card"; // data-fetching action, used in ImportTooltip -export async function loadModelCardAction({ - owner, - slug, -}: { - owner: string; - slug: string; -}): Promise { - return loadModelCard({ owner, slug }); -} +export const loadModelCardAction = makeServerAction( + z.object({ + owner: zSlug, + slug: zSlug, + }), + async ({ owner, slug }): Promise => { + return loadModelCard({ owner, slug }); + } +); From 5872983eff9de41e6faeb6e30791c0e1c67a4919 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 28 Nov 2024 16:32:07 -0300 Subject: [PATCH 33/68] migrate model view page --- .../[owner]/[slug]/view/ViewModelPage.tsx | 48 ------------------- .../[slug]/view/ViewSquiggleSnippet.tsx | 26 ++++++++++ .../app/models/[owner]/[slug]/view/layout.tsx | 5 ++ .../app/models/[owner]/[slug]/view/page.tsx | 37 ++++++++------ packages/hub/src/server/models/data/card.ts | 1 + .../components/ViewSquiggleSnippet.tsx | 37 -------------- 6 files changed, 54 insertions(+), 100 deletions(-) delete mode 100644 packages/hub/src/app/models/[owner]/[slug]/view/ViewModelPage.tsx create mode 100644 packages/hub/src/app/models/[owner]/[slug]/view/ViewSquiggleSnippet.tsx create mode 100644 packages/hub/src/app/models/[owner]/[slug]/view/layout.tsx delete mode 100644 packages/hub/src/squiggle/components/ViewSquiggleSnippet.tsx diff --git a/packages/hub/src/app/models/[owner]/[slug]/view/ViewModelPage.tsx b/packages/hub/src/app/models/[owner]/[slug]/view/ViewModelPage.tsx deleted file mode 100644 index 12d41fe61c..0000000000 --- a/packages/hub/src/app/models/[owner]/[slug]/view/ViewModelPage.tsx +++ /dev/null @@ -1,48 +0,0 @@ -"use client"; - -import { FC } from "react"; -import { graphql } from "react-relay"; - -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; -import { ViewSquiggleSnippet } from "@/squiggle/components/ViewSquiggleSnippet"; - -import { ViewModelPageQuery } from "@/__generated__/ViewModelPageQuery.graphql"; - -export const ViewModelPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [{ model: result }] = usePageQuery( - graphql` - query ViewModelPageQuery($input: QueryModelInput!) { - model(input: $input) { - __typename - ... on Model { - id - slug - currentRevision { - content { - ...ViewSquiggleSnippet - __typename - } - } - } - } - } - `, - query - ); - - const model = extractFromGraphqlErrorUnion(result, "Model"); - - const content = model.currentRevision.content; - const typename = content.__typename; - - switch (typename) { - case "SquiggleSnippet": - return ; - default: - return
Unknown model type {typename}
; - } -}; diff --git a/packages/hub/src/app/models/[owner]/[slug]/view/ViewSquiggleSnippet.tsx b/packages/hub/src/app/models/[owner]/[slug]/view/ViewSquiggleSnippet.tsx new file mode 100644 index 0000000000..cc5c46a77f --- /dev/null +++ b/packages/hub/src/app/models/[owner]/[slug]/view/ViewSquiggleSnippet.tsx @@ -0,0 +1,26 @@ +"use client"; +import { FC, use, useMemo } from "react"; + +import { + useAdjustSquiggleVersion, + versionedSquigglePackages, +} from "@quri/versioned-squiggle-components"; + +import { ModelCardDTO } from "@/server/models/data/card"; +import { sqProjectWithHubLinker } from "@/squiggle/components/linker"; + +type Props = { + data: NonNullable; +}; + +export const ViewSquiggleSnippet: FC = ({ data }) => { + const { version, code } = data; + + const checkedVersion = useAdjustSquiggleVersion(version); + + const squiggle = use(versionedSquigglePackages(checkedVersion)); + + const project = useMemo(() => sqProjectWithHubLinker(squiggle), [squiggle]); + + return ; +}; diff --git a/packages/hub/src/app/models/[owner]/[slug]/view/layout.tsx b/packages/hub/src/app/models/[owner]/[slug]/view/layout.tsx new file mode 100644 index 0000000000..3054ff2a9e --- /dev/null +++ b/packages/hub/src/app/models/[owner]/[slug]/view/layout.tsx @@ -0,0 +1,5 @@ +import { PropsWithChildren } from "react"; + +export default function Layout({ children }: PropsWithChildren) { + return
{children}
; +} diff --git a/packages/hub/src/app/models/[owner]/[slug]/view/page.tsx b/packages/hub/src/app/models/[owner]/[slug]/view/page.tsx index 5ea3b4bbfa..c6ea463f97 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/view/page.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/view/page.tsx @@ -1,24 +1,31 @@ -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { notFound } from "next/navigation"; -import { ViewModelPage } from "./ViewModelPage"; - -import QueryNode, { - ViewModelPageQuery, -} from "@/__generated__/ViewModelPageQuery.graphql"; +import { ViewSquiggleSnippet } from "@/app/models/[owner]/[slug]/view/ViewSquiggleSnippet"; +import { loadModelCard } from "@/server/models/data/card"; type Props = { params: Promise<{ owner: string; slug: string }>; }; -export default async function OuterModelPage({ params }: Props) { +// Note: this page is currently unused, we used it a long time ago. +// Views are now per-variable, not per-model. +export default async function ViewModelPage({ params }: Props) { const { owner, slug } = await params; - const query = await loadPageQuery(QueryNode, { - input: { owner, slug }, - }); + const model = await loadModelCard({ owner, slug }); + if (!model) { + notFound(); + } + const currentRevision = model.currentRevision; + + switch (currentRevision.contentType) { + case "SquiggleSnippet": { + if (!currentRevision.squiggleSnippet) { + throw new Error("No squiggle snippet"); + } - return ( -
- -
- ); + return ; + } + default: + return
Unknown model type {currentRevision.contentType}
; + } } diff --git a/packages/hub/src/server/models/data/card.ts b/packages/hub/src/server/models/data/card.ts index e91bc9f785..ababf08ac0 100644 --- a/packages/hub/src/server/models/data/card.ts +++ b/packages/hub/src/server/models/data/card.ts @@ -75,6 +75,7 @@ const modelCardSelect = { select: { id: true, code: true, + version: true, }, }, relativeValuesExports: { diff --git a/packages/hub/src/squiggle/components/ViewSquiggleSnippet.tsx b/packages/hub/src/squiggle/components/ViewSquiggleSnippet.tsx deleted file mode 100644 index 0fee27e165..0000000000 --- a/packages/hub/src/squiggle/components/ViewSquiggleSnippet.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { FC, use, useMemo } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { - useAdjustSquiggleVersion, - versionedSquigglePackages, -} from "@quri/versioned-squiggle-components"; - -import { sqProjectWithHubLinker } from "./linker"; - -import { ViewSquiggleSnippet$key } from "@/__generated__/ViewSquiggleSnippet.graphql"; - -type Props = { - dataRef: ViewSquiggleSnippet$key; -}; - -export const ViewSquiggleSnippet: FC = ({ dataRef }) => { - const { version, code } = useFragment( - graphql` - fragment ViewSquiggleSnippet on SquiggleSnippet { - id - code - version - } - `, - dataRef - ); - - const checkedVersion = useAdjustSquiggleVersion(version); - - const squiggle = use(versionedSquigglePackages(checkedVersion)); - - const project = useMemo(() => sqProjectWithHubLinker(squiggle), [squiggle]); - - return ; -}; From ddfbc5c1b692a9c1b0687bddbba5a20ac20df44a Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 28 Nov 2024 17:48:15 -0300 Subject: [PATCH 34/68] model edit page, /admin/upgrade-versions --- .../upgrade-versions/UpgradeVersionsPage.tsx | 62 ++----- .../upgrade-versions/UpgradeableModel.tsx | 109 +++++------- .../src/app/admin/upgrade-versions/page.tsx | 15 +- .../models/[owner]/[slug]/EditModelPage.tsx | 49 ------ .../[slug]/EditSquiggleSnippetModel.tsx | 82 ++------- .../[slug]/SquiggleSnippetDraftDialog.tsx | 21 +-- .../src/app/models/[owner]/[slug]/page.tsx | 33 ++-- .../exports/EditRelativeValueExports.tsx | 27 +-- packages/hub/src/graphql/queries/groups.ts | 59 ------- .../src/graphql/queries/modelsByVersion.ts | 65 ------- packages/hub/src/graphql/queries/variables.ts | 51 ------ packages/hub/src/graphql/schema.ts | 4 - .../models/actions/loadModelCardAction.ts | 3 +- .../models/actions/loadModelFullAction.ts | 18 ++ .../hub/src/server/models/data/byVersion.ts | 53 ++++++ packages/hub/src/server/models/data/card.ts | 15 +- packages/hub/src/server/models/data/full.ts | 164 ++++++++++++++++++ .../hub/src/server/models/data/helpers.ts | 32 +--- packages/hub/src/server/owners/auth.ts | 33 ++++ 19 files changed, 379 insertions(+), 516 deletions(-) delete mode 100644 packages/hub/src/app/models/[owner]/[slug]/EditModelPage.tsx delete mode 100644 packages/hub/src/graphql/queries/groups.ts delete mode 100644 packages/hub/src/graphql/queries/modelsByVersion.ts delete mode 100644 packages/hub/src/graphql/queries/variables.ts create mode 100644 packages/hub/src/server/models/actions/loadModelFullAction.ts create mode 100644 packages/hub/src/server/models/data/byVersion.ts create mode 100644 packages/hub/src/server/models/data/full.ts create mode 100644 packages/hub/src/server/owners/auth.ts diff --git a/packages/hub/src/app/admin/upgrade-versions/UpgradeVersionsPage.tsx b/packages/hub/src/app/admin/upgrade-versions/UpgradeVersionsPage.tsx index 31b8f6ba6d..16b9380c87 100644 --- a/packages/hub/src/app/admin/upgrade-versions/UpgradeVersionsPage.tsx +++ b/packages/hub/src/app/admin/upgrade-versions/UpgradeVersionsPage.tsx @@ -1,6 +1,5 @@ "use client"; import { FC, useState } from "react"; -import { useFragment } from "react-relay"; import { graphql } from "relay-runtime"; import { @@ -14,35 +13,16 @@ import { defaultSquiggleVersion } from "@quri/versioned-squiggle-components"; import { H2 } from "@/components/ui/Headers"; import { MutationButton } from "@/components/ui/MutationButton"; import { StyledLink } from "@/components/ui/StyledLink"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; import { modelRoute } from "@/routes"; +import { ModelByVersion } from "@/server/models/data/byVersion"; import { UpgradeableModel } from "./UpgradeableModel"; -import { UpgradeVersionsPage_List$key } from "@/__generated__/UpgradeVersionsPage_List.graphql"; import { UpgradeVersionsPage_updateMutation } from "@/__generated__/UpgradeVersionsPage_updateMutation.graphql"; -import { UpgradeVersionsPageQuery } from "@/__generated__/UpgradeVersionsPageQuery.graphql"; const ModelList: FC<{ - modelsRef: UpgradeVersionsPage_List$key; - reload: () => void; -}> = ({ modelsRef, reload }) => { - const models = useFragment( - graphql` - fragment UpgradeVersionsPage_List on Model @relay(plural: true) { - id - slug - owner { - id - slug - } - ...UpgradeableModel_Ref - } - `, - modelsRef - ); - + models: ModelByVersion["models"]; +}> = ({ models }) => { const [pos, setPos] = useState(0); if (!models.length) return null; @@ -85,11 +65,8 @@ const ModelList: FC<{ } } `} - updater={(store) => { - // reload() from usePageQuery doesn't work for some reason - store.get(model.id)?.invalidateRecord(); - reload(); - // window.location.reload(); + updater={() => { + window.location.reload(); }} variables={{ input: { @@ -110,32 +87,15 @@ const ModelList: FC<{ Next →
- +
); }; export const UpgradeVersionsPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - // TODO - this fetches all models even if we show just one, can we optimize it? - const [{ modelsByVersion }, { reload }] = usePageQuery( - graphql` - query UpgradeVersionsPageQuery { - modelsByVersion { - version - count - privateCount - models { - ...UpgradeVersionsPage_List - } - } - } - `, - query - ); - - type Entry = (typeof upgradeableModelsByVersion)[number]; + modelsByVersion: ModelByVersion[]; +}> = ({ modelsByVersion }) => { + type Entry = (typeof modelsByVersion)[number]; const upgradeableModelsByVersion = modelsByVersion.filter( (entry) => @@ -206,9 +166,7 @@ export const UpgradeVersionsPage: FC<{
- {selectedEntry ? ( - - ) : null} + {selectedEntry ? : null}
); }; diff --git a/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx b/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx index 6fe6f3d8e4..3033bc2d17 100644 --- a/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx +++ b/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx @@ -1,7 +1,6 @@ "use client"; -import { FC, use } from "react"; -import { useFragment, useLazyLoadQuery } from "react-relay"; -import { graphql } from "relay-runtime"; +import { FC, use, useEffect, useState } from "react"; +import Skeleton from "react-loading-skeleton"; import { defaultSquiggleVersion, @@ -11,69 +10,25 @@ import { } from "@quri/versioned-squiggle-components"; import { EditSquiggleSnippetModel } from "@/app/models/[owner]/[slug]/EditSquiggleSnippetModel"; -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; +import { loadModelFullAction } from "@/server/models/actions/loadModelFullAction"; +import { ModelByVersion } from "@/server/models/data/byVersion"; +import { ModelFullDTO } from "@/server/models/data/full"; import { sqProjectWithHubLinker } from "@/squiggle/components/linker"; -import { UpgradeableModel_Ref$key } from "@/__generated__/UpgradeableModel_Ref.graphql"; -import { UpgradeableModelQuery } from "@/__generated__/UpgradeableModelQuery.graphql"; - -export const UpgradeableModel: FC<{ - modelRef: UpgradeableModel_Ref$key; -}> = ({ modelRef }) => { - const incompleteModel = useFragment( - graphql` - fragment UpgradeableModel_Ref on Model { - id - slug - owner { - id - slug - } - } - `, - modelRef - ); - - const result = useLazyLoadQuery( - graphql` - query UpgradeableModelQuery($input: QueryModelInput!) { - model(input: $input) { - __typename - ... on Model { - id - currentRevision { - content { - __typename - ... on SquiggleSnippet { - id - code - version - seed - } - } - } - ...EditSquiggleSnippetModel - } - } - } - `, - { - input: { - slug: incompleteModel.slug, - owner: incompleteModel.owner.slug, - }, - } - ); - - const model = extractFromGraphqlErrorUnion(result.model, "Model"); - +const InnerUpgradeableModel: FC<{ + model: ModelFullDTO; +}> = ({ model }) => { const currentRevision = model.currentRevision; - if (currentRevision.content.__typename !== "SquiggleSnippet") { + if (currentRevision.contentType !== "SquiggleSnippet") { throw new Error("Wrong content type"); } - const version = useAdjustSquiggleVersion(currentRevision.content.version); + const code = currentRevision.squiggleSnippet.code; + + const version = useAdjustSquiggleVersion( + currentRevision.squiggleSnippet.version + ); const updatedVersion = defaultSquiggleVersion; const squiggle = use(versionedSquigglePackages(version)); @@ -90,12 +45,9 @@ export const UpgradeableModel: FC<{
{version}
{updatedVersion}
- +
@@ -104,9 +56,36 @@ export const UpgradeableModel: FC<{ return ( ); } }; + +export const UpgradeableModel: FC<{ + model: ModelByVersion["models"][number]; +}> = ({ model: incompleteModel }) => { + const [model, setModel] = useState( + "loading" + ); + + useEffect(() => { + // TODO - this is done with a server action, so it's not cached. + // A route would be better. + loadModelFullAction({ + owner: incompleteModel.owner.slug, + slug: incompleteModel.slug, + }).then(setModel); + }, []); + + if (model === "loading") { + return ; + } + + if (!model) { + return
Model not found
; + } + + return ; +}; diff --git a/packages/hub/src/app/admin/upgrade-versions/page.tsx b/packages/hub/src/app/admin/upgrade-versions/page.tsx index 64291265e5..7b4f182b34 100644 --- a/packages/hub/src/app/admin/upgrade-versions/page.tsx +++ b/packages/hub/src/app/admin/upgrade-versions/page.tsx @@ -1,18 +1,17 @@ import { Metadata } from "next"; -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { loadModelsByVersion } from "@/server/models/data/byVersion"; +import { checkRootUser } from "@/server/users/auth"; import { UpgradeVersionsPage } from "./UpgradeVersionsPage"; -import QueryNode, { - UpgradeVersionsPageQuery, -} from "@/__generated__/UpgradeVersionsPageQuery.graphql"; - export default async function OuterUpgradeVersionsPage() { - // permissions are checked in ./layout.tsx - const query = await loadPageQuery(QueryNode, {}); + await checkRootUser(); + + // TODO - this fetches all models even if we show just one, can we optimize it? + const data = await loadModelsByVersion(); - return ; + return ; } export const metadata: Metadata = { diff --git a/packages/hub/src/app/models/[owner]/[slug]/EditModelPage.tsx b/packages/hub/src/app/models/[owner]/[slug]/EditModelPage.tsx deleted file mode 100644 index d6155c74e7..0000000000 --- a/packages/hub/src/app/models/[owner]/[slug]/EditModelPage.tsx +++ /dev/null @@ -1,49 +0,0 @@ -"use client"; - -import { FC } from "react"; -import { graphql } from "react-relay"; - -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; - -import { EditSquiggleSnippetModel } from "./EditSquiggleSnippetModel"; - -import { EditModelPageQuery } from "@/__generated__/EditModelPageQuery.graphql"; - -export const EditModelPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [{ model: result }] = usePageQuery( - graphql` - query EditModelPageQuery($input: QueryModelInput!) { - model(input: $input) { - __typename - ... on BaseError { - message - } - ... on Model { - id - currentRevision { - content { - __typename - } - } - ...EditSquiggleSnippetModel - } - } - } - `, - query - ); - - const model = extractFromGraphqlErrorUnion(result, "Model"); - const typename = model.currentRevision.content.__typename; - - switch (typename) { - case "SquiggleSnippet": - return ; - default: - return
Unknown model type {typename}
; - } -}; diff --git a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx index 2a912078fa..9cf6e21c11 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx @@ -1,3 +1,4 @@ +"use client"; import { useRouter } from "next/navigation"; import { BaseSyntheticEvent, @@ -8,7 +9,7 @@ import { useState, } from "react"; import { FormProvider, useFieldArray, useForm } from "react-hook-form"; -import { graphql, useFragment } from "react-relay"; +import { graphql } from "react-relay"; import { ButtonWithDropdown, @@ -29,7 +30,6 @@ import { useAdjustSquiggleVersion, versionedSquigglePackages, versionSupportsDropdownMenu, - versionSupportsExports, versionSupportsImportTooltip, versionSupportsOnOpenExport, } from "@quri/versioned-squiggle-components"; @@ -41,8 +41,8 @@ import { FormModal } from "@/components/ui/FormModal"; import { SAMPLE_COUNT_DEFAULT, XY_POINT_LENGTH_DEFAULT } from "@/constants"; import { useAvailableHeight } from "@/hooks/useAvailableHeight"; import { useMutationForm } from "@/hooks/useMutationForm"; -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; import { modelRoute, variableRoute } from "@/routes"; +import { ModelFullDTO } from "@/server/models/data/full"; import { ImportTooltip } from "@/squiggle/components/ImportTooltip"; import { getHubLinker, @@ -57,7 +57,6 @@ import { useDraftLocator, } from "./SquiggleSnippetDraftDialog"; -import { EditSquiggleSnippetModel$key } from "@/__generated__/EditSquiggleSnippetModel.graphql"; import { EditSquiggleSnippetModelMutation, RelativeValuesExportInput, @@ -128,69 +127,18 @@ const SaveButton: FC<{ onSubmit: OnSubmit; disabled: boolean }> = ({ type Props = { // We have to pass the entire model here and not just content; // it's too hard to split the editing form into "content-type-specific" part and "generic model fields" part. - modelRef: EditSquiggleSnippetModel$key; + model: ModelFullDTO; forceVersionPicker?: boolean; }; export const EditSquiggleSnippetModel: FC = ({ - modelRef, + model, forceVersionPicker, }) => { - const model = useFragment( - graphql` - fragment EditSquiggleSnippetModel on Model { - id - slug - isEditable - ...EditRelativeValueExports_Model - ...SquiggleSnippetDraftDialog_Model - owner { - slug - } - lastRevisionWithBuild { - lastBuild { - runSeconds - } - } - currentRevision { - id - content { - __typename - ... on SquiggleSnippet { - id - code - version - seed - autorunMode - sampleCount - xyPointLength - } - } - exportNames - relativeValuesExports { - id - variableName - definition { - slug - owner { - slug - } - } - } - } - } - `, - modelRef - ); const revision = model.currentRevision; const router = useRouter(); - const content = extractFromGraphqlErrorUnion( - revision.content, - "SquiggleSnippet" - ); - - const lastBuildSpeed = model.lastRevisionWithBuild?.lastBuild?.runSeconds; + const content = revision.squiggleSnippet; const seed = content.seed; @@ -320,7 +268,11 @@ export const EditSquiggleSnippetModel: FC = ({ // Automatically turn off autorun, if the last build speed was > 5s. Note that this does not stop in the case of memory errors or similar. const autorunMode = content.autorunMode || - (lastBuildSpeed ? (lastBuildSpeed > 5 ? false : true) : true); + (model.lastBuildSeconds + ? model.lastBuildSeconds > 5 + ? false + : true + : true); // Build props for versioned SquigglePlayground first, since they might depend on the version we use, // and we want to populate them incrementally. @@ -328,7 +280,7 @@ export const EditSquiggleSnippetModel: FC = ({ typeof squiggle.components.SquigglePlayground >[0] = { defaultCode, - autorunMode: autorunMode, + autorunMode, sourceId: serializeSourceId({ owner: model.owner.slug, slug: model.slug, @@ -377,7 +329,7 @@ export const EditSquiggleSnippetModel: FC = ({ onSubmit(); }} items={variablesWithDefinitionsFields} - modelRef={model} + model={model} />
), @@ -410,14 +362,6 @@ export const EditSquiggleSnippetModel: FC = ({ ) : null; } - if ( - versionSupportsExports.propsByVersion<"SquigglePlayground">( - squiggle.version, - playgroundProps - ) - ) { - } - playgroundProps.environment = { sampleCount: content.sampleCount || SAMPLE_COUNT_DEFAULT, xyPointLength: content.xyPointLength || XY_POINT_LENGTH_DEFAULT, diff --git a/packages/hub/src/app/models/[owner]/[slug]/SquiggleSnippetDraftDialog.tsx b/packages/hub/src/app/models/[owner]/[slug]/SquiggleSnippetDraftDialog.tsx index a82de66400..dcd891fd52 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/SquiggleSnippetDraftDialog.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/SquiggleSnippetDraftDialog.tsx @@ -1,14 +1,12 @@ import { FC, PropsWithChildren, useState } from "react"; -import { graphql, useFragment } from "react-relay"; import { Button, Modal } from "@quri/ui"; import { useClientOnlyRender } from "@/hooks/useClientOnlyRender"; +import { ModelFullDTO } from "@/server/models/data/full"; import { SquiggleSnippetFormShape } from "./EditSquiggleSnippetModel"; -import { SquiggleSnippetDraftDialog_Model$key } from "@/__generated__/SquiggleSnippetDraftDialog_Model.graphql"; - export type Draft = { formState: SquiggleSnippetFormShape; version: string; @@ -98,22 +96,7 @@ export const draftUtils = { }, }; -export function useDraftLocator( - modelRef: SquiggleSnippetDraftDialog_Model$key -) { - const model = useFragment( - graphql` - fragment SquiggleSnippetDraftDialog_Model on Model { - id - slug - owner { - slug - } - } - `, - modelRef - ); - +export function useDraftLocator(model: ModelFullDTO) { const draftLocator: DraftLocator = { ownerSlug: model.owner.slug, modelSlug: model.slug, diff --git a/packages/hub/src/app/models/[owner]/[slug]/page.tsx b/packages/hub/src/app/models/[owner]/[slug]/page.tsx index f7a4474e18..b2c6e0e714 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/page.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/page.tsx @@ -1,13 +1,10 @@ +import { notFound } from "next/navigation"; import { Suspense } from "react"; import Skeleton from "react-loading-skeleton"; -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { loadModelFull } from "@/server/models/data/full"; -import { EditModelPage } from "./EditModelPage"; - -import QueryNode, { - EditModelPageQuery, -} from "@/__generated__/EditModelPageQuery.graphql"; +import { EditSquiggleSnippetModel } from "./EditSquiggleSnippetModel"; type Props = { params: Promise<{ owner: string; slug: string }>; @@ -15,15 +12,23 @@ type Props = { async function InnerPage({ params }: Props) { const { owner, slug } = await params; - const query = await loadPageQuery(QueryNode, { - input: { owner, slug }, - }); - return ( -
- -
- ); + const model = await loadModelFull({ owner, slug }); + if (!model) { + notFound(); + } + + const { contentType } = model.currentRevision; + switch (contentType) { + case "SquiggleSnippet": + return ( +
+ +
+ ); + default: + return
Unknown model type {contentType}
; + } } const Loading = () => { diff --git a/packages/hub/src/components/exports/EditRelativeValueExports.tsx b/packages/hub/src/components/exports/EditRelativeValueExports.tsx index a914c18290..8738afcdae 100644 --- a/packages/hub/src/components/exports/EditRelativeValueExports.tsx +++ b/packages/hub/src/components/exports/EditRelativeValueExports.tsx @@ -1,7 +1,5 @@ -"use client"; import { FC, useState } from "react"; import { useForm } from "react-hook-form"; -import { graphql, useFragment } from "react-relay"; import { Button, TextFormField } from "@quri/ui"; @@ -9,6 +7,7 @@ import { modelForRelativeValuesExportRoute, relativeValuesRoute, } from "@/routes"; +import { ModelFullDTO } from "@/server/models/data/full"; import { SelectOwner, SelectOwnerOption } from "../SelectOwner"; import { FormModal } from "../ui/FormModal"; @@ -20,7 +19,6 @@ import { SelectRelativeValuesDefinitionOption, } from "./SelectRelativeValuesDefinition"; -import { EditRelativeValueExports_Model$key } from "@/__generated__/EditRelativeValueExports_Model.graphql"; import { RelativeValuesExportInput } from "@/__generated__/EditSquiggleSnippetModelMutation.graphql"; const CreateVariableWithDefinitionModal: FC<{ @@ -90,22 +88,9 @@ const CreateVariableWithDefinitionModal: FC<{ const ExportItem: FC<{ item: RelativeValuesExportInput; - modelRef: EditRelativeValueExports_Model$key; + model: ModelFullDTO; remove: () => void; -}> = ({ item, modelRef, remove }) => { - const model = useFragment( - graphql` - fragment EditRelativeValueExports_Model on Model { - id - slug - owner { - slug - } - } - `, - modelRef - ); - +}> = ({ item, model, remove }) => { return (
@@ -139,14 +124,14 @@ type Props = { append: (item: RelativeValuesExportInput) => void; remove: (id: number) => void; items: RelativeValuesExportInput[]; - modelRef: EditRelativeValueExports_Model$key; + model: ModelFullDTO; }; export const EditRelativeValueExports: FC = ({ append, remove, items, - modelRef, + model, }) => { const [isOpen, setIsOpen] = useState(false); @@ -158,7 +143,7 @@ export const EditRelativeValueExports: FC = ({ remove(i)} /> ))} diff --git a/packages/hub/src/graphql/queries/groups.ts b/packages/hub/src/graphql/queries/groups.ts deleted file mode 100644 index e264925eb7..0000000000 --- a/packages/hub/src/graphql/queries/groups.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { isSignedIn } from "../helpers/userHelpers"; -import { Group, GroupConnection } from "../types/Group"; - -const GroupsQueryInput = builder.inputType("GroupsQueryInput", { - fields: (t) => ({ - slugContains: t.string(), - myOnly: t.boolean({ - description: "List only groups that you're a member of", - }), - }), -}); - -builder.queryField("groups", (t) => - t.prismaConnection( - { - type: Group, - cursor: "id", - args: { - input: t.arg({ type: GroupsQueryInput }), - }, - resolve: async (query, _, { input }, { session }) => { - if (input?.myOnly && !isSignedIn(session)) { - // Relay stuggles with union types on connection fields (see e.g. https://github.com/facebook/relay/issues/4366) - // So we return an empty list instead of throwing an error. - return []; - } - - return await prisma.group.findMany({ - ...query, - orderBy: { - updatedAt: "desc", - }, - where: { - ...(input?.slugContains && { - asOwner: { - slug: { - contains: input.slugContains, - mode: "insensitive", - }, - }, - }), - ...(input?.myOnly && - isSignedIn(session) && { - memberships: { - some: { - user: { email: session.user.email }, - }, - }, - }), - }, - }); - }, - }, - GroupConnection - ) -); diff --git a/packages/hub/src/graphql/queries/modelsByVersion.ts b/packages/hub/src/graphql/queries/modelsByVersion.ts deleted file mode 100644 index a97641aa60..0000000000 --- a/packages/hub/src/graphql/queries/modelsByVersion.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { Model } from "../types/Model"; - -const ModelsByVersion = builder.simpleObject("ModelsByVersion", { - fields: (t) => ({ - version: t.string(), - count: t.int(), - privateCount: t.int(), - models: t.field({ - type: [Model], - }), - }), -}); - -builder.queryField("modelsByVersion", (t) => - t.field({ - description: "Admin-only query for listing models in /admin UI", - type: [ModelsByVersion], - authScopes: { - isRootUser: true, - }, - resolve: async () => { - const models = await prisma.model.findMany({ - include: { - currentRevision: { - where: { - contentType: "SquiggleSnippet", - }, - include: { - squiggleSnippet: true, - }, - }, - }, - }); - - const groupedModels: Record = {}; - const privateStats: Record = {}; - const versions = new Set(); - - for (const model of models) { - const version = model.currentRevision?.squiggleSnippet?.version; - if (!version) continue; - - versions.add(version); - - if (model.isPrivate) { - privateStats[version] ??= 0; - privateStats[version]++; - } else { - // don't expose private models, they won't be available through GraphQL anyway - groupedModels[version] ??= []; - groupedModels[version].push(model); - } - } - return [...versions.values()].map((version) => ({ - version, - count: (groupedModels[version] ?? []).length, - privateCount: privateStats[version] ?? 0, - models: groupedModels[version] ?? [], - })); - }, - }) -); diff --git a/packages/hub/src/graphql/queries/variables.ts b/packages/hub/src/graphql/queries/variables.ts deleted file mode 100644 index 7470053ef9..0000000000 --- a/packages/hub/src/graphql/queries/variables.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { modelWhereHasAccess } from "../helpers/modelHelpers"; -import { Variable, VariableConnection } from "../types/Variable"; - -const VariableQueryInput = builder.inputType("VariableQueryInput", { - fields: (t) => ({ - modelId: t.string(), - variableName: t.string(), - owner: t.string(), - variableType: t.string(), - }), -}); - -builder.queryField("variables", (t) => - t.prismaConnection( - { - type: Variable, - cursor: "id", - args: { - input: t.arg({ type: VariableQueryInput }), - }, - resolve: (query, _, { input }, { session }) => { - const modelId = input?.modelId; - - const queries = { - model: { - ...modelWhereHasAccess(session), - ...(input?.owner && { owner: { slug: input.owner } }), - }, - ...(modelId && { modelId: modelId }), - ...(input?.variableName && { variableName: input.variableName }), - ...(input?.variableType && { - currentRevision: { - variableType: input.variableType, - }, - }), - }; - - return prisma.variable.findMany({ - ...query, - where: { - ...queries, - }, - }); - }, - }, - VariableConnection - ) -); diff --git a/packages/hub/src/graphql/schema.ts b/packages/hub/src/graphql/schema.ts index 3bf95f7e97..05494179f1 100644 --- a/packages/hub/src/graphql/schema.ts +++ b/packages/hub/src/graphql/schema.ts @@ -2,13 +2,9 @@ import "./errors/BaseError"; import "./errors/NotFoundError"; import "./errors/ValidationError"; import "./queries/group"; -import "./queries/groups"; import "./queries/me"; import "./queries/model"; -import "./queries/variables"; import "./queries/variable"; -import "./queries/models"; -import "./queries/modelsByVersion"; import "./queries/relativeValuesDefinition"; import "./queries/relativeValuesDefinitions"; import "./queries/runSquiggle"; diff --git a/packages/hub/src/server/models/actions/loadModelCardAction.ts b/packages/hub/src/server/models/actions/loadModelCardAction.ts index b5b7b80a2a..23d056499f 100644 --- a/packages/hub/src/server/models/actions/loadModelCardAction.ts +++ b/packages/hub/src/server/models/actions/loadModelCardAction.ts @@ -5,7 +5,8 @@ import { makeServerAction, zSlug } from "@/server/utils"; import { loadModelCard, ModelCardDTO } from "../data/card"; -// data-fetching action, used in ImportTooltip +// Data-fetching action, used in ImportTooltip. +// Don't use this for loading models; server actions are discouraged for data fetching. export const loadModelCardAction = makeServerAction( z.object({ owner: zSlug, diff --git a/packages/hub/src/server/models/actions/loadModelFullAction.ts b/packages/hub/src/server/models/actions/loadModelFullAction.ts new file mode 100644 index 0000000000..2b4a0a57ed --- /dev/null +++ b/packages/hub/src/server/models/actions/loadModelFullAction.ts @@ -0,0 +1,18 @@ +"use server"; +import { z } from "zod"; + +import { makeServerAction, zSlug } from "@/server/utils"; + +import { loadModelFull, ModelFullDTO } from "../data/full"; + +// Data-fetching action, used in /admin/upgrade-versions. +// Don't use this for loading models; server actions are discouraged for data fetching. +export const loadModelFullAction = makeServerAction( + z.object({ + owner: zSlug, + slug: zSlug, + }), + async ({ owner, slug }): Promise => { + return loadModelFull({ owner, slug }); + } +); diff --git a/packages/hub/src/server/models/data/byVersion.ts b/packages/hub/src/server/models/data/byVersion.ts new file mode 100644 index 0000000000..fb0d4f44ec --- /dev/null +++ b/packages/hub/src/server/models/data/byVersion.ts @@ -0,0 +1,53 @@ +import "server-only"; + +import { prisma } from "@/prisma"; +import { checkRootUser } from "@/server/users/auth"; + +// Admin-only, for listing models in /admin UI +export async function loadModelsByVersion() { + await checkRootUser(); + + const models = await prisma.model.findMany({ + include: { + currentRevision: { + where: { + contentType: "SquiggleSnippet", + }, + include: { + squiggleSnippet: true, + }, + }, + owner: true, + }, + }); + + const groupedModels: Record = {}; + const privateStats: Record = {}; + const versions = new Set(); + + for (const model of models) { + const version = model.currentRevision?.squiggleSnippet?.version; + if (!version) continue; + + versions.add(version); + + if (model.isPrivate) { + privateStats[version] ??= 0; + privateStats[version]++; + } else { + // don't expose private models, they won't be available for loading anyway + groupedModels[version] ??= []; + groupedModels[version].push(model); + } + } + return [...versions.values()].map((version) => ({ + version, + count: (groupedModels[version] ?? []).length, + privateCount: privateStats[version] ?? 0, + models: groupedModels[version] ?? [], + })); +} + +export type ModelByVersion = Awaited< + ReturnType +>[number]; diff --git a/packages/hub/src/server/models/data/card.ts b/packages/hub/src/server/models/data/card.ts index ababf08ac0..b1e46ab92e 100644 --- a/packages/hub/src/server/models/data/card.ts +++ b/packages/hub/src/server/models/data/card.ts @@ -7,6 +7,7 @@ import { prisma } from "@/prisma"; import { Paginated } from "../../types"; import { modelWhereHasAccess } from "./authHelpers"; +// FIXME - explicit ModelCardDTO function toDTO(dbModel: DbModelCard) { function check(model: DbModelCard): asserts model is Omit< DbModelCard, @@ -40,7 +41,7 @@ function toDTO(dbModel: DbModelCard) { }; } -const modelCardSelect = { +const select = { id: true, slug: true, updatedAt: true, @@ -103,11 +104,7 @@ const modelCardSelect = { } satisfies Prisma.ModelSelect; type DbModelCard = NonNullable< - Awaited< - ReturnType< - typeof prisma.model.findFirst<{ select: typeof modelCardSelect }> - > - > + Awaited>> >; export type ModelCardDTO = ReturnType; @@ -122,7 +119,7 @@ export async function loadModelCards( const limit = params.limit ?? 20; const dbModels = await prisma.model.findMany({ - select: modelCardSelect, + select, orderBy: { updatedAt: "desc" }, cursor: params.cursor ? { id: params.cursor } : undefined, where: { @@ -161,9 +158,9 @@ export async function loadModelCard({ slug: string; }): Promise { const dbModel = await prisma.model.findFirst({ - select: modelCardSelect, + select: select, where: { - slug: slug, + slug, owner: { slug: owner }, OR: await modelWhereHasAccess(), }, diff --git a/packages/hub/src/server/models/data/full.ts b/packages/hub/src/server/models/data/full.ts new file mode 100644 index 0000000000..7120adb520 --- /dev/null +++ b/packages/hub/src/server/models/data/full.ts @@ -0,0 +1,164 @@ +import "server-only"; + +import { Prisma } from "@prisma/client"; + +import { prisma } from "@/prisma"; +import { controlsOwnerId } from "@/server/owners/auth"; + +import { modelWhereHasAccess } from "./authHelpers"; + +const select = { + id: true, + slug: true, + owner: { + select: { + id: true, + slug: true, + }, + }, + currentRevision: { + select: { + contentType: true, + squiggleSnippet: { + select: { + id: true, + code: true, + version: true, + seed: true, + autorunMode: true, + sampleCount: true, + xyPointLength: true, + }, + }, + relativeValuesExports: { + select: { + variableName: true, + definition: { + select: { + slug: true, + owner: { + select: { + slug: true, + }, + }, + }, + }, + }, + }, + }, + }, +} satisfies Prisma.ModelSelect; + +type Row = NonNullable< + Awaited>> +>; + +export type ModelFullDTO = { + id: string; + slug: string; + owner: { + id: string; + slug: string; + }; + currentRevision: { + contentType: "SquiggleSnippet"; + squiggleSnippet: { + id: string; + code: string; + version: string; + seed: string; + autorunMode: boolean | null; + sampleCount: number | null; + xyPointLength: number | null; + }; + relativeValuesExports: { + variableName: string; + definition: { + slug: string; + owner: { + slug: string; + }; + }; + }[]; + }; + isEditable: boolean; + lastBuildSeconds: number | null; +}; + +async function toDTO(row: Row): Promise { + if (!row.currentRevision) { + throw new Error("No current revision"); + } + + if (!row.currentRevision.squiggleSnippet) { + throw new Error("No squiggle snippet"); + } + + let lastBuildSeconds: number | null = null; + { + const lastRevisionWithBuild = await prisma.modelRevision.findFirst({ + select: { + builds: { + select: { + runSeconds: true, + }, + }, + }, + orderBy: { + createdAt: "desc", + }, + where: { + modelId: row.id, + builds: { + some: { + id: { + not: undefined, + }, + }, + }, + }, + }); + if (lastRevisionWithBuild) { + lastBuildSeconds = lastRevisionWithBuild.builds[0]?.runSeconds; + } + } + + return { + id: row.id, + slug: row.slug, + owner: { + id: row.owner.id, + slug: row.owner.slug, + }, + currentRevision: { + contentType: row.currentRevision.contentType, + squiggleSnippet: row.currentRevision.squiggleSnippet, + relativeValuesExports: row.currentRevision.relativeValuesExports, + }, + isEditable: await controlsOwnerId(row.owner.id), + lastBuildSeconds, + }; +} + +export async function loadModelFull({ + owner, + slug, +}: { + owner: string; + slug: string; +}): Promise { + const row = await prisma.model.findFirst({ + select, + where: { + slug: slug, + owner: { slug: owner }, + OR: await modelWhereHasAccess(), + }, + }); + + if (!row) { + return null; + } + + return toDTO(row); +} diff --git a/packages/hub/src/server/models/data/helpers.ts b/packages/hub/src/server/models/data/helpers.ts index 610b61698c..1876ed4511 100644 --- a/packages/hub/src/server/models/data/helpers.ts +++ b/packages/hub/src/server/models/data/helpers.ts @@ -1,35 +1,7 @@ -import { auth } from "@/auth"; -import { prisma } from "@/prisma"; +import { controlsOwnerId } from "@/server/owners/auth"; import { ModelCardDTO } from "./card"; export async function isModelEditable(model: ModelCardDTO): Promise { - const session = await auth(); - if (!session?.user.email) { - return false; - } - - return Boolean( - await prisma.owner.count({ - where: { - id: model.owner.id, - OR: [ - { - user: { email: session.user.email }, - }, - { - group: { - memberships: { - some: { - user: { - email: session.user.email, - }, - }, - }, - }, - }, - ], - }, - }) - ); + return controlsOwnerId(model.owner.id); } diff --git a/packages/hub/src/server/owners/auth.ts b/packages/hub/src/server/owners/auth.ts new file mode 100644 index 0000000000..816bb62fa1 --- /dev/null +++ b/packages/hub/src/server/owners/auth.ts @@ -0,0 +1,33 @@ +import { auth } from "@/auth"; +import { prisma } from "@/prisma"; + +export async function controlsOwnerId(ownerId: string): Promise { + const session = await auth(); + if (!session?.user.email) { + return false; + } + + return Boolean( + await prisma.owner.count({ + where: { + id: ownerId, + OR: [ + { + user: { email: session.user.email }, + }, + { + group: { + memberships: { + some: { + user: { + email: session.user.email, + }, + }, + }, + }, + }, + ], + }, + }) + ); +} From 2c2939419719e1dce23827abf3adce4e47bc8252 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 28 Nov 2024 21:25:38 -0300 Subject: [PATCH 35/68] search route --- packages/hub/src/app/api/search/route.ts | 261 ++++++++++++++++++ packages/hub/src/app/api/search/schema.ts | 42 +++ .../components/GlobalSearch/SearchResult.tsx | 68 ++--- .../GlobalSearch/SearchResultGroup.tsx | 26 +- .../GlobalSearch/SearchResultModel.tsx | 28 +- .../SearchResultRelativeValuesDefinition.tsx | 25 +- .../GlobalSearch/SearchResultUser.tsx | 26 +- .../hub/src/components/GlobalSearch/index.tsx | 79 ++---- packages/hub/src/graphql/queries/search.ts | 126 --------- packages/hub/src/graphql/types/Searchable.ts | 108 -------- 10 files changed, 363 insertions(+), 426 deletions(-) create mode 100644 packages/hub/src/app/api/search/route.ts create mode 100644 packages/hub/src/app/api/search/schema.ts delete mode 100644 packages/hub/src/graphql/queries/search.ts delete mode 100644 packages/hub/src/graphql/types/Searchable.ts diff --git a/packages/hub/src/app/api/search/route.ts b/packages/hub/src/app/api/search/route.ts new file mode 100644 index 0000000000..ec90e5d197 --- /dev/null +++ b/packages/hub/src/app/api/search/route.ts @@ -0,0 +1,261 @@ +import { Searchable as PrismaSearchable } from "@prisma/client"; +import { NextRequest } from "next/server"; +import { z } from "zod"; + +import { auth } from "@/auth"; +import { prisma } from "@/prisma"; +import { + groupRoute, + modelRoute, + relativeValuesRoute, + userRoute, +} from "@/routes"; + +import { SearchResult } from "./schema"; + +type SearchableWithEdgeData = PrismaSearchable & { + rank: number; + slugSnippet: string; + textSnippet: string; +}; + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + + const { query } = z + .object({ + query: z.string(), + }) + .parse(Object.fromEntries(searchParams.entries())); + + const limit = 20; + const offset = 0; + + const session = await auth(); + + const rows = await prisma.$queryRaw` + SELECT + "Searchable".*, + -- ranking function + ts_rank_cd( + setweight(to_tsvector(coalesce("Model".slug, '')), 'A') || + setweight(to_tsvector(coalesce("SquiggleSnippet".code, '')), 'B') || + setweight(to_tsvector(coalesce("RelativeValuesDefinition".slug, '')), 'A') || + setweight(to_tsvector(coalesce("UserOwner".slug, '')), 'A') || + setweight(to_tsvector(coalesce("GroupOwner".slug, '')), 'A'), + websearch_to_tsquery(${query}) + ) AS rank, + ts_headline( + concat_ws( + ' ', + "Model".slug, + "RelativeValuesDefinition".slug, + "UserOwner".slug, + "GroupOwner".slug + ), + websearch_to_tsquery(${query}), + 'HighlightAll=t' + ) AS "slugSnippet", + ts_headline(concat_ws( + ' ', + "SquiggleSnippet".code + ), websearch_to_tsquery(${query}) + ) AS "textSnippet" + FROM "Searchable" + LEFT JOIN "Model" ON "Model".id = "Searchable"."modelId" + LEFT JOIN "Owner" AS "ModelOwner" ON "Model"."ownerId" = "ModelOwner".id + LEFT JOIN "User" AS "ModelOwnerUser" ON "ModelOwner".id = "ModelOwnerUser"."ownerId" + LEFT JOIN "Group" AS "ModelOwnerGroup" ON "ModelOwner".id = "ModelOwnerGroup"."ownerId" + LEFT JOIN "ModelRevision" ON "Model"."currentRevisionId" = "ModelRevision".id + LEFT JOIN "SquiggleSnippet" ON "ModelRevision"."contentId" = "SquiggleSnippet".id + LEFT JOIN "RelativeValuesDefinition" ON "RelativeValuesDefinition".id = "Searchable"."definitionId" + -- LEFT JOIN "Owner" AS "RelativeValuesDefinitionOwner" ON "RelativeValuesDefinition"."ownerId" = "RelativeValuesDefinitionOwner".id + LEFT JOIN "User" ON "User".id = "Searchable"."userId" + LEFT JOIN "Owner" AS "UserOwner" ON "User"."ownerId" = "UserOwner".id + LEFT JOIN "Group" ON "Group".id = "Searchable"."groupId" + LEFT JOIN "Owner" AS "GroupOwner" ON "Group"."ownerId" = "GroupOwner".id + WHERE + ( + -- check permissions, should match "modelWhereHasAccess" function + "Model".id IS NULL OR + "Model"."isPrivate" IS FALSE + OR "ModelOwnerUser".email = ${session?.user.email} + OR "ModelOwnerGroup".id IN ( + SELECT DISTINCT "groupId" + FROM "UserGroupMembership" + LEFT JOIN "User" ON "User".id = "UserGroupMembership"."userId" + WHERE "User".email = ${session?.user.email} + ) + ) AND ( + -- construct search document + to_tsvector(concat_ws( + ' ', + "Model".slug, + "SquiggleSnippet".code, + "RelativeValuesDefinition".slug, + "UserOwner".slug, + "GroupOwner".slug + )) @@ websearch_to_tsquery(${query}) + ) + ORDER BY rank DESC + LIMIT ${limit} + OFFSET ${offset} + `; + + const modelIds = rows + .filter( + (row): row is SearchableWithEdgeData & { modelId: string } => + row.modelId !== null + ) + .map((row) => row.modelId); + + const definitionIds = rows + .filter( + (row): row is SearchableWithEdgeData & { definitionId: string } => + row.definitionId !== null + ) + .map((row) => row.definitionId); + + const userIds = rows + .filter( + (row): row is SearchableWithEdgeData & { userId: string } => + row.userId !== null + ) + .map((row) => row.userId); + + const groupIds = rows + .filter( + (row): row is SearchableWithEdgeData & { groupId: string } => + row.groupId !== null + ) + .map((row) => row.groupId); + + const models = modelIds.length + ? await prisma.model.findMany({ + where: { + id: { in: modelIds }, + }, + select: { + id: true, + slug: true, + owner: { + select: { + slug: true, + }, + }, + }, + }) + : []; + + const definitions = definitionIds.length + ? await prisma.relativeValuesDefinition.findMany({ + where: { + id: { in: definitionIds }, + }, + select: { + id: true, + slug: true, + owner: { + select: { + slug: true, + }, + }, + }, + }) + : []; + + const users = userIds.length + ? await prisma.user.findMany({ + where: { + id: { in: userIds }, + }, + select: { + id: true, + asOwner: { + select: { + slug: true, + }, + }, + }, + }) + : []; + + const groups = groupIds.length + ? await prisma.group.findMany({ + where: { + id: { in: groupIds }, + }, + select: { + id: true, + asOwner: { + select: { + slug: true, + }, + }, + }, + }) + : []; + + const result: SearchResult = []; + + for (const row of rows) { + if (row.modelId !== null) { + const model = models.find((m) => m.id === row.modelId); + if (!model) continue; + + result.push({ + id: row.id, + link: modelRoute({ owner: model.owner.slug, slug: model.slug }), + slugSnippet: row.slugSnippet, + textSnippet: row.textSnippet, + object: { type: "Model", owner: model.owner.slug, slug: model.slug }, + }); + } else if (row.definitionId !== null) { + const definition = definitions.find((d) => d.id === row.definitionId); + if (!definition) continue; + + result.push({ + id: row.id, + link: relativeValuesRoute({ + owner: definition.owner.slug, + slug: definition.slug, + }), + slugSnippet: row.slugSnippet, + textSnippet: row.textSnippet, + object: { + type: "RelativeValuesDefinition", + owner: definition.owner.slug, + slug: definition.slug, + }, + }); + } else if (row.userId !== null) { + const user = users.find((u) => u.id === row.userId); + if (!user?.asOwner) continue; + + const username = user.asOwner.slug; + + result.push({ + id: row.id, + link: userRoute({ username }), + slugSnippet: row.slugSnippet, + textSnippet: row.textSnippet, + object: { type: "User", slug: username }, + }); + } else if (row.groupId !== null) { + const group = groups.find((g) => g.id === row.groupId); + if (!group?.asOwner) continue; + + const slug = group.asOwner.slug; + + result.push({ + id: row.id, + link: groupRoute({ slug }), + slugSnippet: row.slugSnippet, + textSnippet: row.textSnippet, + object: { type: "Group", slug }, + }); + } + } + + return Response.json(result); +} diff --git a/packages/hub/src/app/api/search/schema.ts b/packages/hub/src/app/api/search/schema.ts new file mode 100644 index 0000000000..8a462c450f --- /dev/null +++ b/packages/hub/src/app/api/search/schema.ts @@ -0,0 +1,42 @@ +import { z } from "zod"; + +const searchResultObjectSchema = z.discriminatedUnion("type", [ + z.object({ + type: z.literal("Model"), + slug: z.string(), + owner: z.string(), + }), + z.object({ + type: z.literal("RelativeValuesDefinition"), + slug: z.string(), + owner: z.string(), + }), + z.object({ + type: z.literal("User"), + slug: z.string(), + }), + z.object({ + type: z.literal("Group"), + slug: z.string(), + }), +]); + +export const searchResultSchema = z.array( + z.object({ + id: z.string(), + link: z.string(), + slugSnippet: z.string(), + textSnippet: z.string(), + object: searchResultObjectSchema, + }) +); + +export type SearchResult = z.infer; + +export type SearchResultItem = SearchResult[number]; + +export type TypedSearchResultItem< + T extends SearchResultItem["object"]["type"], +> = SearchResultItem & { + object: Extract; +}; diff --git a/packages/hub/src/components/GlobalSearch/SearchResult.tsx b/packages/hub/src/components/GlobalSearch/SearchResult.tsx index cb868623bd..a85f096280 100644 --- a/packages/hub/src/components/GlobalSearch/SearchResult.tsx +++ b/packages/hub/src/components/GlobalSearch/SearchResult.tsx @@ -1,7 +1,11 @@ import { FC } from "react"; -import { graphql, useFragment } from "react-relay"; import { components, type OptionProps } from "react-select"; +import { + SearchResultItem, + TypedSearchResultItem, +} from "@/app/api/search/schema"; + import { Link } from "../ui/Link"; import { SearchOption } from "./"; import { SearchResultGroup } from "./SearchResultGroup"; @@ -9,65 +13,36 @@ import { SearchResultModel } from "./SearchResultModel"; import { SearchResultRelativeValuesDefinition } from "./SearchResultRelativeValuesDefinition"; import { SearchResultUser } from "./SearchResultUser"; -import { SearchResult$key } from "@/__generated__/SearchResult.graphql"; -import { SearchResultEdge$key } from "@/__generated__/SearchResultEdge.graphql"; - -export function useEdgeFragment(edgeFragment: SearchResultEdge$key) { - return useFragment( - graphql` - fragment SearchResultEdge on SearchEdge { - slugSnippet - textSnippet - } - `, - edgeFragment - ); -} - -export type SearchResultComponent = FC<{ - fragment: T; - edgeFragment: SearchResultEdge$key; +export type SearchResultComponent< + T extends SearchResultItem["object"]["type"], +> = FC<{ + item: TypedSearchResultItem; }>; const OkSearchResult: FC<{ - fragment: SearchResult$key; - edgeFragment: SearchResultEdge$key; -}> = ({ fragment: objectRef, edgeFragment }) => { - const object = useFragment( - graphql` - fragment SearchResult on SearchableObject { - __typename - ...SearchResultModel - ...SearchResultRelativeValuesDefinition - ...SearchResultUser - ...SearchResultGroup - } - `, - objectRef - ); - - switch (object.__typename) { + item: SearchResultItem; +}> = ({ item }) => { + switch (item.object.type) { case "Model": return ( - + } /> ); case "RelativeValuesDefinition": return ( } /> ); case "User": - return ; + return } />; case "Group": return ( - + } /> ); default: return (
- Unknown result type: {object.__typename} + Unknown result type: {item.object satisfies never}
); } @@ -78,14 +53,11 @@ export const SearchResult: FC> = ({ ...props }) => { switch (props.data.type) { - case "object": + case "ok": return ( - - + + ); diff --git a/packages/hub/src/components/GlobalSearch/SearchResultGroup.tsx b/packages/hub/src/components/GlobalSearch/SearchResultGroup.tsx index 4d24453cba..e342d45b4a 100644 --- a/packages/hub/src/components/GlobalSearch/SearchResultGroup.tsx +++ b/packages/hub/src/components/GlobalSearch/SearchResultGroup.tsx @@ -1,31 +1,13 @@ -import { FC } from "react"; -import { graphql, useFragment } from "react-relay"; - -import { SearchResultGroup$key } from "@/__generated__/SearchResultGroup.graphql"; +import { SearchResultComponent } from "./SearchResult"; import { SearchResultBox } from "./SearchResultBox"; -import { SearchResultComponent, useEdgeFragment } from "./SearchResult"; -import { Snippet } from "./Snippet"; import { SearchResultTitle } from "./SearchResultTItle"; +import { Snippet } from "./Snippet"; -export const SearchResultGroup: SearchResultComponent< - SearchResultGroup$key -> = ({ fragment, edgeFragment }) => { - const edge = useEdgeFragment(edgeFragment); - - // Unused, because `SearchEdge.slugSnippet` is better than `Group.slug`. - useFragment( - graphql` - fragment SearchResultGroup on Group { - slug - } - `, - fragment - ); - +export const SearchResultGroup: SearchResultComponent<"Group"> = ({ item }) => { return ( - {edge.slugSnippet} + {item.slugSnippet} ); diff --git a/packages/hub/src/components/GlobalSearch/SearchResultModel.tsx b/packages/hub/src/components/GlobalSearch/SearchResultModel.tsx index 685afd2d1b..36ff3202e1 100644 --- a/packages/hub/src/components/GlobalSearch/SearchResultModel.tsx +++ b/packages/hub/src/components/GlobalSearch/SearchResultModel.tsx @@ -1,34 +1,18 @@ -import { graphql, useFragment } from "react-relay"; - -import { SearchResultModel$key } from "@/__generated__/SearchResultModel.graphql"; +import { SearchResultComponent } from "./SearchResult"; import { SearchResultBox } from "./SearchResultBox"; -import { SearchResultComponent, useEdgeFragment } from "./SearchResult"; +import { SearchResultTitle } from "./SearchResultTItle"; import { Snippet } from "./Snippet"; import { TextSnippet } from "./TextSnippet"; -import { SearchResultTitle } from "./SearchResultTItle"; -export const SearchResultModel: SearchResultComponent< - SearchResultModel$key -> = ({ fragment, edgeFragment }) => { - const edge = useEdgeFragment(edgeFragment); - const model = useFragment( - graphql` - fragment SearchResultModel on Model { - slug - owner { - slug - } - } - `, - fragment - ); +export const SearchResultModel: SearchResultComponent<"Model"> = ({ item }) => { + const model = item.object; return ( - {model.owner.slug}/{edge.slugSnippet} + {model.owner}/{item.slugSnippet} - {edge.textSnippet} + {item.textSnippet} ); }; diff --git a/packages/hub/src/components/GlobalSearch/SearchResultRelativeValuesDefinition.tsx b/packages/hub/src/components/GlobalSearch/SearchResultRelativeValuesDefinition.tsx index 9fb18b5128..050a623d85 100644 --- a/packages/hub/src/components/GlobalSearch/SearchResultRelativeValuesDefinition.tsx +++ b/packages/hub/src/components/GlobalSearch/SearchResultRelativeValuesDefinition.tsx @@ -1,30 +1,17 @@ -import { graphql, useFragment } from "react-relay"; - -import { SearchResultRelativeValuesDefinition$key } from "@/__generated__/SearchResultRelativeValuesDefinition.graphql"; -import { SearchResultBox } from "./SearchResultBox"; import { SearchResultComponent } from "./SearchResult"; -import { Snippet } from "./Snippet"; +import { SearchResultBox } from "./SearchResultBox"; import { SearchResultTitle } from "./SearchResultTItle"; +import { Snippet } from "./Snippet"; export const SearchResultRelativeValuesDefinition: SearchResultComponent< - SearchResultRelativeValuesDefinition$key -> = ({ fragment }) => { - const definition = useFragment( - graphql` - fragment SearchResultRelativeValuesDefinition on RelativeValuesDefinition { - slug - owner { - slug - } - } - `, - fragment - ); + "RelativeValuesDefinition" +> = ({ item }) => { + const definition = item.object; return ( - {definition.owner.slug}/{definition.slug} + {definition.owner}/{definition.slug} ); diff --git a/packages/hub/src/components/GlobalSearch/SearchResultUser.tsx b/packages/hub/src/components/GlobalSearch/SearchResultUser.tsx index 9217e71a62..0017475030 100644 --- a/packages/hub/src/components/GlobalSearch/SearchResultUser.tsx +++ b/packages/hub/src/components/GlobalSearch/SearchResultUser.tsx @@ -1,31 +1,13 @@ -import { graphql, useFragment } from "react-relay"; - -import { SearchResultUser$key } from "@/__generated__/SearchResultUser.graphql"; +import { SearchResultComponent } from "./SearchResult"; import { SearchResultBox } from "./SearchResultBox"; -import { SearchResultComponent, useEdgeFragment } from "./SearchResult"; -import { Snippet } from "./Snippet"; import { SearchResultTitle } from "./SearchResultTItle"; +import { Snippet } from "./Snippet"; -export const SearchResultUser: SearchResultComponent = ({ - fragment, - edgeFragment, -}) => { - const edge = useEdgeFragment(edgeFragment); - - // Unused, because `SearchEdge.slugSnippet` is better than `User.username`. - useFragment( - graphql` - fragment SearchResultUser on User { - username - } - `, - fragment - ); - +export const SearchResultUser: SearchResultComponent<"User"> = ({ item }) => { return ( - {edge.slugSnippet} + {item.slugSnippet} ); diff --git a/packages/hub/src/components/GlobalSearch/index.tsx b/packages/hub/src/components/GlobalSearch/index.tsx index 235df570c9..297f0f81e9 100644 --- a/packages/hub/src/components/GlobalSearch/index.tsx +++ b/packages/hub/src/components/GlobalSearch/index.tsx @@ -2,7 +2,6 @@ import { clsx } from "clsx"; import { useRouter } from "next/navigation"; import { FC, useRef } from "react"; -import { fetchQuery, graphql, useRelayEnvironment } from "react-relay"; import { components, DropdownIndicatorProps, @@ -12,43 +11,14 @@ import AsyncSelect from "react-select/async"; import { SearchIcon, useGlobalShortcut } from "@quri/ui"; -import { SearchResult } from "./SearchResult"; - -import { GlobalSearchQuery } from "@/__generated__/GlobalSearchQuery.graphql"; -import { SearchResult$key } from "@/__generated__/SearchResult.graphql"; -import { SearchResultEdge$key } from "@/__generated__/SearchResultEdge.graphql"; +import { SearchResultItem, searchResultSchema } from "@/app/api/search/schema"; -export const Query = graphql` - query GlobalSearchQuery($text: String!) { - result: search(text: $text) { - __typename - ... on BaseError { - message - } - ... on QuerySearchConnection { - edges { - cursor - ...SearchResultEdge - node { - id - link - object { - ...SearchResult - } - } - } - } - } - } -`; +import { SearchResult } from "./SearchResult"; export type SearchOption = | { - type: "object"; - id: string; - link: string; - edge: SearchResultEdge$key; - object: SearchResult$key; + type: "ok"; + item: SearchResultItem; } | { type: "error"; @@ -64,36 +34,27 @@ const DropdownIndicator = (props: DropdownIndicatorProps) => { }; export const GlobalSearch: FC = () => { - const environment = useRelayEnvironment(); const router = useRouter(); const loadOptions = async (text: string): Promise => { - const result = await fetchQuery(environment, Query, { - text, - }).toPromise(); - if (!result) return []; - if (result.result.__typename === "BaseError") { - return [ - { - type: "error", - message: result.result.message, - }, - ]; - } else if (result.result.__typename !== "QuerySearchConnection") { + try { + const result = await fetch( + `/api/search?${new URLSearchParams({ query: text })}` + ).then((r) => r.json()); + + const parsed = searchResultSchema.parse(result); + return parsed.map((item) => ({ + type: "ok", + item, + })); + } catch (e) { return [ { type: "error", - message: "Unknown error", + message: String(e), }, ]; } - return result.result.edges.map((edge) => ({ - type: "object", - id: edge.node.id, - link: edge.node.link, - edge, - object: edge.node.object, - })); }; // https://github.com/JedWatson/react-select/discussions/4669#discussioncomment-1994888 @@ -124,17 +85,17 @@ export const GlobalSearch: FC = () => { openMenuOnClick={false} placeholder="Search..." getOptionValue={(option) => - option.type === "error" ? "error" : option.id + option.type === "error" ? "error" : option.item.id } getOptionLabel={(option) => - option.type === "error" ? "error" : option.id + option.type === "error" ? "error" : option.item.id } onKeyDown={(event) => { event.key === "Escape" && ref.current?.blur(); }} onChange={(option) => { - if (option?.type === "object") { - router.push(option.link); + if (option?.type === "ok") { + router.push(option.item.link); } }} controlShouldRenderValue={false} diff --git a/packages/hub/src/graphql/queries/search.ts b/packages/hub/src/graphql/queries/search.ts deleted file mode 100644 index dd9b1c7237..0000000000 --- a/packages/hub/src/graphql/queries/search.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; -import { Searchable } from "../types/Searchable"; -import { Searchable as PrismaSearchable } from "@prisma/client"; -import { resolveOffsetConnection } from "@pothos/plugin-relay"; - -type SearchableWithEdgeData = PrismaSearchable & { - rank: number; - slugSnippet: string; - textSnippet: string; -}; - -const SearchEdge = builder.edgeObject({ - type: Searchable, - name: "SearchEdge", - fields: (t) => ({ - rank: t.float({ - resolve: (obj) => { - const rank = (obj.node as SearchableWithEdgeData).rank; - return rank ?? 1e9; - }, - }), - slugSnippet: t.string({ - resolve: (obj) => { - const snippet = (obj.node as SearchableWithEdgeData).slugSnippet; - return String(snippet ?? ""); - }, - }), - textSnippet: t.string({ - resolve: (obj) => { - const snippet = (obj.node as SearchableWithEdgeData).textSnippet; - return String(snippet ?? ""); - }, - }), - }), -}); - -builder.queryField("search", (t) => - t.connection( - { - type: Searchable, - args: { - text: t.arg.string({ required: true }), - }, - errors: {}, - resolve: async (_, args, { session }) => { - return await resolveOffsetConnection( - { args }, - async ({ limit, offset }) => { - const { text } = args; - return await prisma.$queryRaw` - SELECT - "Searchable".*, - -- ranking function - ts_rank_cd( - setweight(to_tsvector(coalesce("Model".slug, '')), 'A') || - setweight(to_tsvector(coalesce("SquiggleSnippet".code, '')), 'B') || - setweight(to_tsvector(coalesce("RelativeValuesDefinition".slug, '')), 'A') || - setweight(to_tsvector(coalesce("UserOwner".slug, '')), 'A') || - setweight(to_tsvector(coalesce("GroupOwner".slug, '')), 'A'), - websearch_to_tsquery(${text}) - ) AS rank, - ts_headline( - concat_ws( - ' ', - "Model".slug, - "RelativeValuesDefinition".slug, - "UserOwner".slug, - "GroupOwner".slug - ), - websearch_to_tsquery(${text}), - 'HighlightAll=t' - ) AS "slugSnippet", - ts_headline(concat_ws( - ' ', - "SquiggleSnippet".code - ), websearch_to_tsquery(${text}) - ) AS "textSnippet" - FROM "Searchable" - LEFT JOIN "Model" ON "Model".id = "Searchable"."modelId" - LEFT JOIN "Owner" AS "ModelOwner" ON "Model"."ownerId" = "ModelOwner".id - LEFT JOIN "User" AS "ModelOwnerUser" ON "ModelOwner".id = "ModelOwnerUser"."ownerId" - LEFT JOIN "Group" AS "ModelOwnerGroup" ON "ModelOwner".id = "ModelOwnerGroup"."ownerId" - LEFT JOIN "ModelRevision" ON "Model"."currentRevisionId" = "ModelRevision".id - LEFT JOIN "SquiggleSnippet" ON "ModelRevision"."contentId" = "SquiggleSnippet".id - LEFT JOIN "RelativeValuesDefinition" ON "RelativeValuesDefinition".id = "Searchable"."definitionId" - -- LEFT JOIN "Owner" AS "RelativeValuesDefinitionOwner" ON "RelativeValuesDefinition"."ownerId" = "RelativeValuesDefinitionOwner".id - LEFT JOIN "User" ON "User".id = "Searchable"."userId" - LEFT JOIN "Owner" AS "UserOwner" ON "User"."ownerId" = "UserOwner".id - LEFT JOIN "Group" ON "Group".id = "Searchable"."groupId" - LEFT JOIN "Owner" AS "GroupOwner" ON "Group"."ownerId" = "GroupOwner".id - WHERE - ( - -- check permissions, should match "modelWhereHasAccess" function - "Model".id IS NULL OR - "Model"."isPrivate" IS FALSE - OR "ModelOwnerUser".email = ${session?.user.email} - OR "ModelOwnerGroup".id IN ( - SELECT DISTINCT "groupId" - FROM "UserGroupMembership" - LEFT JOIN "User" ON "User".id = "UserGroupMembership"."userId" - WHERE "User".email = ${session?.user.email} - ) - ) AND ( - -- construct search document - to_tsvector(concat_ws( - ' ', - "Model".slug, - "SquiggleSnippet".code, - "RelativeValuesDefinition".slug, - "UserOwner".slug, - "GroupOwner".slug - )) @@ websearch_to_tsquery(${text}) - ) - ORDER BY rank DESC - LIMIT ${limit} - OFFSET ${offset} - `; - } - ); - }, - }, - {}, - SearchEdge - ) -); diff --git a/packages/hub/src/graphql/types/Searchable.ts b/packages/hub/src/graphql/types/Searchable.ts deleted file mode 100644 index 23b905ec9c..0000000000 --- a/packages/hub/src/graphql/types/Searchable.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { - groupRoute, - modelRoute, - relativeValuesRoute, - userRoute, -} from "@/routes"; - -import { builder } from "../builder"; -import { Group } from "./Group"; -import { Model } from "./Model"; -import { RelativeValuesDefinition } from "./RelativeValuesDefinition"; -import { User } from "./User"; - -type Tag = "Model" | "RelativeValuesDefinition" | "User" | "Group"; - -function tagged(obj: any, tag: Tag) { - obj._searchableTag = tag; - return obj; -} - -const SearchableObject = builder.unionType("SearchableObject", { - types: [Model, RelativeValuesDefinition, User, Group], - resolveType: (wrappedObj) => { - const tag: Tag = (wrappedObj as any)._searchableTag; - switch (tag) { - case "Model": - return Model; - case "RelativeValuesDefinition": - return RelativeValuesDefinition; - case "User": - return User; - case "Group": - return Group; - default: - throw new Error("Incorrectly tagged object"); - } - }, -}); - -export const Searchable = builder.prismaNode("Searchable", { - id: { field: "id" }, - fields: (t) => ({ - link: t.string({ - select: { - model: { - select: { - owner: true, - slug: true, - }, - }, - definition: { - select: { owner: true, slug: true }, - }, - user: { - select: { asOwner: true }, - }, - group: { - select: { asOwner: true }, - }, - }, - resolve: (object) => { - // TODO - should be a field on object types - switch (true) { - case !!object.model: - return modelRoute({ - owner: object.model.owner.slug, - slug: object.model.slug, - }); - case !!object.definition: - return relativeValuesRoute({ - owner: object.definition.owner.slug, - slug: object.definition.slug, - }); - case !!object.user?.asOwner: - return userRoute({ username: object.user.asOwner.slug }); - case !!object.group: - return groupRoute({ slug: object.group.asOwner.slug }); - default: - throw new Error("Invalid Searchable record"); - } - }, - }), - object: t.field({ - type: SearchableObject, - select: { - // TODO: queryFromInfo, https://github.com/hayes/pothos/discussions/656#discussioncomment-4823928 - model: true, - definition: true, - user: true, - group: true, - }, - resolve: (object) => { - switch (true) { - case !!object.model: - return tagged(object.model, "Model"); - case !!object.definitionId: - return tagged(object.definition, "RelativeValuesDefinition"); - case !!object.userId: - return tagged(object.user, "User"); - case !!object.groupId: - return tagged(object.group, "Group"); - default: - throw new Error("Invalid Searchable record"); - } - }, - }), - }), -}); From 11dcdf9b4a08f78d2f71dc257d7d9d908bcb35ac Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 28 Nov 2024 23:50:00 -0300 Subject: [PATCH 36/68] convert group members page --- .../[slug]/members/AddUserToGroupAction.tsx | 78 +++------- .../[slug]/members/CancelInviteAction.tsx | 49 ------- .../[slug]/members/DeleteMembershipAction.tsx | 96 +++---------- .../groups/[slug]/members/GroupInviteCard.tsx | 60 -------- .../groups/[slug]/members/GroupInviteList.tsx | 60 -------- .../groups/[slug]/members/GroupMemberCard.tsx | 63 +++------ .../groups/[slug]/members/GroupMemberList.tsx | 87 +++++------- .../[slug]/members/GroupMembersPage.tsx | 66 --------- .../members/GroupReusableInviteSection.tsx | 102 ++++---------- .../[slug]/members/InviteRoleButton.tsx | 46 ------ .../[slug]/members/MembershipRoleButton.tsx | 48 ++----- .../[slug]/members/SetInviteRoleAction.tsx | 49 ------- .../members/SetMembershipRoleAction.tsx | 64 +++------ .../groups/[slug]/members/UserGroupInvite.tsx | 30 ---- .../src/app/groups/[slug]/members/page.tsx | 38 +++-- .../[owner]/[slug]/ModelAccessControls.tsx | 90 ------------ .../app/models/[owner]/[slug]/ModelLayout.tsx | 6 +- .../[owner]/[slug]/ModelPrivacyControls.tsx | 68 +++++++++ .../models/[owner]/[slug]/MoveModelAction.tsx | 1 - .../[owner]/[slug]/UpdateModelSlugAction.tsx | 1 - packages/hub/src/app/new/model/page.tsx | 2 +- .../src/components/ui/ServerActionButton.tsx | 1 + .../ui/ServerActionDropdownAction.tsx | 42 ++++++ .../components/ui/ServerActionModalAction.tsx | 12 +- .../hub/src/graphql/helpers/groupHelpers.ts | 29 ---- .../src/graphql/mutations/addUserToGroup.ts | 107 -------------- .../createReusableGroupInviteToken.ts | 49 ------- .../src/graphql/mutations/deleteMembership.ts | 69 --------- .../deleteReusableGroupInviteToken.ts | 38 ----- .../mutations/updateGroupInviteRole.ts | 64 --------- .../graphql/mutations/updateMembershipRole.ts | 77 ---------- packages/hub/src/graphql/schema.ts | 7 - packages/hub/src/hooks/usePaginator.ts | 42 +++++- .../groups/actions/addUserToGroupAction.ts | 106 ++++++++++++++ .../createReusableGroupInviteTokenAction.ts | 48 +++++++ .../groups/actions/deleteMembershipAction.ts | 67 +++++++++ .../deleteReusableGroupInviteTokenAction.ts | 41 ++++++ .../actions/updateMembershipRoleAction.ts | 73 ++++++++++ .../hub/src/server/groups/data/helpers.ts | 29 ++++ .../hub/src/server/groups/data/members.ts | 133 ++++++++++++++++++ 40 files changed, 832 insertions(+), 1306 deletions(-) delete mode 100644 packages/hub/src/app/groups/[slug]/members/CancelInviteAction.tsx delete mode 100644 packages/hub/src/app/groups/[slug]/members/GroupInviteCard.tsx delete mode 100644 packages/hub/src/app/groups/[slug]/members/GroupInviteList.tsx delete mode 100644 packages/hub/src/app/groups/[slug]/members/GroupMembersPage.tsx delete mode 100644 packages/hub/src/app/groups/[slug]/members/InviteRoleButton.tsx delete mode 100644 packages/hub/src/app/groups/[slug]/members/SetInviteRoleAction.tsx delete mode 100644 packages/hub/src/app/groups/[slug]/members/UserGroupInvite.tsx delete mode 100644 packages/hub/src/app/models/[owner]/[slug]/ModelAccessControls.tsx create mode 100644 packages/hub/src/app/models/[owner]/[slug]/ModelPrivacyControls.tsx create mode 100644 packages/hub/src/components/ui/ServerActionDropdownAction.tsx delete mode 100644 packages/hub/src/graphql/mutations/addUserToGroup.ts delete mode 100644 packages/hub/src/graphql/mutations/createReusableGroupInviteToken.ts delete mode 100644 packages/hub/src/graphql/mutations/deleteMembership.ts delete mode 100644 packages/hub/src/graphql/mutations/deleteReusableGroupInviteToken.ts delete mode 100644 packages/hub/src/graphql/mutations/updateGroupInviteRole.ts delete mode 100644 packages/hub/src/graphql/mutations/updateMembershipRole.ts create mode 100644 packages/hub/src/server/groups/actions/addUserToGroupAction.ts create mode 100644 packages/hub/src/server/groups/actions/createReusableGroupInviteTokenAction.ts create mode 100644 packages/hub/src/server/groups/actions/deleteMembershipAction.ts create mode 100644 packages/hub/src/server/groups/actions/deleteReusableGroupInviteTokenAction.ts create mode 100644 packages/hub/src/server/groups/actions/updateMembershipRoleAction.ts create mode 100644 packages/hub/src/server/groups/data/members.ts diff --git a/packages/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx b/packages/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx index 0314466896..7c936a9ae7 100644 --- a/packages/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx +++ b/packages/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx @@ -1,80 +1,38 @@ +import { MembershipRole } from "@prisma/client"; import { FC } from "react"; -import { useFragment } from "react-relay"; -import { ConnectionHandler, graphql } from "relay-runtime"; import { PlusIcon, SelectStringFormField } from "@quri/ui"; import { SelectUser, SelectUserOption } from "@/components/SelectUser"; -import { MutationModalAction } from "@/components/ui/MutationModalAction"; - -import { AddUserToGroupAction_group$key } from "@/__generated__/AddUserToGroupAction_group.graphql"; -import { AddUserToGroupActionMutation } from "@/__generated__/AddUserToGroupActionMutation.graphql"; -import { MembershipRole } from "@/__generated__/SetMembershipRoleActionMutation.graphql"; - -const Mutation = graphql` - mutation AddUserToGroupActionMutation( - $input: MutationAddUserToGroupInput! - $connections: [ID!]! - ) { - result: addUserToGroup(input: $input) { - __typename - ... on BaseError { - message - } - ... on AddUserToGroupResult { - membership - @appendNode( - connections: $connections - edgeTypeName: "UserGroupMembershipEdge" - ) { - ...GroupMemberCard - } - } - } - } -`; +import { ServerActionModalAction } from "@/components/ui/ServerActionModalAction"; +import { addUserToGroupAction } from "@/server/groups/actions/addUserToGroupAction"; +import { GroupMemberDTO } from "@/server/groups/data/members"; type Props = { - groupRef: AddUserToGroupAction_group$key; - close: () => void; + groupSlug: string; + append: (item: GroupMemberDTO) => void; }; type FormShape = { user: SelectUserOption; role: MembershipRole }; -export const AddUserToGroupAction: FC = ({ groupRef, close }) => { - const group = useFragment( - graphql` - fragment AddUserToGroupAction_group on Group { - id - slug - } - `, - groupRef - ); - +export const AddUserToGroupAction: FC = ({ groupSlug, append }) => { return ( - + title="Add" icon={PlusIcon} - close={close} - mutation={Mutation} - expectedTypename="AddUserToGroupResult" + action={async (data) => { + const membership = await addUserToGroupAction(data); + append(membership); + return membership; + }} defaultValues={{ role: "Member" }} formDataToVariables={(data) => ({ - input: { - group: group.slug, - username: data.user.slug, - role: data.role, - }, - connections: [ - ConnectionHandler.getConnectionID( - group.id, - "GroupMemberList_memberships" - ), - ], + group: groupSlug, + username: data.user.slug, + role: data.role, })} submitText="Add" - modalTitle={`Add to group ${group.slug}`} + modalTitle={`Add to group ${groupSlug}`} > {() => (
@@ -87,6 +45,6 @@ export const AddUserToGroupAction: FC = ({ groupRef, close }) => { />
)} - + ); }; diff --git a/packages/hub/src/app/groups/[slug]/members/CancelInviteAction.tsx b/packages/hub/src/app/groups/[slug]/members/CancelInviteAction.tsx deleted file mode 100644 index f88bddab76..0000000000 --- a/packages/hub/src/app/groups/[slug]/members/CancelInviteAction.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { FC } from "react"; -import { ConnectionHandler, graphql } from "relay-runtime"; - -import { TrashIcon } from "@quri/ui"; - -import { MutationAction } from "@/components/ui/MutationAction"; - -import { CancelInviteActionMutation } from "@/__generated__/CancelInviteActionMutation.graphql"; - -type Props = { - inviteId: string; - groupId: string; - close: () => void; -}; - -export const CancelInviteAction: FC = ({ inviteId, groupId, close }) => { - return ( - - title="Cancel" - icon={TrashIcon} - mutation={graphql` - mutation CancelInviteActionMutation( - $input: MutationCancelGroupInviteInput! - $connections: [ID!]! - ) { - result: cancelGroupInvite(input: $input) { - __typename - ... on BaseError { - message - } - ... on CancelGroupInviteResult { - invite { - id @deleteEdge(connections: $connections) - } - } - } - } - `} - expectedTypename="CancelGroupInviteResult" - variables={{ - input: { inviteId }, - connections: [ - ConnectionHandler.getConnectionID(groupId, "GroupInviteList_invites"), - ], - }} - close={close} - /> - ); -}; diff --git a/packages/hub/src/app/groups/[slug]/members/DeleteMembershipAction.tsx b/packages/hub/src/app/groups/[slug]/members/DeleteMembershipAction.tsx index aa57674192..f7b4579397 100644 --- a/packages/hub/src/app/groups/[slug]/members/DeleteMembershipAction.tsx +++ b/packages/hub/src/app/groups/[slug]/members/DeleteMembershipAction.tsx @@ -1,93 +1,33 @@ import { FC } from "react"; -import { useFragment } from "react-relay"; -import { ConnectionHandler, graphql } from "relay-runtime"; -import { DropdownMenuAsyncActionItem, TrashIcon } from "@quri/ui"; +import { TrashIcon } from "@quri/ui"; -import { useAsyncMutation } from "@/hooks/useAsyncMutation"; - -import { DeleteMembershipAction_Group$key } from "@/__generated__/DeleteMembershipAction_Group.graphql"; -import { DeleteMembershipAction_Membership$key } from "@/__generated__/DeleteMembershipAction_Membership.graphql"; -import { DeleteMembershipActionMutation } from "@/__generated__/DeleteMembershipActionMutation.graphql"; - -const Mutation = graphql` - mutation DeleteMembershipActionMutation( - $input: MutationDeleteMembershipInput! - ) { - result: deleteMembership(input: $input) { - __typename - ... on BaseError { - message - } - ... on DeleteMembershipResult { - ok - } - } - } -`; +import { ServerActionDropdownAction } from "@/components/ui/ServerActionDropdownAction"; +import { deleteMembershipAction } from "@/server/groups/actions/deleteMembershipAction"; +import { GroupMemberDTO } from "@/server/groups/data/members"; type Props = { - groupRef: DeleteMembershipAction_Group$key; - membershipRef: DeleteMembershipAction_Membership$key; - close: () => void; + groupSlug: string; + membership: GroupMemberDTO; + remove: (item: GroupMemberDTO) => void; }; export const DeleteMembershipAction: FC = ({ - groupRef, - membershipRef, - close, + groupSlug, + membership, + remove, }) => { - const [runMutation] = useAsyncMutation({ - mutation: Mutation, - expectedTypename: "DeleteMembershipResult", - }); - - const group = useFragment( - graphql` - fragment DeleteMembershipAction_Group on Group { - id - slug - } - `, - groupRef - ); - - const membership = useFragment( - graphql` - fragment DeleteMembershipAction_Membership on UserGroupMembership { - id - user { - slug - } - } - `, - membershipRef - ); - - const act = async () => { - await runMutation({ - variables: { - input: { group: group.slug, user: membership.user.slug }, - }, - updater: (store) => { - const groupRecord = store.get(group.id); - if (!groupRecord) return; - const connectionRecord = ConnectionHandler.getConnection( - groupRecord, - "GroupMemberList_memberships" - ); - if (!connectionRecord) return; - ConnectionHandler.deleteNode(connectionRecord, membership.id); - }, - }); - }; - return ( - { + await deleteMembershipAction({ + group: groupSlug, + username: membership.user.slug, + }); + remove(membership); + }} /> ); }; diff --git a/packages/hub/src/app/groups/[slug]/members/GroupInviteCard.tsx b/packages/hub/src/app/groups/[slug]/members/GroupInviteCard.tsx deleted file mode 100644 index 797abefcd4..0000000000 --- a/packages/hub/src/app/groups/[slug]/members/GroupInviteCard.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { FC } from "react"; -import { graphql, useFragment } from "react-relay"; - -import { DropdownMenu } from "@quri/ui"; - -import { Card } from "@/components/ui/Card"; -import { DotsDropdown } from "@/components/ui/DotsDropdown"; - -import { CancelInviteAction } from "./CancelInviteAction"; -import { InviteRoleButton } from "./InviteRoleButton"; -import { UserGroupInvite } from "./UserGroupInvite"; - -import { GroupInviteCard$key } from "@/__generated__/GroupInviteCard.graphql"; - -export const GroupInviteCard: FC<{ - inviteRef: GroupInviteCard$key; - groupId: string; -}> = (props) => { - const invite = useFragment( - graphql` - fragment GroupInviteCard on GroupInvite { - __typename - id - role - ...InviteRoleButton - ...UserGroupInvite - } - `, - props.inviteRef - ); - - return ( - -
-
- {invite.__typename === "UserGroupInvite" ? ( - - ) : ( - "Unknown invite type" - )} -
-
- Invited as: - - - {({ close }) => ( - - - - )} - -
-
-
- ); -}; diff --git a/packages/hub/src/app/groups/[slug]/members/GroupInviteList.tsx b/packages/hub/src/app/groups/[slug]/members/GroupInviteList.tsx deleted file mode 100644 index 3ea33884d5..0000000000 --- a/packages/hub/src/app/groups/[slug]/members/GroupInviteList.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { FC } from "react"; -import { usePaginationFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { LoadMore } from "@/components/LoadMore"; -import { H2 } from "@/components/ui/Headers"; - -import { GroupInviteCard } from "./GroupInviteCard"; - -import { GroupInviteList$key } from "@/__generated__/GroupInviteList.graphql"; -import { GroupInviteListPaginationQuery } from "@/__generated__/GroupInviteListPaginationQuery.graphql"; - -const fragment = graphql` - fragment GroupInviteList on Group - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 20 } - ) - @refetchable(queryName: "GroupInviteListPaginationQuery") { - invites(first: $count, after: $cursor) - @connection(key: "GroupInviteList_invites") { - edges { - node { - id - ...GroupInviteCard - } - } - pageInfo { - hasNextPage - } - } - } -`; - -type Props = { - groupRef: GroupInviteList$key; -}; - -export const GroupInviteList: FC = ({ groupRef }) => { - const { data: group, loadNext } = usePaginationFragment< - GroupInviteListPaginationQuery, - GroupInviteList$key - >(fragment, groupRef); - - return group.invites?.edges.length ? ( -
-

Pending invites

-
- {group.invites.edges.map(({ node: invite }) => ( - - ))} -
- {group.invites.pageInfo.hasNextPage && } -
- ) : null; -}; diff --git a/packages/hub/src/app/groups/[slug]/members/GroupMemberCard.tsx b/packages/hub/src/app/groups/[slug]/members/GroupMemberCard.tsx index 6ac649c61b..7b0e4e80e0 100644 --- a/packages/hub/src/app/groups/[slug]/members/GroupMemberCard.tsx +++ b/packages/hub/src/app/groups/[slug]/members/GroupMemberCard.tsx @@ -1,6 +1,4 @@ import { FC } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; import { DropdownMenu } from "@quri/ui"; @@ -8,68 +6,39 @@ import { Card } from "@/components/ui/Card"; import { DotsDropdown } from "@/components/ui/DotsDropdown"; import { StyledLink } from "@/components/ui/StyledLink"; import { userRoute } from "@/routes"; +import { GroupMemberDTO } from "@/server/groups/data/members"; -import { useIsGroupAdmin } from "../hooks"; import { DeleteMembershipAction } from "./DeleteMembershipAction"; import { MembershipRoleButton } from "./MembershipRoleButton"; -import { GroupMemberCard$key } from "@/__generated__/GroupMemberCard.graphql"; -import { GroupMemberCard_group$key } from "@/__generated__/GroupMemberCard_group.graphql"; - export const GroupMemberCard: FC<{ - membershipRef: GroupMemberCard$key; - groupRef: GroupMemberCard_group$key; -}> = ({ membershipRef, groupRef }) => { - const membership = useFragment( - graphql` - fragment GroupMemberCard on UserGroupMembership { - id - role - user { - id - username - } - ...DeleteMembershipAction_Membership - ...MembershipRoleButton_Membership - } - `, - membershipRef - ); - - const group = useFragment( - graphql` - fragment GroupMemberCard_group on Group { - id - ...hooks_useIsGroupAdmin - ...DeleteMembershipAction_Group - ...MembershipRoleButton_Group - } - `, - groupRef - ); - - const isAdmin = useIsGroupAdmin(group); - + groupSlug: string; + isAdmin: boolean; + membership: GroupMemberDTO; + remove: (membership: GroupMemberDTO) => void; + update: (membership: GroupMemberDTO) => void; +}> = ({ groupSlug, isAdmin, membership, remove, update }) => { return (
- - {membership.user.username} + + {membership.user.slug}
{isAdmin ? (
- {({ close }) => ( + {() => ( )} diff --git a/packages/hub/src/app/groups/[slug]/members/GroupMemberList.tsx b/packages/hub/src/app/groups/[slug]/members/GroupMemberList.tsx index 0e7aa9db1e..ccd052ced7 100644 --- a/packages/hub/src/app/groups/[slug]/members/GroupMemberList.tsx +++ b/packages/hub/src/app/groups/[slug]/members/GroupMemberList.tsx @@ -1,57 +1,44 @@ -import { FC } from "react"; -import { usePaginationFragment } from "react-relay"; -import { graphql } from "relay-runtime"; +"use client"; +import { FC, useCallback } from "react"; import { DropdownMenu } from "@quri/ui"; import { LoadMore } from "@/components/LoadMore"; import { DotsDropdown } from "@/components/ui/DotsDropdown"; import { H2 } from "@/components/ui/Headers"; +import { usePaginator } from "@/hooks/usePaginator"; +import { GroupMemberDTO } from "@/server/groups/data/members"; +import { Paginated } from "@/server/types"; -import { useIsGroupAdmin } from "../hooks"; import { AddUserToGroupAction } from "./AddUserToGroupAction"; import { GroupMemberCard } from "./GroupMemberCard"; -import { GroupMemberList$key } from "@/__generated__/GroupMemberList.graphql"; -import { GroupMemberListPaginationQuery } from "@/__generated__/GroupMemberListPaginationQuery.graphql"; - -const fragment = graphql` - fragment GroupMemberList on Group - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 20 } - ) - @refetchable(queryName: "GroupMemberListPaginationQuery") { - ...hooks_useIsGroupAdmin - ...AddUserToGroupAction_group - ...GroupMemberCard_group - - memberships(first: $count, after: $cursor) - @connection(key: "GroupMemberList_memberships") { - edges { - node { - id - ...GroupMemberCard - } - } - pageInfo { - hasNextPage - } - } - } -`; - type Props = { - groupRef: GroupMemberList$key; + groupSlug: string; + page: Paginated; + isAdmin: boolean; }; -export const GroupMemberList: FC = ({ groupRef }) => { - const { data: group, loadNext } = usePaginationFragment< - GroupMemberListPaginationQuery, - GroupMemberList$key - >(fragment, groupRef); +export const GroupMemberList: FC = ({ + groupSlug, + page: initialPage, + isAdmin, +}) => { + const page = usePaginator(initialPage); - const isAdmin = useIsGroupAdmin(group); + const updateMembership = useCallback( + (membership: GroupMemberDTO) => { + page.update((item) => (item.id === membership.id ? membership : item)); + }, + [page.update] + ); + + const removeMembership = useCallback( + (membership: GroupMemberDTO) => { + page.remove((item) => item.id === membership.id); + }, + [page.remove] + ); return (
@@ -59,26 +46,30 @@ export const GroupMemberList: FC = ({ groupRef }) => {

Members

{isAdmin && ( - {({ close }) => ( + {() => ( - + )} )}
- {group.memberships.edges.map(({ node: membership }) => ( + {page.items.map((membership) => ( ))}
- {group.memberships.pageInfo.hasNextPage && ( - - )} + {page.loadNext && }
); }; diff --git a/packages/hub/src/app/groups/[slug]/members/GroupMembersPage.tsx b/packages/hub/src/app/groups/[slug]/members/GroupMembersPage.tsx deleted file mode 100644 index 705d276314..0000000000 --- a/packages/hub/src/app/groups/[slug]/members/GroupMembersPage.tsx +++ /dev/null @@ -1,66 +0,0 @@ -"use client"; -import { FC } from "react"; -import { useSubscribeToInvalidationState } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; - -import { useIsGroupAdmin } from "../hooks"; -import { GroupInviteList } from "./GroupInviteList"; -import { GroupMemberList } from "./GroupMemberList"; -import { GroupReusableInviteSection } from "./GroupReusableInviteSection"; - -import { GroupMembersPageQuery } from "@/__generated__/GroupMembersPageQuery.graphql"; - -const Query = graphql` - query GroupMembersPageQuery($slug: String!) { - result: group(slug: $slug) { - __typename - ... on BaseError { - message - } - ... on NotFoundError { - message - } - ... on Group { - id - ...GroupMemberList - ...GroupInviteList - ...GroupReusableInviteSection - ...hooks_useIsGroupAdmin - } - } - } -`; - -export const GroupMembersPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [{ result }, { reload }] = usePageQuery(Query, query); - - const group = extractFromGraphqlErrorUnion(result, "Group"); - - useSubscribeToInvalidationState([group.id], reload); - - const isAdmin = useIsGroupAdmin(group); - - return ( -
-
- -
- {isAdmin && ( - <> -
- -
-
- -
- - )} -
- ); -}; diff --git a/packages/hub/src/app/groups/[slug]/members/GroupReusableInviteSection.tsx b/packages/hub/src/app/groups/[slug]/members/GroupReusableInviteSection.tsx index 3bcc3f1b52..0d73d10677 100644 --- a/packages/hub/src/app/groups/[slug]/members/GroupReusableInviteSection.tsx +++ b/packages/hub/src/app/groups/[slug]/members/GroupReusableInviteSection.tsx @@ -1,34 +1,24 @@ +"use client"; import { FC, useEffect, useMemo, useState } from "react"; import Skeleton from "react-loading-skeleton"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; import { ClipboardCopyIcon, TextTooltip, useToast } from "@quri/ui"; import { H2 } from "@/components/ui/Headers"; -import { MutationButton } from "@/components/ui/MutationButton"; +import { ServerActionButton } from "@/components/ui/ServerActionButton"; import { groupInviteLink } from "@/routes"; - -import { GroupReusableInviteSection$key } from "@/__generated__/GroupReusableInviteSection.graphql"; -import { GroupReusableInviteSection_CreateMutation } from "@/__generated__/GroupReusableInviteSection_CreateMutation.graphql"; -import { GroupReusableInviteSection_DeleteMutation } from "@/__generated__/GroupReusableInviteSection_DeleteMutation.graphql"; +import { createReusableGroupInviteTokenAction } from "@/server/groups/actions/createReusableGroupInviteTokenAction"; +import { deleteReusableGroupInviteTokenAction } from "@/server/groups/actions/deleteReusableGroupInviteTokenAction"; type Props = { - groupRef: GroupReusableInviteSection$key; + groupSlug: string; + reusableInviteToken: string | null; }; -export const GroupReusableInviteSection: FC = ({ groupRef }) => { - const group = useFragment( - graphql` - fragment GroupReusableInviteSection on Group { - id - slug - reusableInviteToken - } - `, - groupRef - ); - +export const GroupReusableInviteSection: FC = ({ + groupSlug, + reusableInviteToken, +}) => { const toast = useToast(); // Necessary for SSR and to avoid hydration errors @@ -36,12 +26,12 @@ export const GroupReusableInviteSection: FC = ({ groupRef }) => { useEffect(() => setOrigin(window.location.origin), []); const inviteLink = useMemo(() => { - if (!group.reusableInviteToken) { + if (!reusableInviteToken) { return undefined; } const routeArgs = { - groupSlug: group.slug, - inviteToken: group.reusableInviteToken, + groupSlug: groupSlug, + inviteToken: reusableInviteToken, }; const fullLink = groupInviteLink(routeArgs); const blurredLink = groupInviteLink({ ...routeArgs, blur: true }); @@ -50,7 +40,7 @@ export const GroupReusableInviteSection: FC = ({ groupRef }) => { full: `${origin}${fullLink}`, blurred: `${origin}${blurredLink}`, }; - }, [group.reusableInviteToken, group.slug, origin]); + }, [reusableInviteToken, groupSlug, origin]); const copy = () => { if (!inviteLink) { @@ -82,63 +72,19 @@ export const GroupReusableInviteSection: FC = ({ groupRef }) => { ) ) : null}
- - mutation={graphql` - mutation GroupReusableInviteSection_CreateMutation( - $input: MutationCreateReusableGroupInviteTokenInput! - ) { - result: createReusableGroupInviteToken(input: $input) { - __typename - ... on BaseError { - message - } - ... on CreateReusableGroupInviteTokenResult { - group { - reusableInviteToken - } - } - } - } - `} - expectedTypename="CreateReusableGroupInviteTokenResult" - variables={{ - input: { slug: group.slug }, - }} + + createReusableGroupInviteTokenAction({ slug: groupSlug }) + } title={ - group.reusableInviteToken - ? "Reset Invite Link" - : "Create Invite Link" + reusableInviteToken ? "Reset Invite Link" : "Create Invite Link" } /> - {group.reusableInviteToken ? ( - - mutation={graphql` - mutation GroupReusableInviteSection_DeleteMutation( - $input: MutationDeleteReusableGroupInviteTokenInput! - ) { - result: deleteReusableGroupInviteToken(input: $input) { - __typename - ... on BaseError { - message - } - ... on DeleteReusableGroupInviteTokenResult { - group { - reusableInviteToken - } - } - } - } - `} - expectedTypename="DeleteReusableGroupInviteTokenResult" - variables={{ - input: { slug: group.slug }, - }} + {reusableInviteToken ? ( + + deleteReusableGroupInviteTokenAction({ slug: groupSlug }) + } title="Delete Invite Link" /> ) : null} diff --git a/packages/hub/src/app/groups/[slug]/members/InviteRoleButton.tsx b/packages/hub/src/app/groups/[slug]/members/InviteRoleButton.tsx deleted file mode 100644 index 2917de36d0..0000000000 --- a/packages/hub/src/app/groups/[slug]/members/InviteRoleButton.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { FC } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { Button, Dropdown, DropdownMenu } from "@quri/ui"; - -import { SetInviteRoleButton } from "./SetInviteRoleAction"; - -import { - InviteRoleButton$key, - MembershipRole, -} from "@/__generated__/InviteRoleButton.graphql"; - -const Fragment = graphql` - fragment InviteRoleButton on GroupInvite { - id - role - } -`; - -type Props = { - inviteRef: InviteRoleButton$key; -}; - -export const InviteRoleButton: FC = ({ inviteRef }) => { - const invite = useFragment(Fragment, inviteRef); - - return ( - ( - - {(["Admin", "Member"] satisfies MembershipRole[]).map((role) => ( - - ))} - - )} - > - - - ); -}; diff --git a/packages/hub/src/app/groups/[slug]/members/MembershipRoleButton.tsx b/packages/hub/src/app/groups/[slug]/members/MembershipRoleButton.tsx index a402ab9fcd..ee7f3c0fb9 100644 --- a/packages/hub/src/app/groups/[slug]/members/MembershipRoleButton.tsx +++ b/packages/hub/src/app/groups/[slug]/members/MembershipRoleButton.tsx @@ -1,57 +1,35 @@ import { FC } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; import { Button, Dropdown, DropdownMenu } from "@quri/ui"; +import { GroupMemberDTO } from "@/server/groups/data/members"; + import { SetMembershipRoleAction } from "./SetMembershipRoleAction"; -import { MembershipRoleButton_Group$key } from "@/__generated__/MembershipRoleButton_Group.graphql"; -import { - MembershipRole, - MembershipRoleButton_Membership$key, -} from "@/__generated__/MembershipRoleButton_Membership.graphql"; +import { MembershipRole } from "@/__generated__/MembershipRoleButton_Membership.graphql"; type Props = { - membershipRef: MembershipRoleButton_Membership$key; - groupRef: MembershipRoleButton_Group$key; + groupSlug: string; + membership: GroupMemberDTO; + update: (membership: GroupMemberDTO) => void; }; export const MembershipRoleButton: FC = ({ - membershipRef, - groupRef, + membership, + groupSlug, + update, }) => { - const group = useFragment( - graphql` - fragment MembershipRoleButton_Group on Group { - ...SetMembershipRoleAction_Group - } - `, - groupRef - ); - - const membership = useFragment( - graphql` - fragment MembershipRoleButton_Membership on UserGroupMembership { - id - role - ...SetMembershipRoleAction_Membership - } - `, - membershipRef - ); - return ( ( + render={() => ( {(["Admin", "Member"] satisfies MembershipRole[]).map((role) => ( ))} diff --git a/packages/hub/src/app/groups/[slug]/members/SetInviteRoleAction.tsx b/packages/hub/src/app/groups/[slug]/members/SetInviteRoleAction.tsx deleted file mode 100644 index 01728c673a..0000000000 --- a/packages/hub/src/app/groups/[slug]/members/SetInviteRoleAction.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { FC } from "react"; -import { graphql } from "relay-runtime"; - -import { MutationAction } from "@/components/ui/MutationAction"; - -import { SetInviteRoleActionMutation } from "@/__generated__/SetInviteRoleActionMutation.graphql"; -import { MembershipRole } from "@/__generated__/SetMembershipRoleActionMutation.graphql"; - -const Mutation = graphql` - mutation SetInviteRoleActionMutation( - $input: MutationUpdateGroupInviteRoleInput! - ) { - result: updateGroupInviteRole(input: $input) { - __typename - ... on BaseError { - message - } - ... on UpdateGroupInviteRoleResult { - invite { - id - role - } - } - } - } -`; - -type Props = { - inviteId: string; - role: MembershipRole; - close: () => void; -}; - -export const SetInviteRoleButton: FC = ({ inviteId, role, close }) => { - return ( - - mutation={Mutation} - variables={{ - input: { - inviteId, - role, - }, - }} - expectedTypename="UpdateGroupInviteRoleResult" - title={role} - close={close} - /> - ); -}; diff --git a/packages/hub/src/app/groups/[slug]/members/SetMembershipRoleAction.tsx b/packages/hub/src/app/groups/[slug]/members/SetMembershipRoleAction.tsx index 92a676f21e..89693d24e6 100644 --- a/packages/hub/src/app/groups/[slug]/members/SetMembershipRoleAction.tsx +++ b/packages/hub/src/app/groups/[slug]/members/SetMembershipRoleAction.tsx @@ -1,15 +1,10 @@ +import { MembershipRole } from "@prisma/client"; import { FC } from "react"; -import { useFragment } from "react-relay"; import { graphql } from "relay-runtime"; -import { MutationAction } from "@/components/ui/MutationAction"; - -import { SetMembershipRoleAction_Group$key } from "@/__generated__/SetMembershipRoleAction_Group.graphql"; -import { SetMembershipRoleAction_Membership$key } from "@/__generated__/SetMembershipRoleAction_Membership.graphql"; -import { - MembershipRole, - SetMembershipRoleActionMutation, -} from "@/__generated__/SetMembershipRoleActionMutation.graphql"; +import { ServerActionDropdownAction } from "@/components/ui/ServerActionDropdownAction"; +import { updateMembershipRoleAction } from "@/server/groups/actions/updateMembershipRoleAction"; +import { GroupMemberDTO } from "@/server/groups/data/members"; const Mutation = graphql` mutation SetMembershipRoleActionMutation( @@ -31,56 +26,29 @@ const Mutation = graphql` `; type Props = { - groupRef: SetMembershipRoleAction_Group$key; - membershipRef: SetMembershipRoleAction_Membership$key; + membership: GroupMemberDTO; + groupSlug: string; role: MembershipRole; - close: () => void; + update: (membership: GroupMemberDTO) => void; }; export const SetMembershipRoleAction: FC = ({ - groupRef, - membershipRef, + membership, + groupSlug, role, - close, + update, }) => { - const group = useFragment( - graphql` - fragment SetMembershipRoleAction_Group on Group { - id - slug - } - `, - groupRef - ); - - const membership = useFragment( - graphql` - fragment SetMembershipRoleAction_Membership on UserGroupMembership { - id - user { - slug - } - } - `, - membershipRef - ); - return ( - - mutation={Mutation} - variables={{ - input: { + { + const newMembership = await updateMembershipRoleAction({ user: membership.user.slug, - group: group.slug, + group: groupSlug, role, - }, + }); + update(newMembership); }} - expectedTypename="UpdateMembershipRoleResult" title={role} - close={close} /> ); }; diff --git a/packages/hub/src/app/groups/[slug]/members/UserGroupInvite.tsx b/packages/hub/src/app/groups/[slug]/members/UserGroupInvite.tsx deleted file mode 100644 index abfae56b53..0000000000 --- a/packages/hub/src/app/groups/[slug]/members/UserGroupInvite.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { FC } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { StyledLink } from "@/components/ui/StyledLink"; -import { userRoute } from "@/routes"; - -import { UserGroupInvite$key } from "@/__generated__/UserGroupInvite.graphql"; - -export const UserGroupInvite: FC<{ inviteRef: UserGroupInvite$key }> = ({ - inviteRef, -}) => { - const invite = useFragment( - graphql` - fragment UserGroupInvite on UserGroupInvite { - id - user { - username - } - } - `, - inviteRef - ); - - return ( - - {invite.user.username} - - ); -}; diff --git a/packages/hub/src/app/groups/[slug]/members/page.tsx b/packages/hub/src/app/groups/[slug]/members/page.tsx index cb180889c2..0e2c417116 100644 --- a/packages/hub/src/app/groups/[slug]/members/page.tsx +++ b/packages/hub/src/app/groups/[slug]/members/page.tsx @@ -1,10 +1,11 @@ -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { + loadGroupMembers, + loadMyMembership, + loadReusableInviteToken, +} from "@/server/groups/data/members"; -import { GroupMembersPage } from "./GroupMembersPage"; - -import QueryNode, { - GroupMembersPageQuery, -} from "@/__generated__/GroupMembersPageQuery.graphql"; +import { GroupMemberList } from "./GroupMemberList"; +import { GroupReusableInviteSection } from "./GroupReusableInviteSection"; type Props = { params: Promise<{ slug: string }>; @@ -12,9 +13,26 @@ type Props = { export default async function OuterGroupMembersPage({ params }: Props) { const { slug } = await params; - const query = await loadPageQuery(QueryNode, { - slug, - }); + const members = await loadGroupMembers({ groupSlug: slug }); + + const myMembership = await loadMyMembership({ groupSlug: slug }); + const isAdmin = myMembership?.role === "Admin"; - return ; + return ( +
+
+ +
+ {isAdmin && ( +
+ +
+ )} +
+ ); } diff --git a/packages/hub/src/app/models/[owner]/[slug]/ModelAccessControls.tsx b/packages/hub/src/app/models/[owner]/[slug]/ModelAccessControls.tsx deleted file mode 100644 index b132a953e3..0000000000 --- a/packages/hub/src/app/models/[owner]/[slug]/ModelAccessControls.tsx +++ /dev/null @@ -1,90 +0,0 @@ -"use client"; -import { clsx } from "clsx"; -import { FC, useEffect, useState, useTransition } from "react"; - -import { - Dropdown, - DropdownMenu, - DropdownMenuActionItem, - GlobeIcon, - LockIcon, - RefreshIcon, -} from "@quri/ui"; - -import { updateModelPrivacyAction } from "@/server/models/actions/updateModelPrivacyAction"; -import { ModelCardDTO } from "@/server/models/data/card"; - -function getIconComponent(isPrivate: boolean) { - return isPrivate ? LockIcon : GlobeIcon; -} - -const UpdatePrivacyAction: FC<{ - model: ModelCardDTO; - close: () => void; -}> = ({ model, close }) => { - const [initialIsPrivate] = useState(model.isPrivate); - const [isPending, startTransition] = useTransition(); - const act = () => { - startTransition(async () => { - await updateModelPrivacyAction({ - owner: model.owner.slug, - slug: model.slug, - isPrivate: !model.isPrivate, - }); - }); - }; - - // We can't just call `close()` in the transition; server action finishes before it sends back the revalidated UI. - // This is an ugly workaround; see also: https://github.com/vercel/next.js/discussions/53206 - // Discussion in QURI Slack: https://quri.slack.com/archives/C059EEU0HMM/p1732810277978719 - useEffect(() => { - if (model.isPrivate !== initialIsPrivate) { - close(); - } - }, [model.isPrivate, initialIsPrivate, close]); - - return ( - - ); -}; - -export const ModelAccessControls: FC<{ - model: ModelCardDTO; - isEditable: boolean; -}> = ({ model, isEditable }) => { - const { isPrivate } = model; - - const Icon = getIconComponent(isPrivate); - - const body = ( - // TODO: copy-pasted from CacheMenu from relative-values, extract to or something -
- - {isPrivate ? "Private" : "Public"} -
- ); - - return isEditable ? ( - ( - - - - )} - > - {body} - - ) : ( - body - ); -}; diff --git a/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx b/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx index 645ec03052..99025f9328 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx @@ -15,8 +15,8 @@ import { modelRevisionsRoute, modelRoute } from "@/routes"; import { ModelCardDTO } from "@/server/models/data/card"; import { getExportedVariableNames } from "@/server/models/utils"; -import { ModelAccessControls } from "./ModelAccessControls"; import { ModelEntityNodes } from "./ModelEntityNodes"; +import { ModelPrivacyControls } from "./ModelPrivacyControls"; import { ModelSettingsButton } from "./ModelSettingsButton"; import { useFixModelUrlCasing } from "./useFixModelUrlCasing"; @@ -65,7 +65,9 @@ export const ModelLayout: FC< } isFluid={true} - headerLeft={} + headerLeft={ + + } headerRight={ = ({ model }) => { + return ( + { + await updateModelPrivacyAction({ + owner: model.owner.slug, + slug: model.slug, + isPrivate: !model.isPrivate, + }); + }} + title={model.isPrivate ? "Make public" : "Make private"} + icon={getIconComponent(!model.isPrivate)} + invariant={model.isPrivate} + /> + ); +}; + +export const ModelPrivacyControls: FC<{ + model: ModelCardDTO; + isEditable: boolean; +}> = ({ model, isEditable }) => { + const { isPrivate } = model; + + const Icon = getIconComponent(isPrivate); + + const body = ( + // TODO: copy-pasted from CacheMenu from relative-values, extract to or something +
+ + {isPrivate ? "Private" : "Public"} +
+ ); + + return isEditable ? ( + ( + + + + )} + > + {body} + + ) : ( + body + ); +}; diff --git a/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx index d65064dadd..1b2a2c8765 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx @@ -47,7 +47,6 @@ export const MoveModelAction: FC = ({ model, close }) => { }} icon={RightArrowIcon} action={moveModelAction} - close={close} initialFocus="owner" blockOnSuccess > diff --git a/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx index f7b63cd77d..8c53ac1442 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx @@ -43,7 +43,6 @@ export const UpdateModelSlugAction: FC = ({ model, close }) => { submitText="Save" modalTitle={`Rename ${model.owner.slug}/${model.slug}`} initialFocus="slug" - close={close} > {() => (
diff --git a/packages/hub/src/app/new/model/page.tsx b/packages/hub/src/app/new/model/page.tsx index 3571c009e3..94de025aa7 100644 --- a/packages/hub/src/app/new/model/page.tsx +++ b/packages/hub/src/app/new/model/page.tsx @@ -7,7 +7,7 @@ import { getSessionUserOrRedirect } from "@/server/users/auth"; import { NewModel } from "./NewModel"; -export default async function OuterNewModelPage({ +export default async function NewModelPage({ searchParams, }: { searchParams: Promise<{ diff --git a/packages/hub/src/components/ui/ServerActionButton.tsx b/packages/hub/src/components/ui/ServerActionButton.tsx index 4665b3b155..16dfaf7794 100644 --- a/packages/hub/src/components/ui/ServerActionButton.tsx +++ b/packages/hub/src/components/ui/ServerActionButton.tsx @@ -17,6 +17,7 @@ export function ServerActionButton({ action: () => Promise; title: string; } & Pick[0], "theme" | "size">): ReactNode { + // TODO - pending based on invariant, similar to ServerActionDropdownAction const [, formAction, isPending] = useActionState(async () => { await action(); }, undefined); diff --git a/packages/hub/src/components/ui/ServerActionDropdownAction.tsx b/packages/hub/src/components/ui/ServerActionDropdownAction.tsx new file mode 100644 index 0000000000..028aa704fc --- /dev/null +++ b/packages/hub/src/components/ui/ServerActionDropdownAction.tsx @@ -0,0 +1,42 @@ +import { FC, useEffect, useState, useTransition } from "react"; + +import { DropdownMenuActionItem, IconProps, useCloseDropdown } from "@quri/ui"; + +export const ServerActionDropdownAction: FC<{ + title: string; + icon?: FC; + act: () => Promise; + // If set, the dropdown will close only when the invariant changes. + invariant?: unknown; +}> = ({ title, icon, act: originalAct, invariant }) => { + const [initialInvariant] = useState(invariant); + const close = useCloseDropdown(); + + const [isPending, startTransition] = useTransition(); + const act = () => { + startTransition(async () => { + await originalAct(); + if (invariant === undefined) { + close(); + } + }); + }; + + // We can't just call `close()` in the transition; server action finishes before it sends back the revalidated UI. + // This is an ugly workaround; see also: https://github.com/vercel/next.js/discussions/53206 + // Discussion in QURI Slack: https://quri.slack.com/archives/C059EEU0HMM/p1732810277978719 + useEffect(() => { + if (invariant !== initialInvariant) { + close(); + } + }, [invariant, initialInvariant, close]); + + return ( + + ); +}; diff --git a/packages/hub/src/components/ui/ServerActionModalAction.tsx b/packages/hub/src/components/ui/ServerActionModalAction.tsx index 6788888845..11bbc03f7e 100644 --- a/packages/hub/src/components/ui/ServerActionModalAction.tsx +++ b/packages/hub/src/components/ui/ServerActionModalAction.tsx @@ -1,7 +1,11 @@ import { FC, PropsWithChildren, ReactNode } from "react"; import { FieldPath, FieldValues } from "react-hook-form"; -import { DropdownMenuModalActionItem, IconProps } from "@quri/ui"; +import { + DropdownMenuModalActionItem, + IconProps, + useCloseDropdown, +} from "@quri/ui"; import { FormModal } from "@/components/ui/FormModal"; import { useServerActionForm } from "@/hooks/useServerActionForm"; @@ -19,7 +23,6 @@ type CommonProps< > & { initialFocus?: FieldPath; submitText: string; - close: () => void; }; function ServerActionFormModal< @@ -32,12 +35,14 @@ function ServerActionFormModal< submitText, action, onCompleted, - close, title, children, }: PropsWithChildren> & { title: string; }): ReactNode { + // Note that we use the same `close` that's responsible for closing the dropdown. + const close = useCloseDropdown(); + const { form, onSubmit, inFlight } = useServerActionForm({ mode: "onChange", defaultValues, @@ -85,7 +90,6 @@ export function ServerActionModalAction< icon={icon} render={() => ( - // Note that we pass the same `close` that's responsible for closing the dropdown. {...modalProps} title={modalTitle} > diff --git a/packages/hub/src/graphql/helpers/groupHelpers.ts b/packages/hub/src/graphql/helpers/groupHelpers.ts index 70a1de298d..05445169ea 100644 --- a/packages/hub/src/graphql/helpers/groupHelpers.ts +++ b/packages/hub/src/graphql/helpers/groupHelpers.ts @@ -91,32 +91,3 @@ export async function getMyMembership({ }); return myMembership; } - -// also returns true if user is not an admin -export async function groupHasAdminsBesidesUser({ - groupSlug, - userSlug, -}: { - groupSlug: string; - userSlug: string; -}) { - return Boolean( - await prisma.userGroupMembership.count({ - where: { - group: { - asOwner: { - slug: groupSlug, - }, - }, - NOT: { - user: { - asOwner: { - slug: userSlug, - }, - }, - }, - role: "Admin", - }, - }) - ); -} diff --git a/packages/hub/src/graphql/mutations/addUserToGroup.ts b/packages/hub/src/graphql/mutations/addUserToGroup.ts deleted file mode 100644 index 1f7780ce35..0000000000 --- a/packages/hub/src/graphql/mutations/addUserToGroup.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { ZodError } from "zod"; - -import { prisma } from "@/prisma"; - -import { builder } from "../builder"; -import { MembershipRoleType, UserGroupMembership } from "../types/Group"; -import { validateSlug } from "../utils"; - -builder.mutationField("addUserToGroup", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("AddUserToGroupResult", { - fields: (t) => ({ - membership: t.field({ type: UserGroupMembership }), - }), - }), - errors: { types: [ZodError] }, - input: { - group: t.input.string({ required: true, validate: validateSlug }), - username: t.input.string({ required: true, validate: validateSlug }), - role: t.input.field({ - type: MembershipRoleType, - required: true, - }), - }, - resolve: async (_, { input }, { session }) => { - const membership = await prisma.$transaction(async (tx) => { - const groupOwner = await tx.owner.findUnique({ - where: { - slug: input.group, - }, - }); - if (!groupOwner) { - throw new Error(`Group ${input.group} not found`); - } - - const requestedUser = await tx.user.findFirst({ - where: { - asOwner: { - slug: input.username, - }, - }, - }); - - if (!requestedUser) { - throw new Error(`User ${input.username} not found`); - } - - // We perform all checks one by one because that allows more precise error reporting. - // (It would be possible to check everything in one big query with clever nested `connect` checks.) - const isAdmin = await tx.group.count({ - where: { - ownerId: groupOwner.id, - memberships: { - some: { - user: { email: session.user.email }, - role: "Admin", - }, - }, - }, - }); - if (!isAdmin) { - throw new Error(`You're not an admin of ${input.group} group`); - } - - const alreadyAMember = await tx.group.count({ - where: { - ownerId: groupOwner.id, - memberships: { - some: { userId: requestedUser.id }, - }, - }, - }); - if (alreadyAMember) { - throw new Error( - `${input.username} is already a member of ${input.group}` - ); - } - - // Cancel all pending invites - await tx.groupInvite.updateMany({ - where: { - userId: requestedUser.id, - groupId: groupOwner.id, - status: "Pending", - }, - data: { - status: "Canceled", - }, - }); - - return await tx.userGroupMembership.create({ - data: { - user: { - connect: { id: requestedUser.id }, - }, - group: { - connect: { ownerId: groupOwner.id }, - }, - role: input.role, - }, - }); - }); - - return { membership }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/createReusableGroupInviteToken.ts b/packages/hub/src/graphql/mutations/createReusableGroupInviteToken.ts deleted file mode 100644 index 89a3c0a4bf..0000000000 --- a/packages/hub/src/graphql/mutations/createReusableGroupInviteToken.ts +++ /dev/null @@ -1,49 +0,0 @@ -import crypto from "crypto"; - -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { Group } from "../types/Group"; - -builder.mutationField("createReusableGroupInviteToken", (t) => - t.fieldWithInput({ - description: `Create or replace a reusable invite token for a group, available as \`reusableInviteToken\` field on group object. - -You must be an admin of the group to call this mutation. Previous invite token, if it existed, will stop working.`, - type: builder.simpleObject("CreateReusableGroupInviteTokenResult", { - fields: (t) => ({ - group: t.field({ type: Group }), - }), - }), - errors: {}, - authScopes: (_, { input }) => ({ - isGroupAdminBySlug: input.slug, - }), - input: { - slug: t.input.string({ required: true }), - }, - resolve: async (_, { input }) => { - let group = await prisma.group.findFirstOrThrow({ - where: { - asOwner: { - slug: input.slug, - }, - }, - }); - - const token = crypto.randomBytes(30).toString("hex"); - - group = await prisma.group.update({ - where: { - id: group.id, - }, - data: { - // old token will be overwritten, that's fine - reusableInviteToken: token, - }, - }); - - return { group }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/deleteMembership.ts b/packages/hub/src/graphql/mutations/deleteMembership.ts deleted file mode 100644 index 6140c2aeb7..0000000000 --- a/packages/hub/src/graphql/mutations/deleteMembership.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { prisma } from "@/prisma"; - -import { builder } from "../builder"; -import { - getMembership, - getMyMembership, - groupHasAdminsBesidesUser, -} from "../helpers/groupHelpers"; - -builder.mutationField("deleteMembership", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("DeleteMembershipResult", { - fields: (t) => ({ - ok: t.boolean(), - }), - }), - errors: {}, - input: { - group: t.input.string({ required: true }), - user: t.input.string({ required: true }), - }, - resolve: async (_, { input }, { session }) => { - // somewhat repetitive compared to `updateMembershipRole`, but with slightly different error messages - const myMembership = await getMyMembership({ - groupSlug: input.group, - session, - }); - - if (!myMembership) { - throw new Error("You're not a member of this group"); - } - - if ( - input.user !== session.user.username && - myMembership.role !== "Admin" - ) { - throw new Error("Only admins can delete other members"); - } - - const membershipToDelete = await getMembership({ - groupSlug: input.group, - userSlug: input.user, - }); - - if (!membershipToDelete) { - throw new Error(`${input.user} is not a member of ${input.group}`); - } - - if ( - !(await groupHasAdminsBesidesUser({ - groupSlug: input.group, - userSlug: input.user, - })) - ) { - throw new Error( - `Can't delete, ${input.user} is the last admin of ${input.group}` - ); - } - - await prisma.userGroupMembership.delete({ - where: { - id: membershipToDelete.id, - }, - }); - - return { ok: true }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/deleteReusableGroupInviteToken.ts b/packages/hub/src/graphql/mutations/deleteReusableGroupInviteToken.ts deleted file mode 100644 index c1d0f36465..0000000000 --- a/packages/hub/src/graphql/mutations/deleteReusableGroupInviteToken.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { Group } from "../types/Group"; - -builder.mutationField("deleteReusableGroupInviteToken", (t) => - t.fieldWithInput({ - description: `Disable a reusable invite token for a group.`, - type: builder.simpleObject("DeleteReusableGroupInviteTokenResult", { - fields: (t) => ({ - group: t.field({ type: Group }), - }), - }), - errors: {}, - authScopes: (_, { input }) => ({ - isGroupAdminBySlug: input.slug, - }), - input: { - slug: t.input.string({ required: true }), - }, - resolve: async (_, { input }) => { - let group = await prisma.group.findFirstOrThrow({ - where: { - asOwner: { - slug: input.slug, - }, - }, - }); - - group = await prisma.group.update({ - where: { id: group.id }, - data: { reusableInviteToken: null }, - }); - - return { group }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/updateGroupInviteRole.ts b/packages/hub/src/graphql/mutations/updateGroupInviteRole.ts deleted file mode 100644 index 1ffdf8aac1..0000000000 --- a/packages/hub/src/graphql/mutations/updateGroupInviteRole.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { MembershipRole } from "@prisma/client"; - -import { prisma } from "@/prisma"; - -import { builder } from "../builder"; -import { MembershipRoleType } from "../types/Group"; -import { GroupInvite } from "../types/GroupInvite"; -import { decodeGlobalIdWithTypename } from "../utils"; - -builder.mutationField("updateGroupInviteRole", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("UpdateGroupInviteRoleResult", { - fields: (t) => ({ - invite: t.field({ type: GroupInvite }), - }), - }), - errors: {}, - input: { - inviteId: t.input.string({ required: true }), - role: t.input.field({ - type: MembershipRoleType, - required: true, - }), - }, - resolve: async (_, { input }, { session }) => { - const user = await prisma.user.findUniqueOrThrow({ - where: { email: session.user.email }, - }); - - const decodedInviteId = decodeGlobalIdWithTypename(input.inviteId, [ - "UserGroupInvite", - "EmailGroupInvite", - ]); - - const invite = await prisma.groupInvite.findUniqueOrThrow({ - where: { id: decodedInviteId }, - include: { group: true }, - }); - if (invite.role === input.role) { - return { invite }; // nothing to do - } - - const myMembership = await prisma.userGroupMembership.findUnique({ - where: { - userId_groupId: { - userId: user.id, - groupId: invite.groupId, - }, - }, - }); - - if (myMembership?.role !== MembershipRole.Admin) { - throw new Error("You're not an admin of this group"); - } - - const updatedInvite = await prisma.groupInvite.update({ - where: { id: invite.id }, - data: { role: input.role }, - }); - - return { invite: updatedInvite }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/updateMembershipRole.ts b/packages/hub/src/graphql/mutations/updateMembershipRole.ts deleted file mode 100644 index a1d6f67eef..0000000000 --- a/packages/hub/src/graphql/mutations/updateMembershipRole.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { prisma } from "@/prisma"; - -import { builder } from "../builder"; -import { - getMembership, - getMyMembership, - groupHasAdminsBesidesUser, -} from "../helpers/groupHelpers"; -import { MembershipRoleType, UserGroupMembership } from "../types/Group"; - -builder.mutationField("updateMembershipRole", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("UpdateMembershipRoleResult", { - fields: (t) => ({ - membership: t.field({ type: UserGroupMembership }), - }), - }), - errors: {}, - input: { - group: t.input.string({ required: true }), - user: t.input.string({ required: true }), - role: t.input.field({ - type: MembershipRoleType, - required: true, - }), - }, - resolve: async (_, { input }, { session }) => { - // somewhat repetitive compared to `deleteMembership`, but with slightly different error messages - const myMembership = await getMyMembership({ - groupSlug: input.group, - session, - }); - - if (!myMembership) { - throw new Error("You're not a member of this group"); - } - - if ( - input.user !== session.user.username && - myMembership.role !== "Admin" - ) { - throw new Error("Only admins can update other members roles"); - } - - const membershipToUpdate = await getMembership({ - groupSlug: input.group, - userSlug: input.user, - }); - - if (!membershipToUpdate) { - throw new Error(`${input.user} is not a member of ${input.group}`); - } - - if (membershipToUpdate.role === input.role) { - return { membership: membershipToUpdate }; // nothing to do - } - - if ( - !(await groupHasAdminsBesidesUser({ - groupSlug: input.group, - userSlug: input.user, - })) - ) { - throw new Error( - `Can't change the role, ${input.user} is the last admin of ${input.group}` - ); - } - - const updatedMembership = await prisma.userGroupMembership.update({ - where: { id: membershipToUpdate.id }, - data: { role: input.role }, - }); - - return { membership: updatedMembership }; - }, - }) -); diff --git a/packages/hub/src/graphql/schema.ts b/packages/hub/src/graphql/schema.ts index 05494179f1..ba072d3024 100644 --- a/packages/hub/src/graphql/schema.ts +++ b/packages/hub/src/graphql/schema.ts @@ -8,7 +8,6 @@ import "./queries/variable"; import "./queries/relativeValuesDefinition"; import "./queries/relativeValuesDefinitions"; import "./queries/runSquiggle"; -import "./queries/search"; import "./queries/userByUsername"; import "./mutations/adminUpdateModelVersion"; import "./mutations/adminRebuildSearchIndex"; @@ -17,13 +16,7 @@ import "./mutations/cancelGroupInvite"; import "./mutations/clearRelativeValuesCache"; import "./mutations/createGroup"; import "./mutations/createRelativeValuesDefinition"; -import "./mutations/createReusableGroupInviteToken"; -import "./mutations/deleteMembership"; import "./mutations/deleteRelativeValuesDefinition"; -import "./mutations/deleteReusableGroupInviteToken"; -import "./mutations/addUserToGroup"; -import "./mutations/updateGroupInviteRole"; -import "./mutations/updateMembershipRole"; import "./mutations/updateRelativeValuesDefinition"; import "./mutations/updateSquiggleSnippetModel"; diff --git a/packages/hub/src/hooks/usePaginator.ts b/packages/hub/src/hooks/usePaginator.ts index f217e0b0ef..243e504aae 100644 --- a/packages/hub/src/hooks/usePaginator.ts +++ b/packages/hub/src/hooks/usePaginator.ts @@ -1,14 +1,47 @@ -import { useState } from "react"; +import { useCallback, useState } from "react"; import { Paginated } from "@/server/types"; -export function usePaginator(initialPage: Paginated): { +type FullPaginated = { items: T[]; // this is intentionally named `loadNext` instead of `loadMore` to avoid confusion loadNext?: (limit: number) => void; -} { + // Helper functions - if the server action has affected some items, we need to update the state. + // This is similar to Relay's edge directives (https://relay.dev/docs/guided-tour/list-data/updating-connections/), + // but more manual. + append: (item: T) => void; + remove: (compare: (item: T) => boolean) => void; + update: (update: (item: T) => T) => void; +}; + +export function usePaginator(initialPage: Paginated): FullPaginated { const [{ items, loadMore }, setPage] = useState(initialPage); + const append = useCallback((newItem: T) => { + setPage(({ items }) => ({ + items: [...items, newItem], + loadMore, + })); + }, []); + + const remove = useCallback((compare: (item: T) => boolean) => { + setPage(({ items }) => ({ + items: items.filter((i) => !compare(i)), + loadMore, + })); + }, []); + + const update = useCallback((update: (item: T) => T) => { + setPage(({ items }) => { + const newItems = { + items: items.map(update), + loadMore, + }; + console.log(newItems); + return newItems; + }); + }, []); + return { items, loadNext: loadMore @@ -24,5 +57,8 @@ export function usePaginator(initialPage: Paginated): { }); } : undefined, + append, + remove, + update, }; } diff --git a/packages/hub/src/server/groups/actions/addUserToGroupAction.ts b/packages/hub/src/server/groups/actions/addUserToGroupAction.ts new file mode 100644 index 0000000000..658ab43f1b --- /dev/null +++ b/packages/hub/src/server/groups/actions/addUserToGroupAction.ts @@ -0,0 +1,106 @@ +"use server"; + +import { MembershipRole } from "@prisma/client"; +import { z } from "zod"; + +import { prisma } from "@/prisma"; +import { getSessionOrRedirect } from "@/server/users/auth"; +import { makeServerAction, zSlug } from "@/server/utils"; + +import { + GroupMemberDTO, + membershipSelect, + membershipToDTO, +} from "../data/members"; + +export const addUserToGroupAction = makeServerAction( + z.object({ + group: zSlug, + username: zSlug, + role: z.enum(Object.keys(MembershipRole) as [keyof typeof MembershipRole]), + }), + async (input): Promise => { + const session = await getSessionOrRedirect(); + + const membership = await prisma.$transaction(async (tx) => { + const groupOwner = await tx.owner.findUnique({ + where: { + slug: input.group, + }, + }); + if (!groupOwner) { + throw new Error(`Group ${input.group} not found`); + } + + const requestedUser = await tx.user.findFirst({ + where: { + asOwner: { + slug: input.username, + }, + }, + }); + + if (!requestedUser) { + throw new Error(`User ${input.username} not found`); + } + + // We perform all checks one by one because that allows more precise error reporting. + // (It would be possible to check everything in one big query with clever nested `connect` checks.) + const isAdmin = await tx.group.count({ + where: { + ownerId: groupOwner.id, + memberships: { + some: { + user: { email: session.user.email }, + role: "Admin", + }, + }, + }, + }); + if (!isAdmin) { + throw new Error(`You're not an admin of ${input.group} group`); + } + + const alreadyAMember = await tx.group.count({ + where: { + ownerId: groupOwner.id, + memberships: { + some: { userId: requestedUser.id }, + }, + }, + }); + if (alreadyAMember) { + throw new Error( + `${input.username} is already a member of ${input.group}` + ); + } + + // Cancel all pending invites + await tx.groupInvite.updateMany({ + where: { + userId: requestedUser.id, + groupId: groupOwner.id, + status: "Pending", + }, + data: { + status: "Canceled", + }, + }); + + return await tx.userGroupMembership.create({ + data: { + user: { + connect: { id: requestedUser.id }, + }, + group: { + connect: { ownerId: groupOwner.id }, + }, + role: input.role, + }, + select: membershipSelect, + }); + }); + + return membershipToDTO(membership); + } +); diff --git a/packages/hub/src/server/groups/actions/createReusableGroupInviteTokenAction.ts b/packages/hub/src/server/groups/actions/createReusableGroupInviteTokenAction.ts new file mode 100644 index 0000000000..a2b7d6ea72 --- /dev/null +++ b/packages/hub/src/server/groups/actions/createReusableGroupInviteTokenAction.ts @@ -0,0 +1,48 @@ +"use server"; +import crypto from "crypto"; +import { revalidatePath } from "next/cache"; +import { z } from "zod"; + +import { prisma } from "@/prisma"; +import { groupMembersRoute } from "@/routes"; +import { makeServerAction, zSlug } from "@/server/utils"; + +import { loadMyMembership } from "../data/members"; + +export const createReusableGroupInviteTokenAction = makeServerAction( + z.object({ + slug: zSlug, + }), + async (input): Promise => { + const myMembership = await loadMyMembership({ groupSlug: input.slug }); + if (!myMembership) { + throw new Error("Not a member of this group"); + } + if (myMembership.role !== "Admin") { + throw new Error("Only group admins can delete reusable invite tokens"); + } + + const group = await prisma.group.findFirstOrThrow({ + where: { + asOwner: { + slug: input.slug, + }, + }, + }); + + const token = crypto.randomBytes(30).toString("hex"); + + await prisma.group.update({ + where: { + id: group.id, + }, + data: { + // old token will be overwritten, that's fine + reusableInviteToken: token, + }, + select: { id: true }, + }); + + revalidatePath(groupMembersRoute({ slug: input.slug })); + } +); diff --git a/packages/hub/src/server/groups/actions/deleteMembershipAction.ts b/packages/hub/src/server/groups/actions/deleteMembershipAction.ts new file mode 100644 index 0000000000..80cbb327d6 --- /dev/null +++ b/packages/hub/src/server/groups/actions/deleteMembershipAction.ts @@ -0,0 +1,67 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { z } from "zod"; + +import { getMembership, getMyMembership } from "@/graphql/helpers/groupHelpers"; +import { prisma } from "@/prisma"; +import { groupMembersRoute } from "@/routes"; +import { getSessionOrRedirect } from "@/server/users/auth"; +import { makeServerAction, zSlug } from "@/server/utils"; + +import { groupHasAdminsBesidesUser } from "../data/helpers"; + +export const deleteMembershipAction = makeServerAction( + z.object({ + group: zSlug, + username: zSlug, + }), + async (input) => { + const session = await getSessionOrRedirect(); + + // somewhat repetitive compared to `updateMembershipRole`, but with slightly different error messages + const myMembership = await getMyMembership({ + groupSlug: input.group, + session, + }); + + if (!myMembership) { + throw new Error("You're not a member of this group"); + } + + if ( + input.username !== session.user.username && + myMembership.role !== "Admin" + ) { + throw new Error("Only admins can delete other members"); + } + + const membershipToDelete = await getMembership({ + groupSlug: input.group, + userSlug: input.username, + }); + + if (!membershipToDelete) { + throw new Error(`${input.username} is not a member of ${input.group}`); + } + + if ( + !(await groupHasAdminsBesidesUser({ + groupSlug: input.group, + userSlug: input.username, + })) + ) { + throw new Error( + `Can't delete, ${input.username} is the last admin of ${input.group}` + ); + } + + await prisma.userGroupMembership.delete({ + where: { + id: membershipToDelete.id, + }, + }); + + revalidatePath(groupMembersRoute({ slug: input.group })); + } +); diff --git a/packages/hub/src/server/groups/actions/deleteReusableGroupInviteTokenAction.ts b/packages/hub/src/server/groups/actions/deleteReusableGroupInviteTokenAction.ts new file mode 100644 index 0000000000..00bcc6208b --- /dev/null +++ b/packages/hub/src/server/groups/actions/deleteReusableGroupInviteTokenAction.ts @@ -0,0 +1,41 @@ +"use server"; +import { revalidatePath } from "next/cache"; +import { z } from "zod"; + +import { prisma } from "@/prisma"; +import { groupMembersRoute } from "@/routes"; +import { makeServerAction, zSlug } from "@/server/utils"; + +import { loadMyMembership } from "../data/members"; + +// Create or replace a reusable invite token for a group, available as \`reusableInviteToken\` field on group object. +// You must be an admin of the group to call this action. Previous invite token, if it existed, will stop working. +export const deleteReusableGroupInviteTokenAction = makeServerAction( + z.object({ + slug: zSlug, + }), + async (input): Promise => { + const myMembership = await loadMyMembership({ groupSlug: input.slug }); + if (!myMembership) { + throw new Error("Not a member of this group"); + } + if (myMembership.role !== "Admin") { + throw new Error("Only group admins can delete reusable invite tokens"); + } + + const group = await prisma.group.findFirstOrThrow({ + where: { + asOwner: { + slug: input.slug, + }, + }, + }); + + await prisma.group.update({ + where: { id: group.id }, + data: { reusableInviteToken: null }, + }); + + revalidatePath(groupMembersRoute({ slug: input.slug })); + } +); diff --git a/packages/hub/src/server/groups/actions/updateMembershipRoleAction.ts b/packages/hub/src/server/groups/actions/updateMembershipRoleAction.ts new file mode 100644 index 0000000000..3865ae79da --- /dev/null +++ b/packages/hub/src/server/groups/actions/updateMembershipRoleAction.ts @@ -0,0 +1,73 @@ +"use server"; + +import { MembershipRole } from "@prisma/client"; +import { z } from "zod"; + +import { prisma } from "@/prisma"; +import { getSessionOrRedirect } from "@/server/users/auth"; +import { makeServerAction, zSlug } from "@/server/utils"; + +import { groupHasAdminsBesidesUser } from "../data/helpers"; +import { + GroupMemberDTO, + loadMembership, + loadMyMembership, + membershipSelect, + membershipToDTO, +} from "../data/members"; + +export const updateMembershipRoleAction = makeServerAction( + z.object({ + group: zSlug, + user: zSlug, + role: z.enum(Object.keys(MembershipRole) as [keyof typeof MembershipRole]), + }), + async (input): Promise => { + const session = await getSessionOrRedirect(); + // somewhat repetitive compared to `deleteMembership`, but with slightly different error messages + + const myMembership = await loadMyMembership({ + groupSlug: input.group, + }); + + if (!myMembership) { + throw new Error("You're not a member of this group"); + } + + if (input.user !== session.user.username && myMembership.role !== "Admin") { + throw new Error("Only admins can update other members roles"); + } + + const membershipToUpdate = await loadMembership({ + groupSlug: input.group, + userSlug: input.user, + }); + + if (!membershipToUpdate) { + throw new Error(`${input.user} is not a member of ${input.group}`); + } + + if (membershipToUpdate.role === input.role) { + return membershipToUpdate; // nothing to do + } + + if ( + !(await groupHasAdminsBesidesUser({ + groupSlug: input.group, + userSlug: input.user, + })) + ) { + throw new Error( + `Can't change the role, ${input.user} is the last admin of ${input.group}` + ); + } + + const updatedMembership = await prisma.userGroupMembership.update({ + where: { id: membershipToUpdate.id }, + data: { role: input.role }, + select: membershipSelect, + }); + + return membershipToDTO(updatedMembership); + } +); diff --git a/packages/hub/src/server/groups/data/helpers.ts b/packages/hub/src/server/groups/data/helpers.ts index b205cc13fa..5af5aa04d6 100644 --- a/packages/hub/src/server/groups/data/helpers.ts +++ b/packages/hub/src/server/groups/data/helpers.ts @@ -66,3 +66,32 @@ export async function validateReusableGroupInviteToken(input: { return true; } + +// also returns true if user is not an admin +export async function groupHasAdminsBesidesUser({ + groupSlug, + userSlug, +}: { + groupSlug: string; + userSlug: string; +}) { + return Boolean( + await prisma.userGroupMembership.count({ + where: { + group: { + asOwner: { + slug: groupSlug, + }, + }, + NOT: { + user: { + asOwner: { + slug: userSlug, + }, + }, + }, + role: "Admin", + }, + }) + ); +} diff --git a/packages/hub/src/server/groups/data/members.ts b/packages/hub/src/server/groups/data/members.ts new file mode 100644 index 0000000000..488fd3b7cd --- /dev/null +++ b/packages/hub/src/server/groups/data/members.ts @@ -0,0 +1,133 @@ +import { MembershipRole, Prisma } from "@prisma/client"; + +import { auth } from "@/auth"; +import { prisma } from "@/prisma"; +import { Paginated } from "@/server/types"; + +export type GroupMemberDTO = { + id: string; + role: MembershipRole; + user: { + slug: string; + }; +}; + +const select = { + id: true, + role: true, + user: { + select: { + asOwner: { + select: { slug: true }, + }, + }, + }, +} satisfies Prisma.UserGroupMembershipSelect; + +export const membershipSelect = select; + +type Row = NonNullable< + Awaited< + ReturnType< + typeof prisma.userGroupMembership.findFirst<{ select: typeof select }> + > + > +>; + +export function membershipToDTO(row: Row): GroupMemberDTO { + return { + id: row.id, + role: row.role, + // guaranteed by the query + user: { slug: row.user.asOwner!.slug }, + }; +} + +export async function loadGroupMembers(params: { + groupSlug: string; + cursor?: string; + limit?: number; +}): Promise> { + const limit = params.limit ?? 20; + + const rows = await prisma.userGroupMembership.findMany({ + where: { + group: { + asOwner: { + slug: params.groupSlug, + }, + }, + user: { + asOwner: { + isNot: null, + }, + }, + }, + select, + take: limit + 1, + }); + + const members = rows.map(membershipToDTO); + + const nextCursor = members[members.length - 1]?.id; + + async function loadMore(limit: number) { + "use server"; + return loadGroupMembers({ ...params, cursor: nextCursor, limit }); + } + + return { + items: members.slice(0, limit), + loadMore: members.length > limit ? loadMore : undefined, + }; +} + +export async function loadMyMembership(params: { + groupSlug: string; +}): Promise { + const session = await auth(); + + const userId = session?.user.id; + if (!userId) { + return null; + } + + const membership = await prisma.userGroupMembership.findFirst({ + where: { + group: { asOwner: { slug: params.groupSlug } }, + user: { id: userId }, + }, + select, + }); + return membership ? membershipToDTO(membership) : null; +} + +export async function loadMembership(params: { + groupSlug: string; + userSlug: string; +}): Promise { + const membership = await prisma.userGroupMembership.findFirst({ + where: { + group: { asOwner: { slug: params.groupSlug } }, + user: { asOwner: { slug: params.userSlug } }, + }, + select, + }); + return membership ? membershipToDTO(membership) : null; +} + +export async function loadReusableInviteToken(params: { + groupSlug: string; +}): Promise { + const myMembership = await loadMyMembership({ groupSlug: params.groupSlug }); + if (myMembership?.role !== "Admin") { + throw new Error("Not an admin"); + } + + const group = await prisma.group.findFirst({ + where: { asOwner: { slug: params.groupSlug } }, + select: { reusableInviteToken: true }, + }); + + return group?.reusableInviteToken ?? null; +} From 4e011725cbc85e223867438c4f018c64ec8ee0ae Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 28 Nov 2024 23:53:18 -0300 Subject: [PATCH 37/68] remove runSquiggle graphql --- packages/hub/src/app/api/runSquiggle/route.ts | 2 +- packages/hub/src/graphql/queries/me.ts | 12 ---- packages/hub/src/graphql/schema.ts | 2 - .../buildRecentModelRevision/worker.ts | 2 +- .../queries => server}/runSquiggle.ts | 68 ++----------------- 5 files changed, 6 insertions(+), 80 deletions(-) delete mode 100644 packages/hub/src/graphql/queries/me.ts rename packages/hub/src/{graphql/queries => server}/runSquiggle.ts (69%) diff --git a/packages/hub/src/app/api/runSquiggle/route.ts b/packages/hub/src/app/api/runSquiggle/route.ts index 3cc422e217..051a51d403 100644 --- a/packages/hub/src/app/api/runSquiggle/route.ts +++ b/packages/hub/src/app/api/runSquiggle/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; -import { runSquiggleWithCache } from "@/graphql/queries/runSquiggle"; +import { runSquiggleWithCache } from "@/server/runSquiggle"; export async function POST(req: NextRequest) { // Assuming 'code' is sent in the request body and is a string diff --git a/packages/hub/src/graphql/queries/me.ts b/packages/hub/src/graphql/queries/me.ts deleted file mode 100644 index 9d906d9446..0000000000 --- a/packages/hub/src/graphql/queries/me.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { builder } from "@/graphql/builder"; - -import { Me } from "../types/Me"; - -builder.queryField("me", (t) => - t.withAuth({ signedIn: true }).field({ - type: Me, - async resolve(_, __, { session }) { - return session.user; - }, - }) -); diff --git a/packages/hub/src/graphql/schema.ts b/packages/hub/src/graphql/schema.ts index ba072d3024..6ba1f8f506 100644 --- a/packages/hub/src/graphql/schema.ts +++ b/packages/hub/src/graphql/schema.ts @@ -2,12 +2,10 @@ import "./errors/BaseError"; import "./errors/NotFoundError"; import "./errors/ValidationError"; import "./queries/group"; -import "./queries/me"; import "./queries/model"; import "./queries/variable"; import "./queries/relativeValuesDefinition"; import "./queries/relativeValuesDefinitions"; -import "./queries/runSquiggle"; import "./queries/userByUsername"; import "./mutations/adminUpdateModelVersion"; import "./mutations/adminRebuildSearchIndex"; diff --git a/packages/hub/src/scripts/buildRecentModelRevision/worker.ts b/packages/hub/src/scripts/buildRecentModelRevision/worker.ts index 5f521721a4..fdd14f54e2 100644 --- a/packages/hub/src/scripts/buildRecentModelRevision/worker.ts +++ b/packages/hub/src/scripts/buildRecentModelRevision/worker.ts @@ -1,6 +1,6 @@ -import { runSquiggle } from "@/graphql/queries/runSquiggle"; import { VariableRevisionInput } from "@/graphql/types/VariableRevision"; import { prisma } from "@/prisma"; +import { runSquiggle } from "@/server/runSquiggle"; export type WorkerRunMessage = { type: "run"; diff --git a/packages/hub/src/graphql/queries/runSquiggle.ts b/packages/hub/src/server/runSquiggle.ts similarity index 69% rename from packages/hub/src/graphql/queries/runSquiggle.ts rename to packages/hub/src/server/runSquiggle.ts index 7394343a8d..262cd8fc4e 100644 --- a/packages/hub/src/graphql/queries/runSquiggle.ts +++ b/packages/hub/src/server/runSquiggle.ts @@ -1,3 +1,5 @@ +import "server-only"; + import { Prisma } from "@prisma/client"; import crypto from "crypto"; @@ -10,12 +12,9 @@ import { } from "@quri/squiggle-lang"; import { SAMPLE_COUNT_DEFAULT, XY_POINT_LENGTH_DEFAULT } from "@/constants"; -import { builder } from "@/graphql/builder"; import { prisma } from "@/prisma"; import { parseSourceId } from "@/squiggle/components/linker"; -import { NotFoundError } from "../errors/NotFoundError"; - function getKey(code: string, seed: string): string { return crypto .createHash("sha256") @@ -44,51 +43,6 @@ type SquiggleOutput = { } ); -const SquiggleOutputObj = builder - .interfaceRef("SquiggleOutput") - .implement({ - fields: (t) => ({ - isCached: t.exposeBoolean("isCached"), - }), - }); - -builder.objectType( - builder.objectRef>( - "SquiggleOkOutput" - ), - { - name: "SquiggleOkOutput", - interfaces: [SquiggleOutputObj], - isTypeOf: (value) => (value as SquiggleOutput).isOk, - fields: (t) => ({ - resultJSON: t.string({ - resolve(obj) { - return JSON.stringify(obj.resultJSON); - }, - }), - bindingsJSON: t.string({ - resolve(obj) { - return JSON.stringify(obj.bindingsJSON); - }, - }), - }), - } -); - -builder.objectType( - builder.objectRef>( - "SquiggleErrorOutput" - ), - { - name: "SquiggleErrorOutput", - interfaces: [SquiggleOutputObj], - isTypeOf: (value) => !(value as SquiggleOutput).isOk, - fields: (t) => ({ - errorString: t.exposeString("errorString"), - }), - } -); - export const squiggleLinker: SqLinker = { resolve(name) { return name; @@ -110,14 +64,14 @@ export const squiggleLinker: SqLinker = { }); if (!model) { - throw new NotFoundError(); + throw new Error("Not found"); } const content = model?.currentRevision?.squiggleSnippet; if (content) { return new SqModule({ name: sourceId, code: content.code }); } else { - throw new NotFoundError(); + throw new Error("Not found"); } }, }; @@ -199,17 +153,3 @@ export async function runSquiggleWithCache( return result; } - -builder.queryField("runSquiggle", (t) => - t.field({ - type: SquiggleOutputObj, - args: { - code: t.arg.string({ required: true }), - seed: t.arg.string({ required: false }), - }, - async resolve(_, { code, seed }) { - const result = await runSquiggleWithCache(code, seed || "DEFAULT_SEED"); - return result; - }, - }) -); From 55ea8320bc42a28b81eebd95207f5d4d9e2aeeca Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 29 Nov 2024 01:11:42 -0300 Subject: [PATCH 38/68] relative values mutations --- .../src/app/new/definition/NewDefinition.tsx | 73 ++----- .../edit/EditRelativeValuesDefinition.tsx | 68 ++----- packages/hub/src/auth.ts | 2 +- packages/hub/src/graphql/errors/common.ts | 28 +-- .../mutations/adminRebuildSearchIndex.ts | 2 +- .../hub/src/graphql/mutations/createGroup.ts | 2 +- .../createRelativeValuesDefinition.ts | 183 ------------------ .../updateRelativeValuesDefinition.ts | 89 --------- packages/hub/src/graphql/schema.ts | 2 - .../RelativeValuesDefinitionForm/index.tsx | 2 - .../createSquiggleSnippetModelAction.ts | 2 +- .../server/relative-values/actions/common.ts | 64 ++++++ .../createRelativeValuesDefinitionAction.ts | 97 ++++++++++ .../updateRelativeValuesDefinitionAction.ts | 72 +++++++ .../search/helpers.ts} | 0 packages/hub/src/server/utils.ts | 45 ++++- 16 files changed, 323 insertions(+), 408 deletions(-) delete mode 100644 packages/hub/src/graphql/mutations/createRelativeValuesDefinition.ts delete mode 100644 packages/hub/src/graphql/mutations/updateRelativeValuesDefinition.ts create mode 100644 packages/hub/src/server/relative-values/actions/common.ts create mode 100644 packages/hub/src/server/relative-values/actions/createRelativeValuesDefinitionAction.ts create mode 100644 packages/hub/src/server/relative-values/actions/updateRelativeValuesDefinitionAction.ts rename packages/hub/src/{graphql/helpers/searchHelpers.ts => server/search/helpers.ts} (100%) diff --git a/packages/hub/src/app/new/definition/NewDefinition.tsx b/packages/hub/src/app/new/definition/NewDefinition.tsx index 51edff3342..647252f811 100644 --- a/packages/hub/src/app/new/definition/NewDefinition.tsx +++ b/packages/hub/src/app/new/definition/NewDefinition.tsx @@ -2,70 +2,37 @@ import { useRouter } from "next/navigation"; import { FC } from "react"; -import { graphql } from "relay-runtime"; import { H1 } from "@/components/ui/Headers"; -import { useAsyncMutation } from "@/hooks/useAsyncMutation"; import { RelativeValuesDefinitionForm } from "@/relative-values/components/RelativeValuesDefinitionForm"; import { FormShape } from "@/relative-values/components/RelativeValuesDefinitionForm/FormShape"; import { relativeValuesRoute } from "@/routes"; - -import { NewDefinitionMutation } from "@/__generated__/NewDefinitionMutation.graphql"; - -const Mutation = graphql` - mutation NewDefinitionMutation( - $input: MutationCreateRelativeValuesDefinitionInput! - ) { - result: createRelativeValuesDefinition(input: $input) { - __typename - ... on ValidationError { - message - } - ... on CreateRelativeValuesDefinitionResult { - definition { - id - slug - owner { - slug - } - } - } - } - } -`; +import { createRelativeValuesDefinitionAction } from "@/server/relative-values/actions/createRelativeValuesDefinitionAction"; export const NewDefinition: FC = () => { const router = useRouter(); - const [runMutation] = useAsyncMutation({ - mutation: Mutation, - expectedTypename: "CreateRelativeValuesDefinitionResult", - confirmation: "Definition created", - blockOnSuccess: true, - }); - const save = async (data: FormShape) => { - await runMutation({ - variables: { - input: { - slug: data.slug, - title: data.title, - items: data.items, - clusters: data.clusters, - recommendedUnit: data.recommendedUnit, - }, - }, - onCompleted: (result) => { - if (result.__typename === "CreateRelativeValuesDefinitionResult") { - router.push( - relativeValuesRoute({ - owner: result.definition.owner.slug, - slug: result.definition.slug, - }) - ); - } - }, + const result = await createRelativeValuesDefinitionAction({ + slug: data.slug, + title: data.title, + items: data.items.map((item) => ({ + ...item, + clusterId: item.clusterId ?? undefined, + })), + clusters: data.clusters.map((cluster) => ({ + ...cluster, + recommendedUnit: cluster.recommendedUnit ?? undefined, + })), + recommendedUnit: data.recommendedUnit ?? undefined, }); + router.push( + relativeValuesRoute({ + owner: result.owner, + slug: result.slug, + }) + ); + // confirmation: "Definition created", }; return ( diff --git a/packages/hub/src/app/relative-values/[owner]/[slug]/edit/EditRelativeValuesDefinition.tsx b/packages/hub/src/app/relative-values/[owner]/[slug]/edit/EditRelativeValuesDefinition.tsx index cdd18b01e3..83b2f16bb3 100644 --- a/packages/hub/src/app/relative-values/[owner]/[slug]/edit/EditRelativeValuesDefinition.tsx +++ b/packages/hub/src/app/relative-values/[owner]/[slug]/edit/EditRelativeValuesDefinition.tsx @@ -1,9 +1,8 @@ "use client"; import { useRouter } from "next/navigation"; import { FC } from "react"; -import { graphql, useFragment } from "react-relay"; +import { useFragment } from "react-relay"; -import { useAsyncMutation } from "@/hooks/useAsyncMutation"; import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; import { RelativeValuesDefinitionForm } from "@/relative-values/components/RelativeValuesDefinitionForm"; import { FormShape } from "@/relative-values/components/RelativeValuesDefinitionForm/FormShape"; @@ -11,35 +10,17 @@ import { RelativeValuesDefinitionRevisionFragment } from "@/relative-values/comp import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; import { usePageQuery } from "@/relay/usePageQuery"; import { relativeValuesRoute } from "@/routes"; +import { updateRelativeValuesDefinitionAction } from "@/server/relative-values/actions/updateRelativeValuesDefinitionAction"; import { RelativeValuesDefinitionPageFragment, RelativeValuesDefinitionPageQuery, } from "../RelativeValuesDefinitionPage"; -import { EditRelativeValuesDefinitionMutation } from "@/__generated__/EditRelativeValuesDefinitionMutation.graphql"; import { RelativeValuesDefinitionPage$key } from "@/__generated__/RelativeValuesDefinitionPage.graphql"; import { RelativeValuesDefinitionPageQuery as QueryType } from "@/__generated__/RelativeValuesDefinitionPageQuery.graphql"; import { RelativeValuesDefinitionRevision$key } from "@/__generated__/RelativeValuesDefinitionRevision.graphql"; -const Mutation = graphql` - mutation EditRelativeValuesDefinitionMutation( - $input: MutationUpdateRelativeValuesDefinitionInput! - ) { - result: updateRelativeValuesDefinition(input: $input) { - __typename - ... on BaseError { - message - } - ... on UpdateRelativeValuesDefinitionResult { - definition { - id - } - } - } - } -`; - export const EditRelativeValuesDefinition: FC<{ query: SerializablePreloadedQuery; }> = ({ query }) => { @@ -64,34 +45,27 @@ export const EditRelativeValuesDefinition: FC<{ definition.currentRevision ); - const [saveMutation] = useAsyncMutation( - { - mutation: Mutation, - expectedTypename: "UpdateRelativeValuesDefinitionResult", - } - ); - const save = async (data: FormShape) => { - await saveMutation({ - variables: { - input: { - slug: definition.slug, - owner: definition.owner.slug, - title: data.title, - items: data.items, - clusters: data.clusters, - recommendedUnit: data.recommendedUnit, - }, - }, - onCompleted() { - router.push( - relativeValuesRoute({ - owner: definition.owner.slug, - slug: definition.slug, - }) - ); - }, + await updateRelativeValuesDefinitionAction({ + slug: definition.slug, + owner: definition.owner.slug, + title: data.title, + items: data.items.map((item) => ({ + ...item, + clusterId: item.clusterId ?? undefined, + })), + clusters: data.clusters.map((cluster) => ({ + ...cluster, + recommendedUnit: cluster.recommendedUnit || undefined, + })), + recommendedUnit: data.recommendedUnit || undefined, }); + router.push( + relativeValuesRoute({ + owner: definition.owner.slug, + slug: definition.slug, + }) + ); }; return ( diff --git a/packages/hub/src/auth.ts b/packages/hub/src/auth.ts index 31d04ee46d..34bc300d39 100644 --- a/packages/hub/src/auth.ts +++ b/packages/hub/src/auth.ts @@ -7,8 +7,8 @@ import GithubProvider from "next-auth/providers/github"; import { Provider } from "next-auth/providers/index"; import { cache } from "react"; -import { indexUserId } from "@/graphql/helpers/searchHelpers"; import { prisma } from "@/prisma"; +import { indexUserId } from "@/server/search/helpers"; function buildAuthConfig(): NextAuthConfig { const providers: Provider[] = []; diff --git a/packages/hub/src/graphql/errors/common.ts b/packages/hub/src/graphql/errors/common.ts index 0c11f5d06a..285d8b6d3a 100644 --- a/packages/hub/src/graphql/errors/common.ts +++ b/packages/hub/src/graphql/errors/common.ts @@ -1,5 +1,3 @@ -import { Prisma } from "@prisma/client"; - import { builder } from "@/graphql/builder"; export const ErrorInterface = builder.interfaceRef("Error").implement({ @@ -8,28 +6,4 @@ export const ErrorInterface = builder.interfaceRef("Error").implement({ }), }); -// Rethrows Prisma constraint error (usually happens on create operations) with a nicer error message. -export async function rethrowOnConstraint( - cb: () => Promise, - ...handlers: { - target: string[]; - error: string; - }[] -): Promise { - try { - return await cb(); - } catch (e) { - for (const handler of handlers) { - if ( - e instanceof Prisma.PrismaClientKnownRequestError && - e.code === "P2002" && - Array.isArray(e.meta?.["target"]) && - e.meta?.["target"].join(",") === handler.target.join(",") - ) { - // TODO - throw more specific error - throw new Error(handler.error); - } - } - throw e; - } -} +export { rethrowOnConstraint } from "@/server/utils"; diff --git a/packages/hub/src/graphql/mutations/adminRebuildSearchIndex.ts b/packages/hub/src/graphql/mutations/adminRebuildSearchIndex.ts index 39205407ba..1ae5c8582f 100644 --- a/packages/hub/src/graphql/mutations/adminRebuildSearchIndex.ts +++ b/packages/hub/src/graphql/mutations/adminRebuildSearchIndex.ts @@ -1,6 +1,6 @@ import { builder } from "@/graphql/builder"; -import { rebuildSearchableTable } from "../helpers/searchHelpers"; +import { rebuildSearchableTable } from "../../server/search/helpers"; builder.mutationField("adminRebuildSearchIndex", (t) => t.withAuth({ signedIn: true }).field({ diff --git a/packages/hub/src/graphql/mutations/createGroup.ts b/packages/hub/src/graphql/mutations/createGroup.ts index 93c8b00d47..94de887636 100644 --- a/packages/hub/src/graphql/mutations/createGroup.ts +++ b/packages/hub/src/graphql/mutations/createGroup.ts @@ -1,9 +1,9 @@ import { prisma } from "@/prisma"; +import { indexGroupId } from "../../server/search/helpers"; import { builder } from "../builder"; import { rethrowOnConstraint } from "../errors/common"; import { Group } from "../types/Group"; -import { indexGroupId } from "../helpers/searchHelpers"; builder.mutationField("createGroup", (t) => t.withAuth({ signedIn: true }).fieldWithInput({ diff --git a/packages/hub/src/graphql/mutations/createRelativeValuesDefinition.ts b/packages/hub/src/graphql/mutations/createRelativeValuesDefinition.ts deleted file mode 100644 index 94ff2cce6c..0000000000 --- a/packages/hub/src/graphql/mutations/createRelativeValuesDefinition.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { InputObjectRef } from "@pothos/core"; -import { ZodError } from "zod"; - -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { rethrowOnConstraint } from "../errors/common"; -import { getWriteableOwner } from "../helpers/ownerHelpers"; -import { indexDefinitionId } from "../helpers/searchHelpers"; -import { RelativeValuesDefinition } from "../types/RelativeValuesDefinition"; -import { validateSlug } from "../utils"; - -const validateColor = { regex: /^#[0-9a-fA-F]{6}$/ }; - -export const RelativeValuesClusterInput = builder.inputType( - "RelativeValuesClusterInput", - { - fields: (t) => ({ - id: t.string({ - required: true, - validate: validateSlug, - }), - color: t.string({ - required: true, - validate: validateColor, - }), - recommendedUnit: t.string({ - validate: validateSlug, - }), - }), - } -); - -export const RelativeValuesItemInput = builder.inputType( - "RelativeValuesItemInput", - { - fields: (t) => ({ - id: t.string({ - required: true, - validate: validateSlug, - }), - name: t.string({ - required: true, - }), - description: t.string(), - clusterId: t.string(), - }), - } -); - -type ExtractInputShape = Type extends InputObjectRef ? T : never; - -export function validateRelativeValuesDefinition({ - items, - clusters, - recommendedUnit, -}: { - items: ExtractInputShape[]; - clusters: ExtractInputShape[]; - recommendedUnit: string | null | undefined; -}) { - if (!items.length) { - throw new Error("RelativeValuesDefinition must include at least one item"); - } - - const itemIds = new Set(); - for (const item of items) { - if (itemIds.has(item.id)) { - throw new Error(`Duplicate item id ${item.id}`); - } - if (!item.id.match(/^\w[\w\-]*$/)) { - throw new Error(`Invalid item id ${item.id}`); - } - itemIds.add(item.id); - } - - const checkId = (id: string | null | undefined) => { - if (id !== null && id !== undefined && !itemIds.has(id)) { - throw new Error(`id ${id} not found in items`); - } - }; - - for (const cluster of clusters) { - checkId(cluster.recommendedUnit); - } - checkId(recommendedUnit); -} - -builder.mutationField("createRelativeValuesDefinition", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("CreateRelativeValuesDefinitionResult", { - fields: (t) => ({ - definition: t.field({ type: RelativeValuesDefinition }), - }), - }), - errors: { types: [ZodError] }, - input: { - groupSlug: t.input.string({ - validate: validateSlug, - description: - "Optional, if not set, definition will be created on current user's account", - }), - slug: t.input.string({ - required: true, - validate: validateSlug, - }), - title: t.input.string({ required: true }), - items: t.input.field({ - type: [RelativeValuesItemInput], - required: true, - }), - clusters: t.input.field({ - type: [RelativeValuesClusterInput], - required: true, - }), - recommendedUnit: t.input.string({ - validate: validateSlug, - }), - }, - resolve: async (_, { input }, { session }) => { - const owner = await getWriteableOwner(session, input.groupSlug); - - validateRelativeValuesDefinition({ - items: input.items, - clusters: input.clusters, - recommendedUnit: input.recommendedUnit, - }); - - const definition = await prisma.$transaction(async (tx) => { - const definition = await rethrowOnConstraint( - () => - tx.relativeValuesDefinition.create({ - data: { - ownerId: owner.id, - slug: input.slug, - revisions: { - create: { - title: input.title, - items: input.items, - clusters: input.clusters, - recommendedUnit: input.recommendedUnit, - }, - }, - }, - }), - { - target: ["slug", "ownerId"], - error: `The definition ${input.slug} already exists on this account`, - } - ); - - const revision = await tx.relativeValuesDefinitionRevision.create({ - data: { - title: input.title, - items: input.items, - clusters: input.clusters, - recommendedUnit: input.recommendedUnit, - definition: { - connect: { - id: definition.id, - }, - }, - }, - }); - - await tx.relativeValuesDefinition.update({ - where: { - id: definition.id, - }, - data: { - currentRevisionId: revision.id, - }, - }); - - return definition; - }); - - await indexDefinitionId(definition.id); - - return { definition }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/updateRelativeValuesDefinition.ts b/packages/hub/src/graphql/mutations/updateRelativeValuesDefinition.ts deleted file mode 100644 index 590db37f92..0000000000 --- a/packages/hub/src/graphql/mutations/updateRelativeValuesDefinition.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { getWriteableOwnerBySlug } from "../helpers/ownerHelpers"; -import { RelativeValuesDefinition } from "../types/RelativeValuesDefinition"; -import { validateSlug } from "../utils"; -import { - RelativeValuesClusterInput, - RelativeValuesItemInput, - validateRelativeValuesDefinition, -} from "./createRelativeValuesDefinition"; - -builder.mutationField("updateRelativeValuesDefinition", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("UpdateRelativeValuesDefinitionResult", { - fields: (t) => ({ - definition: t.field({ - type: RelativeValuesDefinition, - nullable: false, - }), - }), - }), - errors: {}, - input: { - owner: t.input.string({ required: true }), - slug: t.input.string({ required: true }), - title: t.input.string({ required: true }), - items: t.input.field({ - type: [RelativeValuesItemInput], - required: true, - }), - clusters: t.input.field({ - type: [RelativeValuesClusterInput], - required: true, - }), - recommendedUnit: t.input.string({ - validate: validateSlug, - }), - }, - resolve: async (_, { input }, { session }) => { - const owner = await getWriteableOwnerBySlug(session, input.owner); - - validateRelativeValuesDefinition({ - items: input.items, - clusters: input.clusters, - recommendedUnit: input.recommendedUnit, - }); - - const definition = await prisma.$transaction(async (tx) => { - const revision = await tx.relativeValuesDefinitionRevision.create({ - data: { - title: input.title, - items: input.items, - clusters: input.clusters, - recommendedUnit: input.recommendedUnit, - definition: { - connect: { - slug_ownerId: { - slug: input.slug, - ownerId: owner.id, - }, - }, - }, - }, - include: { - definition: { - select: { - id: true, - }, - }, - }, - }); - - const definition = await tx.relativeValuesDefinition.update({ - where: { - id: revision.definition.id, - }, - data: { - currentRevisionId: revision.id, - }, - }); - - return definition; - }); - - return { definition }; - }, - }) -); diff --git a/packages/hub/src/graphql/schema.ts b/packages/hub/src/graphql/schema.ts index 6ba1f8f506..f77827261d 100644 --- a/packages/hub/src/graphql/schema.ts +++ b/packages/hub/src/graphql/schema.ts @@ -13,9 +13,7 @@ import "./mutations/buildRelativeValuesCache"; import "./mutations/cancelGroupInvite"; import "./mutations/clearRelativeValuesCache"; import "./mutations/createGroup"; -import "./mutations/createRelativeValuesDefinition"; import "./mutations/deleteRelativeValuesDefinition"; -import "./mutations/updateRelativeValuesDefinition"; import "./mutations/updateSquiggleSnippetModel"; import { builder } from "./builder"; diff --git a/packages/hub/src/relative-values/components/RelativeValuesDefinitionForm/index.tsx b/packages/hub/src/relative-values/components/RelativeValuesDefinitionForm/index.tsx index eb3b38aa1f..2974694018 100644 --- a/packages/hub/src/relative-values/components/RelativeValuesDefinitionForm/index.tsx +++ b/packages/hub/src/relative-values/components/RelativeValuesDefinitionForm/index.tsx @@ -1,5 +1,3 @@ -"use client"; - import { FC } from "react"; import { FormProvider, useForm } from "react-hook-form"; diff --git a/packages/hub/src/server/models/actions/createSquiggleSnippetModelAction.ts b/packages/hub/src/server/models/actions/createSquiggleSnippetModelAction.ts index d9aad210e6..f329256a61 100644 --- a/packages/hub/src/server/models/actions/createSquiggleSnippetModelAction.ts +++ b/packages/hub/src/server/models/actions/createSquiggleSnippetModelAction.ts @@ -4,9 +4,9 @@ import { z } from "zod"; import { rethrowOnConstraint } from "@/graphql/errors/common"; import { getWriteableOwner } from "@/graphql/helpers/ownerHelpers"; -import { indexModelId } from "@/graphql/helpers/searchHelpers"; import { getSelf } from "@/graphql/helpers/userHelpers"; import { prisma } from "@/prisma"; +import { indexModelId } from "@/server/search/helpers"; import { getSessionOrRedirect } from "@/server/users/auth"; import { makeServerAction, zSlug } from "@/server/utils"; diff --git a/packages/hub/src/server/relative-values/actions/common.ts b/packages/hub/src/server/relative-values/actions/common.ts new file mode 100644 index 0000000000..4f776c6bee --- /dev/null +++ b/packages/hub/src/server/relative-values/actions/common.ts @@ -0,0 +1,64 @@ +import { z } from "zod"; + +import { zColor, zSlug } from "@/server/utils"; + +// Appropriate both for create and update actions. +export const inputSchema = z.object({ + owner: zSlug.optional(), + slug: zSlug, + title: z.string(), + items: z.array( + z.object({ + id: zSlug, + name: z.string(), + description: z.string().optional(), + clusterId: z.string().optional(), + }) + ), + clusters: z.array( + z.object({ + id: zSlug, + color: zColor, + recommendedUnit: zSlug.optional(), + }) + ), + recommendedUnit: zSlug.optional(), +}); + +export type Input = z.infer; + +export async function validateRelativeValuesDefinition({ + items, + clusters, + recommendedUnit, +}: { + items: Input["items"]; + clusters: Input["clusters"]; + recommendedUnit: Input["recommendedUnit"]; +}) { + if (!items.length) { + throw new Error("RelativeValuesDefinition must include at least one item"); + } + + const itemIds = new Set(); + for (const item of items) { + if (itemIds.has(item.id)) { + throw new Error(`Duplicate item id ${item.id}`); + } + if (!item.id.match(/^\w[\w\-]*$/)) { + throw new Error(`Invalid item id ${item.id}`); + } + itemIds.add(item.id); + } + + const checkId = (id: string | null | undefined) => { + if (id !== null && id !== undefined && !itemIds.has(id)) { + throw new Error(`id ${id} not found in items`); + } + }; + + for (const cluster of clusters) { + checkId(cluster.recommendedUnit); + } + checkId(recommendedUnit); +} diff --git a/packages/hub/src/server/relative-values/actions/createRelativeValuesDefinitionAction.ts b/packages/hub/src/server/relative-values/actions/createRelativeValuesDefinitionAction.ts new file mode 100644 index 0000000000..4a9b761ecc --- /dev/null +++ b/packages/hub/src/server/relative-values/actions/createRelativeValuesDefinitionAction.ts @@ -0,0 +1,97 @@ +"use server"; + +import { getWriteableOwnerBySlug } from "@/graphql/helpers/ownerHelpers"; +import { prisma } from "@/prisma"; +import { indexDefinitionId } from "@/server/search/helpers"; +import { getSessionOrRedirect } from "@/server/users/auth"; +import { makeServerAction, rethrowOnConstraint } from "@/server/utils"; + +import { inputSchema, validateRelativeValuesDefinition } from "./common"; + +export const createRelativeValuesDefinitionAction = makeServerAction( + inputSchema, + async ( + input + ): Promise<{ + owner: string; + slug: string; + }> => { + const session = await getSessionOrRedirect(); + const ownerSlug = input.owner ?? session.user.username; + if (!ownerSlug) { + throw new Error("Owner slug or username is required"); + } + const owner = await getWriteableOwnerBySlug(session, ownerSlug); + + validateRelativeValuesDefinition({ + items: input.items, + clusters: input.clusters, + recommendedUnit: input.recommendedUnit, + }); + + const definition = await prisma.$transaction(async (tx) => { + const definition = await rethrowOnConstraint( + () => + tx.relativeValuesDefinition.create({ + data: { + ownerId: owner.id, + slug: input.slug, + revisions: { + create: { + title: input.title, + items: input.items, + clusters: input.clusters, + recommendedUnit: input.recommendedUnit, + }, + }, + }, + select: { + id: true, + slug: true, + owner: { + select: { + slug: true, + }, + }, + }, + }), + { + target: ["slug", "ownerId"], + error: `The definition ${input.slug} already exists on this account`, + } + ); + + const revision = await tx.relativeValuesDefinitionRevision.create({ + data: { + title: input.title, + items: input.items, + clusters: input.clusters, + recommendedUnit: input.recommendedUnit, + definition: { + connect: { + id: definition.id, + }, + }, + }, + }); + + await tx.relativeValuesDefinition.update({ + where: { + id: definition.id, + }, + data: { + currentRevisionId: revision.id, + }, + }); + + return definition; + }); + + await indexDefinitionId(definition.id); + + return { + owner: definition.owner.slug, + slug: definition.slug, + }; + } +); diff --git a/packages/hub/src/server/relative-values/actions/updateRelativeValuesDefinitionAction.ts b/packages/hub/src/server/relative-values/actions/updateRelativeValuesDefinitionAction.ts new file mode 100644 index 0000000000..8df413fb26 --- /dev/null +++ b/packages/hub/src/server/relative-values/actions/updateRelativeValuesDefinitionAction.ts @@ -0,0 +1,72 @@ +"use server"; +import { prisma } from "@/prisma"; +import { getSessionOrRedirect } from "@/server/users/auth"; +import { makeServerAction } from "@/server/utils"; + +import { getWriteableOwnerBySlug } from "../../../graphql/helpers/ownerHelpers"; +import { inputSchema, validateRelativeValuesDefinition } from "./common"; + +export const updateRelativeValuesDefinitionAction = makeServerAction( + inputSchema, + async (input): Promise<{ owner: string; slug: string }> => { + const session = await getSessionOrRedirect(); + const ownerSlug = input.owner ?? session.user.username; + if (!ownerSlug) { + throw new Error("Owner slug or username is required"); + } + const owner = await getWriteableOwnerBySlug(session, ownerSlug); + + validateRelativeValuesDefinition({ + items: input.items, + clusters: input.clusters, + recommendedUnit: input.recommendedUnit, + }); + + const definition = await prisma.$transaction(async (tx) => { + const revision = await tx.relativeValuesDefinitionRevision.create({ + data: { + title: input.title, + items: input.items, + clusters: input.clusters, + recommendedUnit: input.recommendedUnit, + definition: { + connect: { + slug_ownerId: { + slug: input.slug, + ownerId: owner.id, + }, + }, + }, + }, + include: { + definition: { + select: { + id: true, + }, + }, + }, + }); + + const definition = await tx.relativeValuesDefinition.update({ + where: { + id: revision.definition.id, + }, + data: { + currentRevisionId: revision.id, + }, + select: { + owner: { + select: { + slug: true, + }, + }, + slug: true, + }, + }); + + return definition; + }); + + return { owner: definition.owner.slug, slug: definition.slug }; + } +); diff --git a/packages/hub/src/graphql/helpers/searchHelpers.ts b/packages/hub/src/server/search/helpers.ts similarity index 100% rename from packages/hub/src/graphql/helpers/searchHelpers.ts rename to packages/hub/src/server/search/helpers.ts diff --git a/packages/hub/src/server/utils.ts b/packages/hub/src/server/utils.ts index f7077260f3..a4cca688e9 100644 --- a/packages/hub/src/server/utils.ts +++ b/packages/hub/src/server/utils.ts @@ -1,17 +1,60 @@ +import { Prisma } from "@prisma/client"; import { z } from "zod"; export const zSlug = z.string().regex(/^\w[\w\-]*$/, { message: "Must be alphanumerical", }); +export const zColor = z.string().regex(/^#[0-9a-fA-F]{6}$/, { + message: "Must be a valid hex color", +}); + +export type DeepReadonly = T extends (infer R)[] + ? DeepReadonlyArray + : T extends object + ? DeepReadonlyObject + : T; + +interface DeepReadonlyArray extends ReadonlyArray> {} + +type DeepReadonlyObject = { + readonly [P in keyof T]: DeepReadonly; +}; + export function makeServerAction( schema: z.ZodType, handler: (input: T) => Promise ) { return async ( - data: T // data type is unknown but we will validate it immediately + data: DeepReadonly // data type is unknown/unsafe, but we will validate it immediately ) => { const input = schema.parse(data); return handler(input); }; } + +// Rethrows Prisma constraint error (usually happens on create operations) with a nicer error message. +export async function rethrowOnConstraint( + cb: () => Promise, + ...handlers: { + target: string[]; + error: string; + }[] +): Promise { + try { + return await cb(); + } catch (e) { + for (const handler of handlers) { + if ( + e instanceof Prisma.PrismaClientKnownRequestError && + e.code === "P2002" && + Array.isArray(e.meta?.["target"]) && + e.meta?.["target"].join(",") === handler.target.join(",") + ) { + // TODO - throw more specific error + throw new Error(handler.error); + } + } + throw e; + } +} From de6406c1b8ada1df92986f1ff23594b2193abde4 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 29 Nov 2024 01:17:42 -0300 Subject: [PATCH 39/68] migrate adminRebuildSearchIndex --- .../hub/src/app/admin/RebuildSearchIndex.tsx | 36 ------------------- packages/hub/src/app/admin/search/page.tsx | 23 ++++++++++++ .../src/components/ui/ServerActionButton.tsx | 9 ++++- .../mutations/adminRebuildSearchIndex.ts | 22 ------------ packages/hub/src/graphql/schema.ts | 2 +- .../actions/adminRebuildSearchIndexAction.ts | 17 +++++++++ 6 files changed, 49 insertions(+), 60 deletions(-) delete mode 100644 packages/hub/src/app/admin/RebuildSearchIndex.tsx create mode 100644 packages/hub/src/app/admin/search/page.tsx delete mode 100644 packages/hub/src/graphql/mutations/adminRebuildSearchIndex.ts create mode 100644 packages/hub/src/server/search/actions/adminRebuildSearchIndexAction.ts diff --git a/packages/hub/src/app/admin/RebuildSearchIndex.tsx b/packages/hub/src/app/admin/RebuildSearchIndex.tsx deleted file mode 100644 index d39e89aaa2..0000000000 --- a/packages/hub/src/app/admin/RebuildSearchIndex.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { RebuildSearchIndexMutation } from "@/__generated__/RebuildSearchIndexMutation.graphql"; -import { H2 } from "@/components/ui/Headers"; -import { MutationButton } from "@/components/ui/MutationButton"; -import { FC } from "react"; -import { graphql } from "relay-runtime"; - -export const RebuildSearchIndex: FC = () => { - return ( -
-

Rebuild search index

- - mutation={graphql` - mutation RebuildSearchIndexMutation { - result: adminRebuildSearchIndex { - __typename - ... on BaseError { - message - } - ... on AdminRebuildSearchIndexResult { - ok - } - } - } - `} - expectedTypename="AdminRebuildSearchIndexResult" - title="Rebuild" - confirmation="Index updated" - theme="primary" - variables={{}} - /> -
- ); -}; diff --git a/packages/hub/src/app/admin/search/page.tsx b/packages/hub/src/app/admin/search/page.tsx new file mode 100644 index 0000000000..dba268bf74 --- /dev/null +++ b/packages/hub/src/app/admin/search/page.tsx @@ -0,0 +1,23 @@ +import { H2 } from "@/components/ui/Headers"; +import { ServerActionButton } from "@/components/ui/ServerActionButton"; +import { adminRebuildSearchIndexAction } from "@/server/search/actions/adminRebuildSearchIndexAction"; +import { checkRootUser } from "@/server/users/auth"; + +export default async function AdminSearchPage() { + await checkRootUser(); + + return ( +
+

Rebuild search index

+ { + "use server"; + await adminRebuildSearchIndexAction({}); + }} + title="Rebuild" + confirmation="Index updated" + theme="primary" + /> +
+ ); +} diff --git a/packages/hub/src/components/ui/ServerActionButton.tsx b/packages/hub/src/components/ui/ServerActionButton.tsx index 16dfaf7794..c22d068f53 100644 --- a/packages/hub/src/components/ui/ServerActionButton.tsx +++ b/packages/hub/src/components/ui/ServerActionButton.tsx @@ -1,6 +1,7 @@ +"use client"; import { ReactNode, useActionState } from "react"; -import { Button } from "@quri/ui"; +import { Button, useToast } from "@quri/ui"; /* * Props for this component include: @@ -13,13 +14,19 @@ export function ServerActionButton({ // button props theme, size, + confirmation, }: { action: () => Promise; title: string; + confirmation?: string; } & Pick[0], "theme" | "size">): ReactNode { + const toast = useToast(); // TODO - pending based on invariant, similar to ServerActionDropdownAction const [, formAction, isPending] = useActionState(async () => { await action(); + if (confirmation) { + toast(confirmation, "confirmation"); + } }, undefined); return ( diff --git a/packages/hub/src/graphql/mutations/adminRebuildSearchIndex.ts b/packages/hub/src/graphql/mutations/adminRebuildSearchIndex.ts deleted file mode 100644 index 1ae5c8582f..0000000000 --- a/packages/hub/src/graphql/mutations/adminRebuildSearchIndex.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { builder } from "@/graphql/builder"; - -import { rebuildSearchableTable } from "../../server/search/helpers"; - -builder.mutationField("adminRebuildSearchIndex", (t) => - t.withAuth({ signedIn: true }).field({ - description: "Admin-only query for rebuilding the search index", - type: builder.simpleObject("AdminRebuildSearchIndexResult", { - fields: (t) => ({ - ok: t.boolean(), - }), - }), - errors: {}, - authScopes: { - isRootUser: true, - }, - resolve: async () => { - await rebuildSearchableTable(); - return { ok: true }; - }, - }) -); diff --git a/packages/hub/src/graphql/schema.ts b/packages/hub/src/graphql/schema.ts index f77827261d..91f72d5c77 100644 --- a/packages/hub/src/graphql/schema.ts +++ b/packages/hub/src/graphql/schema.ts @@ -8,7 +8,7 @@ import "./queries/relativeValuesDefinition"; import "./queries/relativeValuesDefinitions"; import "./queries/userByUsername"; import "./mutations/adminUpdateModelVersion"; -import "./mutations/adminRebuildSearchIndex"; +import "../server/search/actions/adminRebuildSearchIndexAction"; import "./mutations/buildRelativeValuesCache"; import "./mutations/cancelGroupInvite"; import "./mutations/clearRelativeValuesCache"; diff --git a/packages/hub/src/server/search/actions/adminRebuildSearchIndexAction.ts b/packages/hub/src/server/search/actions/adminRebuildSearchIndexAction.ts new file mode 100644 index 0000000000..838349e868 --- /dev/null +++ b/packages/hub/src/server/search/actions/adminRebuildSearchIndexAction.ts @@ -0,0 +1,17 @@ +"use server"; + +import { z } from "zod"; + +import { checkRootUser } from "@/server/users/auth"; +import { makeServerAction } from "@/server/utils"; + +import { rebuildSearchableTable } from "../helpers"; + +// Admin-only query for rebuilding the search index +export const adminRebuildSearchIndexAction = makeServerAction( + z.object({}), + async () => { + await checkRootUser(); + await rebuildSearchableTable(); + } +); From 293c4fd3c2a6a12a9bb40656d5583adc33e9375a Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 29 Nov 2024 01:26:06 -0300 Subject: [PATCH 40/68] migrate deleteRelativeValuesDefinition --- .../members/SetMembershipRoleAction.tsx | 20 ------- .../DeleteRelativeValuesDefinitionAction.tsx | 52 +++---------------- .../deleteRelativeValuesDefinition.tsx | 33 ------------ packages/hub/src/graphql/schema.ts | 1 - .../deleteRelativeValuesDefinitionAction.tsx | 29 +++++++++++ 5 files changed, 37 insertions(+), 98 deletions(-) delete mode 100644 packages/hub/src/graphql/mutations/deleteRelativeValuesDefinition.tsx create mode 100644 packages/hub/src/server/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx diff --git a/packages/hub/src/app/groups/[slug]/members/SetMembershipRoleAction.tsx b/packages/hub/src/app/groups/[slug]/members/SetMembershipRoleAction.tsx index 89693d24e6..4b21c20ea6 100644 --- a/packages/hub/src/app/groups/[slug]/members/SetMembershipRoleAction.tsx +++ b/packages/hub/src/app/groups/[slug]/members/SetMembershipRoleAction.tsx @@ -1,30 +1,10 @@ import { MembershipRole } from "@prisma/client"; import { FC } from "react"; -import { graphql } from "relay-runtime"; import { ServerActionDropdownAction } from "@/components/ui/ServerActionDropdownAction"; import { updateMembershipRoleAction } from "@/server/groups/actions/updateMembershipRoleAction"; import { GroupMemberDTO } from "@/server/groups/data/members"; -const Mutation = graphql` - mutation SetMembershipRoleActionMutation( - $input: MutationUpdateMembershipRoleInput! - ) { - result: updateMembershipRole(input: $input) { - __typename - ... on BaseError { - message - } - ... on UpdateMembershipRoleResult { - membership { - id - role - } - } - } - } -`; - type Props = { membership: GroupMemberDTO; groupSlug: string; diff --git a/packages/hub/src/app/relative-values/[owner]/[slug]/DeleteRelativeValuesDefinitionAction.tsx b/packages/hub/src/app/relative-values/[owner]/[slug]/DeleteRelativeValuesDefinitionAction.tsx index cbc419575e..55de367939 100644 --- a/packages/hub/src/app/relative-values/[owner]/[slug]/DeleteRelativeValuesDefinitionAction.tsx +++ b/packages/hub/src/app/relative-values/[owner]/[slug]/DeleteRelativeValuesDefinitionAction.tsx @@ -1,65 +1,29 @@ import { useRouter } from "next/navigation"; -import { FC, useCallback } from "react"; -import { useMutation } from "react-relay"; -import { graphql } from "relay-runtime"; +import { FC } from "react"; import { DropdownMenuAsyncActionItem, TrashIcon, useToast } from "@quri/ui"; -import { DeleteRelativeValuesDefinitionActionMutation } from "@/__generated__/DeleteRelativeValuesDefinitionActionMutation.graphql"; - -const Mutation = graphql` - mutation DeleteRelativeValuesDefinitionActionMutation( - $input: MutationDeleteRelativeValuesDefinitionInput! - ) { - result: deleteRelativeValuesDefinition(input: $input) { - __typename - ... on BaseError { - message - } - } - } -`; +import { deleteRelativeValuesDefinitionAction } from "@/server/relative-values/actions/deleteRelativeValuesDefinitionAction"; type Props = { owner: string; slug: string; - close(): void; }; -export const DeleteDefinitionAction: FC = ({ owner, slug, close }) => { +export const DeleteDefinitionAction: FC = ({ owner, slug }) => { const router = useRouter(); - const [mutation] = - useMutation(Mutation); - const toast = useToast(); - const onClick = useCallback((): Promise => { - return new Promise(() => { - mutation({ - variables: { input: { owner, slug } }, - onCompleted(response) { - if (response.result.__typename === "BaseError") { - toast(response.result.message, "error"); - close(); - } else { - router.push("/"); - } - }, - onError(e) { - toast(e.toString(), "error"); - close(); - }, - }); - }); - }, [mutation, owner, slug, close, router, toast]); - return ( { + await deleteRelativeValuesDefinitionAction({ owner, slug }); + toast("Definition deleted", "confirmation"); + router.push("/"); + }} icon={TrashIcon} - close={close} /> ); }; diff --git a/packages/hub/src/graphql/mutations/deleteRelativeValuesDefinition.tsx b/packages/hub/src/graphql/mutations/deleteRelativeValuesDefinition.tsx deleted file mode 100644 index c084a90f44..0000000000 --- a/packages/hub/src/graphql/mutations/deleteRelativeValuesDefinition.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { getWriteableOwnerBySlug } from "../helpers/ownerHelpers"; - -builder.mutationField("deleteRelativeValuesDefinition", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("DeleteRelativeValuesDefinitionResult", { - fields: (t) => ({ - ok: t.boolean(), - }), - }), - input: { - owner: t.input.string({ required: true }), - slug: t.input.string({ required: true }), - }, - errors: {}, - async resolve(_, { input }, { session }) { - const owner = await getWriteableOwnerBySlug(session, input.owner); - - await prisma.relativeValuesDefinition.delete({ - where: { - slug_ownerId: { - slug: input.slug, - ownerId: owner.id, - }, - }, - }); - - return { ok: true }; - }, - }) -); diff --git a/packages/hub/src/graphql/schema.ts b/packages/hub/src/graphql/schema.ts index 91f72d5c77..8013f3cd2b 100644 --- a/packages/hub/src/graphql/schema.ts +++ b/packages/hub/src/graphql/schema.ts @@ -13,7 +13,6 @@ import "./mutations/buildRelativeValuesCache"; import "./mutations/cancelGroupInvite"; import "./mutations/clearRelativeValuesCache"; import "./mutations/createGroup"; -import "./mutations/deleteRelativeValuesDefinition"; import "./mutations/updateSquiggleSnippetModel"; import { builder } from "./builder"; diff --git a/packages/hub/src/server/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx b/packages/hub/src/server/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx new file mode 100644 index 0000000000..ba0b0d575f --- /dev/null +++ b/packages/hub/src/server/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx @@ -0,0 +1,29 @@ +"use server"; +import { z } from "zod"; + +import { prisma } from "@/prisma"; +import { getSessionOrRedirect } from "@/server/users/auth"; +import { makeServerAction, zSlug } from "@/server/utils"; + +import { getWriteableOwnerBySlug } from "../../../graphql/helpers/ownerHelpers"; + +export const deleteRelativeValuesDefinitionAction = makeServerAction( + z.object({ + owner: zSlug, + slug: zSlug, + }), + async (input) => { + const session = await getSessionOrRedirect(); + + const owner = await getWriteableOwnerBySlug(session, input.owner); + + await prisma.relativeValuesDefinition.delete({ + where: { + slug_ownerId: { + slug: input.slug, + ownerId: owner.id, + }, + }, + }); + } +); From 2c2c2f3012414bac2f82406778a72f410b0e6f32 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 29 Nov 2024 12:32:31 -0300 Subject: [PATCH 41/68] migrate new group page --- packages/hub/src/app/new/group/NewGroup.tsx | 37 +++----------- .../[owner]/[slug]/DefinitionLayout.tsx | 3 +- .../hub/src/graphql/mutations/createGroup.ts | 49 ------------------- packages/hub/src/graphql/schema.ts | 1 - .../groups/actions/createGroupAction.ts | 45 +++++++++++++++++ 5 files changed, 53 insertions(+), 82 deletions(-) delete mode 100644 packages/hub/src/graphql/mutations/createGroup.ts create mode 100644 packages/hub/src/server/groups/actions/createGroupAction.ts diff --git a/packages/hub/src/app/new/group/NewGroup.tsx b/packages/hub/src/app/new/group/NewGroup.tsx index e285000ad9..a3f0770601 100644 --- a/packages/hub/src/app/new/group/NewGroup.tsx +++ b/packages/hub/src/app/new/group/NewGroup.tsx @@ -2,33 +2,14 @@ import { useRouter } from "next/navigation"; import { FC } from "react"; import { FormProvider } from "react-hook-form"; -import { graphql } from "relay-runtime"; import { Button } from "@quri/ui"; import { H1 } from "@/components/ui/Headers"; import { SlugFormField } from "@/components/ui/SlugFormField"; -import { useMutationForm } from "@/hooks/useMutationForm"; +import { useServerActionForm } from "@/hooks/useServerActionForm"; import { groupRoute } from "@/routes"; - -import { NewGroupMutation } from "@/__generated__/NewGroupMutation.graphql"; - -const Mutation = graphql` - mutation NewGroupMutation($input: MutationCreateGroupInput!) { - result: createGroup(input: $input) { - __typename - ... on BaseError { - message - } - ... on CreateGroupResult { - group { - id - slug - } - } - } - } -`; +import { createGroupAction } from "@/server/groups/actions/createGroupAction"; export const NewGroup: FC = () => { const router = useRouter(); @@ -37,23 +18,19 @@ export const NewGroup: FC = () => { slug: string | undefined; }; - const { form, onSubmit, inFlight } = useMutationForm< + const { form, onSubmit, inFlight } = useServerActionForm< FormShape, - NewGroupMutation, - "CreateGroupResult" + typeof createGroupAction >({ defaultValues: {}, mode: "onChange", - mutation: Mutation, - expectedTypename: "CreateGroupResult", blockOnSuccess: true, formDataToVariables: (data) => ({ - input: { - slug: data.slug ?? "", // shouldn't happen, but satisfies TypeScript - }, + slug: data.slug ?? "", // shouldn't happen, but satisfies TypeScript }), + action: createGroupAction, onCompleted(result) { - router.push(groupRoute({ slug: result.group.slug })); + router.push(groupRoute({ slug: result.slug })); }, }); diff --git a/packages/hub/src/app/relative-values/[owner]/[slug]/DefinitionLayout.tsx b/packages/hub/src/app/relative-values/[owner]/[slug]/DefinitionLayout.tsx index cbe0f43537..27f687de3d 100644 --- a/packages/hub/src/app/relative-values/[owner]/[slug]/DefinitionLayout.tsx +++ b/packages/hub/src/app/relative-values/[owner]/[slug]/DefinitionLayout.tsx @@ -92,12 +92,11 @@ export const DefinitionLayout: FC = ({ queryRef, children }) => { })} /> ( + render={() => ( )} diff --git a/packages/hub/src/graphql/mutations/createGroup.ts b/packages/hub/src/graphql/mutations/createGroup.ts deleted file mode 100644 index 94de887636..0000000000 --- a/packages/hub/src/graphql/mutations/createGroup.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { prisma } from "@/prisma"; - -import { indexGroupId } from "../../server/search/helpers"; -import { builder } from "../builder"; -import { rethrowOnConstraint } from "../errors/common"; -import { Group } from "../types/Group"; - -builder.mutationField("createGroup", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("CreateGroupResult", { - fields: (t) => ({ - group: t.field({ type: Group }), - }), - }), - errors: {}, - input: { - slug: t.input.string({ required: true }), - }, - resolve: async (_, { input }, { session }) => { - const user = await prisma.user.findUniqueOrThrow({ - where: { email: session.user.email }, - }); - - const group = await rethrowOnConstraint( - () => - prisma.group.create({ - data: { - asOwner: { - create: { - slug: input.slug, - }, - }, - memberships: { - create: [{ userId: user.id, role: "Admin" }], - }, - }, - }), - { - target: ["slug"], - error: `The group ${input.slug} already exists`, - } - ); - - await indexGroupId(group.id); - - return { group }; - }, - }) -); diff --git a/packages/hub/src/graphql/schema.ts b/packages/hub/src/graphql/schema.ts index 8013f3cd2b..b90a94fa97 100644 --- a/packages/hub/src/graphql/schema.ts +++ b/packages/hub/src/graphql/schema.ts @@ -12,7 +12,6 @@ import "../server/search/actions/adminRebuildSearchIndexAction"; import "./mutations/buildRelativeValuesCache"; import "./mutations/cancelGroupInvite"; import "./mutations/clearRelativeValuesCache"; -import "./mutations/createGroup"; import "./mutations/updateSquiggleSnippetModel"; import { builder } from "./builder"; diff --git a/packages/hub/src/server/groups/actions/createGroupAction.ts b/packages/hub/src/server/groups/actions/createGroupAction.ts new file mode 100644 index 0000000000..5d5e23fbc4 --- /dev/null +++ b/packages/hub/src/server/groups/actions/createGroupAction.ts @@ -0,0 +1,45 @@ +"use server"; +import { z } from "zod"; + +import { prisma } from "@/prisma"; +import { getSessionOrRedirect } from "@/server/users/auth"; +import { makeServerAction, rethrowOnConstraint, zSlug } from "@/server/utils"; + +import { indexGroupId } from "../../search/helpers"; + +export const createGroupAction = makeServerAction( + z.object({ + slug: zSlug, + }), + async (input): Promise<{ slug: string }> => { + const session = await getSessionOrRedirect(); + + const user = await prisma.user.findUniqueOrThrow({ + where: { email: session.user.email }, + }); + + const group = await rethrowOnConstraint( + () => + prisma.group.create({ + data: { + asOwner: { + create: { + slug: input.slug, + }, + }, + memberships: { + create: [{ userId: user.id, role: "Admin" }], + }, + }, + }), + { + target: ["slug"], + error: `The group ${input.slug} already exists`, + } + ); + + await indexGroupId(group.id); + + return { slug: input.slug }; + } +); From 47552e8735af5f5288a395ae46038e069713be0f Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 29 Nov 2024 16:28:13 -0300 Subject: [PATCH 42/68] convert relative values code --- .../src/app/(frontpage)/definitions/page.tsx | 2 +- packages/hub/src/app/(frontpage)/page.tsx | 2 +- packages/hub/src/app/api/find-owners/route.ts | 2 +- .../src/app/api/find-relative-values/route.ts | 24 +++ packages/hub/src/app/groups/[slug]/page.tsx | 2 +- .../[owner]/[slug]/DeleteModelAction.tsx | 2 +- .../app/models/[owner]/[slug]/ModelLayout.tsx | 2 +- .../[owner]/[slug]/ModelPrivacyControls.tsx | 2 +- .../[owner]/[slug]/ModelSettingsButton.tsx | 2 +- .../models/[owner]/[slug]/MoveModelAction.tsx | 2 +- .../[owner]/[slug]/UpdateModelSlugAction.tsx | 2 +- .../src/app/models/[owner]/[slug]/layout.tsx | 2 +- .../BuildRelativeValuesCacheAction.tsx | 58 ++---- .../ClearRelativeValuesCacheAction.tsx | 63 ++----- .../[variableName]/CacheMenu/index.tsx | 34 ++-- .../RelativeValuesModelLayout.tsx | 177 +----------------- .../relative-values/[variableName]/Tabs.tsx | 49 +++++ .../relative-values/[variableName]/layout.tsx | 100 ++++++++-- .../[slug]/revisions/ModelRevisionsList.tsx | 2 +- .../models/[owner]/[slug]/revisions/page.tsx | 2 +- .../[owner]/[slug]/useFixModelUrlCasing.ts | 2 +- .../[slug]/view/ViewSquiggleSnippet.tsx | 2 +- .../app/models/[owner]/[slug]/view/page.tsx | 2 +- .../[owner]/[slug]/DefinitionLayout.tsx | 48 +---- .../[slug]/RelativeValuesDefinitionPage.tsx | 99 ++-------- .../edit/EditRelativeValuesDefinition.tsx | 50 ++--- .../[owner]/[slug]/edit/page.tsx | 38 ++-- .../relative-values/[owner]/[slug]/layout.tsx | 24 ++- .../relative-values/[owner]/[slug]/page.tsx | 28 +-- .../app/users/[username]/definitions/page.tsx | 2 +- .../hub/src/app/users/[username]/page.tsx | 2 +- .../exports/EditRelativeValueExports.tsx | 8 +- .../SelectRelativeValuesDefinition.tsx | 64 +++---- .../ui/CloseDropdownOnInvariantChange.tsx | 21 +++ .../ui/ServerActionDropdownAction.tsx | 27 +-- .../mutations/buildRelativeValuesCache.ts | 110 ----------- .../mutations/clearRelativeValuesCache.ts | 43 ----- packages/hub/src/graphql/schema.ts | 4 +- .../hub/src/models/components/ModelCard.tsx | 2 +- .../hub/src/models/components/ModelList.tsx | 2 +- .../RelativeValuesDefinitionCard.tsx | 4 +- .../RelativeValuesDefinitionList.tsx | 4 +- .../RelativeValuesDefinitionRevision.tsx | 32 +--- .../views/RelativeValuesProvider.tsx | 7 +- .../models/actions/loadModelCardAction.ts | 2 +- .../server/models/data/{card.ts => cards.ts} | 33 +--- .../hub/src/server/models/data/helpers.ts | 2 +- .../owners/{data.ts => data/findOwners.ts} | 0 .../hub/src/server/owners/data/typedOwner.ts | 38 ++++ .../actions/buildRelativeValuesCacheAction.ts | 113 +++++++++++ .../actions/clearRelativeValuesCacheAction.ts | 58 ++++++ .../{data.ts => data/cards.ts} | 53 ++++-- .../server/relative-values/data/exports.ts | 85 +++++++++ .../data/findRelativeValuesForSelect.ts | 31 +++ .../src/server/relative-values/data/full.ts | 101 ++++++++++ .../server/relative-values/data/fullExport.ts | 106 +++++++++++ .../src/squiggle/components/ImportTooltip.tsx | 2 +- 57 files changed, 972 insertions(+), 808 deletions(-) create mode 100644 packages/hub/src/app/api/find-relative-values/route.ts create mode 100644 packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/Tabs.tsx create mode 100644 packages/hub/src/components/ui/CloseDropdownOnInvariantChange.tsx delete mode 100644 packages/hub/src/graphql/mutations/buildRelativeValuesCache.ts delete mode 100644 packages/hub/src/graphql/mutations/clearRelativeValuesCache.ts rename packages/hub/src/server/models/data/{card.ts => cards.ts} (85%) rename packages/hub/src/server/owners/{data.ts => data/findOwners.ts} (100%) create mode 100644 packages/hub/src/server/owners/data/typedOwner.ts create mode 100644 packages/hub/src/server/relative-values/actions/buildRelativeValuesCacheAction.ts create mode 100644 packages/hub/src/server/relative-values/actions/clearRelativeValuesCacheAction.ts rename packages/hub/src/server/relative-values/{data.ts => data/cards.ts} (56%) create mode 100644 packages/hub/src/server/relative-values/data/exports.ts create mode 100644 packages/hub/src/server/relative-values/data/findRelativeValuesForSelect.ts create mode 100644 packages/hub/src/server/relative-values/data/full.ts create mode 100644 packages/hub/src/server/relative-values/data/fullExport.ts diff --git a/packages/hub/src/app/(frontpage)/definitions/page.tsx b/packages/hub/src/app/(frontpage)/definitions/page.tsx index 92df30d193..d0cac5566e 100644 --- a/packages/hub/src/app/(frontpage)/definitions/page.tsx +++ b/packages/hub/src/app/(frontpage)/definitions/page.tsx @@ -1,5 +1,5 @@ import { RelativeValuesDefinitionList } from "@/relative-values/components/RelativeValuesDefinitionList"; -import { loadDefinitionCards } from "@/server/relative-values/data"; +import { loadDefinitionCards } from "@/server/relative-values/data/cards"; export default async function DefinitionsPage() { const page = await loadDefinitionCards(); diff --git a/packages/hub/src/app/(frontpage)/page.tsx b/packages/hub/src/app/(frontpage)/page.tsx index 32f08efd89..fd896f9923 100644 --- a/packages/hub/src/app/(frontpage)/page.tsx +++ b/packages/hub/src/app/(frontpage)/page.tsx @@ -1,5 +1,5 @@ import { ModelList } from "@/models/components/ModelList"; -import { loadModelCards } from "@/server/models/data/card"; +import { loadModelCards } from "@/server/models/data/cards"; export default async function FrontPage() { const page = await loadModelCards(); diff --git a/packages/hub/src/app/api/find-owners/route.ts b/packages/hub/src/app/api/find-owners/route.ts index ba41288cea..81a7656cc7 100644 --- a/packages/hub/src/app/api/find-owners/route.ts +++ b/packages/hub/src/app/api/find-owners/route.ts @@ -1,7 +1,7 @@ import { NextRequest } from "next/server"; import { z } from "zod"; -import { findOwnersForSelect } from "@/server/owners/data"; +import { findOwnersForSelect } from "@/server/owners/data/findOwners"; // We're not calling this as a server actions because it'd be too slow (server actions are sequential). // TODO: it'd be good to use tRPC for this. diff --git a/packages/hub/src/app/api/find-relative-values/route.ts b/packages/hub/src/app/api/find-relative-values/route.ts new file mode 100644 index 0000000000..496abc56ad --- /dev/null +++ b/packages/hub/src/app/api/find-relative-values/route.ts @@ -0,0 +1,24 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; + +import { findRelativeValuesForSelect } from "@/server/relative-values/data/findRelativeValuesForSelect"; + +// We're not calling this as a server actions because it'd be too slow (server actions are sequential). +// TODO: it'd be good to use tRPC for this. +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + + const { owner, slugContains } = z + .object({ + owner: z.string(), + slugContains: z.string(), + }) + .parse(Object.fromEntries(searchParams.entries())); + + return Response.json( + await findRelativeValuesForSelect({ + owner, + slugContains, + }) + ); +} diff --git a/packages/hub/src/app/groups/[slug]/page.tsx b/packages/hub/src/app/groups/[slug]/page.tsx index da68121152..395b93a6f3 100644 --- a/packages/hub/src/app/groups/[slug]/page.tsx +++ b/packages/hub/src/app/groups/[slug]/page.tsx @@ -1,6 +1,6 @@ import { ModelList } from "@/models/components/ModelList"; import { hasGroupMembership } from "@/server/groups/data/helpers"; -import { loadModelCards } from "@/server/models/data/card"; +import { loadModelCards } from "@/server/models/data/cards"; type Props = { params: Promise<{ slug: string }>; diff --git a/packages/hub/src/app/models/[owner]/[slug]/DeleteModelAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/DeleteModelAction.tsx index 58e9e91d0f..f5c8a8a52c 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/DeleteModelAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/DeleteModelAction.tsx @@ -5,7 +5,7 @@ import { DropdownMenuAsyncActionItem, TrashIcon, useToast } from "@quri/ui"; import { ownerRoute } from "@/routes"; import { deleteModelAction } from "@/server/models/actions/deleteModelAction"; -import { ModelCardDTO } from "@/server/models/data/card"; +import { ModelCardDTO } from "@/server/models/data/cards"; type Props = { model: ModelCardDTO; diff --git a/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx b/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx index 99025f9328..d3bbd6b348 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx @@ -12,7 +12,7 @@ import { VariablesDropdown, } from "@/lib/VariablesDropdown"; import { modelRevisionsRoute, modelRoute } from "@/routes"; -import { ModelCardDTO } from "@/server/models/data/card"; +import { ModelCardDTO } from "@/server/models/data/cards"; import { getExportedVariableNames } from "@/server/models/utils"; import { ModelEntityNodes } from "./ModelEntityNodes"; diff --git a/packages/hub/src/app/models/[owner]/[slug]/ModelPrivacyControls.tsx b/packages/hub/src/app/models/[owner]/[slug]/ModelPrivacyControls.tsx index d11b0a771b..a6ce61d13f 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/ModelPrivacyControls.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/ModelPrivacyControls.tsx @@ -6,7 +6,7 @@ import { Dropdown, DropdownMenu, GlobeIcon, LockIcon } from "@quri/ui"; import { ServerActionDropdownAction } from "@/components/ui/ServerActionDropdownAction"; import { updateModelPrivacyAction } from "@/server/models/actions/updateModelPrivacyAction"; -import { ModelCardDTO } from "@/server/models/data/card"; +import { ModelCardDTO } from "@/server/models/data/cards"; function getIconComponent(isPrivate: boolean) { return isPrivate ? LockIcon : GlobeIcon; diff --git a/packages/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx b/packages/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx index ad81797d2b..0b956c0d5a 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx @@ -4,7 +4,7 @@ import { FC } from "react"; import { Cog8ToothIcon, Dropdown, DropdownMenu } from "@quri/ui"; import { EntityTab } from "@/components/ui/EntityTab"; -import { ModelCardDTO } from "@/server/models/data/card"; +import { ModelCardDTO } from "@/server/models/data/cards"; import { DeleteModelAction } from "./DeleteModelAction"; import { MoveModelAction } from "./MoveModelAction"; diff --git a/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx index 1b2a2c8765..0ef5988472 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx @@ -7,7 +7,7 @@ import { SelectOwner, SelectOwnerOption } from "@/components/SelectOwner"; import { ServerActionModalAction } from "@/components/ui/ServerActionModalAction"; import { modelRoute } from "@/routes"; import { moveModelAction } from "@/server/models/actions/moveModelAction"; -import { ModelCardDTO } from "@/server/models/data/card"; +import { ModelCardDTO } from "@/server/models/data/cards"; import { draftUtils, modelToDraftLocator } from "./SquiggleSnippetDraftDialog"; diff --git a/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx index 8c53ac1442..b8fb096914 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx @@ -7,7 +7,7 @@ import { ServerActionModalAction } from "@/components/ui/ServerActionModalAction import { SlugFormField } from "@/components/ui/SlugFormField"; import { modelRoute } from "@/routes"; import { updateModelSlugAction } from "@/server/models/actions/updateModelSlugAction"; -import { ModelCardDTO } from "@/server/models/data/card"; +import { ModelCardDTO } from "@/server/models/data/cards"; import { draftUtils, modelToDraftLocator } from "./SquiggleSnippetDraftDialog"; diff --git a/packages/hub/src/app/models/[owner]/[slug]/layout.tsx b/packages/hub/src/app/models/[owner]/[slug]/layout.tsx index d62c04a625..96d22f8360 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/layout.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/layout.tsx @@ -2,7 +2,7 @@ import { Metadata } from "next"; import { notFound } from "next/navigation"; import { PropsWithChildren, Suspense } from "react"; -import { loadModelCard } from "@/server/models/data/card"; +import { loadModelCard } from "@/server/models/data/cards"; import { isModelEditable } from "@/server/models/data/helpers"; import { FallbackModelLayout } from "./FallbackLayout"; diff --git a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/BuildRelativeValuesCacheAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/BuildRelativeValuesCacheAction.tsx index e16eac570a..8e49cf4b7f 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/BuildRelativeValuesCacheAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/BuildRelativeValuesCacheAction.tsx @@ -1,57 +1,29 @@ "use client"; import { FC } from "react"; -import { graphql } from "react-relay"; -import { DropdownMenuAsyncActionItem, RefreshIcon } from "@quri/ui"; +import { RefreshIcon, useToast } from "@quri/ui"; -import { useAsyncMutation } from "@/hooks/useAsyncMutation"; - -import { BuildRelativeValuesCacheActionMutation } from "@/__generated__/BuildRelativeValuesCacheActionMutation.graphql"; - -export const Mutation = graphql` - mutation BuildRelativeValuesCacheActionMutation( - $input: MutationBuildRelativeValuesCacheInput! - ) { - result: buildRelativeValuesCache(input: $input) { - __typename - ... on BaseError { - message - } - ... on BuildRelativeValuesCacheResult { - relativeValuesExport { - id - cache { - firstItem - secondItem - resultJSON - errorString - } - } - } - } - } -`; +import { ServerActionDropdownAction } from "@/components/ui/ServerActionDropdownAction"; +import { buildRelativeValuesCacheAction } from "@/server/relative-values/actions/buildRelativeValuesCacheAction"; +import { RelativeValuesExportFullDTO } from "@/server/relative-values/data/fullExport"; export const BuildRelativeValuesCacheAction: FC<{ - exportId: string; - close(): void; -}> = ({ exportId, close }) => { + relativeValuesExport: RelativeValuesExportFullDTO; +}> = ({ relativeValuesExport }) => { // TODO - fill cache in ModelEvaluator and re-render - const [runMutation] = - useAsyncMutation({ - mutation: Mutation, - confirmation: "Cache filled", - expectedTypename: "BuildRelativeValuesCacheResult", - }); - - const act = () => runMutation({ variables: { input: { exportId } } }); + const toast = useToast(); return ( - { + await buildRelativeValuesCacheAction({ + exportId: relativeValuesExport.id, + }); + toast("Cache filled", "confirmation"); + }} + invariant={1} // close is controlled by the parent /> ); }; diff --git a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/ClearRelativeValuesCacheAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/ClearRelativeValuesCacheAction.tsx index 7bcd13c7da..71e6c5d570 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/ClearRelativeValuesCacheAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/ClearRelativeValuesCacheAction.tsx @@ -1,62 +1,29 @@ "use strict"; import { FC } from "react"; -import { graphql } from "react-relay"; -import { DropdownMenuAsyncActionItem, TrashIcon } from "@quri/ui"; +import { TrashIcon, useToast } from "@quri/ui"; -import { useAsyncMutation } from "@/hooks/useAsyncMutation"; - -import { ClearRelativeValuesCacheActionMutation } from "@/__generated__/ClearRelativeValuesCacheActionMutation.graphql"; - -export const Mutation = graphql` - mutation ClearRelativeValuesCacheActionMutation( - $input: MutationClearRelativeValuesCacheInput! - ) { - result: clearRelativeValuesCache(input: $input) { - __typename - ... on BaseError { - message - } - ... on ClearRelativeValuesCacheResult { - relativeValuesExport { - id - cache { - firstItem - secondItem - resultJSON - errorString - } - } - } - } - } -`; +import { ServerActionDropdownAction } from "@/components/ui/ServerActionDropdownAction"; +import { clearRelativeValuesCacheAction } from "@/server/relative-values/actions/clearRelativeValuesCacheAction"; +import { RelativeValuesExportFullDTO } from "@/server/relative-values/data/fullExport"; export const ClearRelativeValuesCacheAction: FC<{ - exportId: string; - close(): void; -}> = ({ exportId, close }) => { + relativeValuesExport: RelativeValuesExportFullDTO; +}> = ({ relativeValuesExport }) => { // TODO - clear cache in ModelEvaluator and re-render - const [runMutation] = - useAsyncMutation({ - mutation: Mutation, - confirmation: "Cache cleared", - expectedTypename: "ClearRelativeValuesCacheResult", - }); - - const act = () => - runMutation({ - variables: { - input: { exportId }, - }, - }); + const toast = useToast(); return ( - { + await clearRelativeValuesCacheAction({ + exportId: relativeValuesExport.id, + }); + toast("Cache cleared", "confirmation"); + }} + invariant={1} // close is controlled by the parent /> ); }; diff --git a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/index.tsx b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/index.tsx index 1a2df16d8f..600b222efe 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/index.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/index.tsx @@ -1,7 +1,6 @@ "use client"; import { clsx } from "clsx"; import { FC, ReactElement } from "react"; -import { useFragment } from "react-relay"; import { CheckIcon, @@ -11,29 +10,24 @@ import { XIcon, } from "@quri/ui"; -import { RelativeValuesDefinitionRevisionFragment } from "@/relative-values/components/RelativeValuesDefinitionRevision"; +import { CloseDropdownOnInvariantChange } from "@/components/ui/CloseDropdownOnInvariantChange"; +import { RelativeValuesDefinitionFullDTO } from "@/server/relative-values/data/full"; +import { RelativeValuesExportFullDTO } from "@/server/relative-values/data/fullExport"; import { BuildRelativeValuesCacheAction } from "./BuildRelativeValuesCacheAction"; import { ClearRelativeValuesCacheAction } from "./ClearRelativeValuesCacheAction"; -import { RelativeValuesDefinitionRevision$key } from "@/__generated__/RelativeValuesDefinitionRevision.graphql"; -import { RelativeValuesExport$data } from "@/__generated__/RelativeValuesExport.graphql"; - export const CacheMenu: FC<{ - relativeValuesExport: RelativeValuesExport$data; + relativeValuesExport: RelativeValuesExportFullDTO; + definitionRevision: RelativeValuesDefinitionFullDTO["currentRevision"]; isEditable: boolean; -}> = ({ relativeValuesExport, isEditable }) => { - const definition = useFragment( - RelativeValuesDefinitionRevisionFragment, - relativeValuesExport.definition.currentRevision - ); - +}> = ({ relativeValuesExport, definitionRevision, isEditable }) => { const isEmpty = relativeValuesExport.cache.length === 0; const fullyCached = !isEmpty && relativeValuesExport.cache.length >= - definition.items.length * definition.items.length; + definitionRevision.items.length * definitionRevision.items.length; const internals = (
( { + render={() => { return ( + {isEmpty ? "Not cached" : `${relativeValuesExport.cache.length}/${ - definition.items.length * definition.items.length + definitionRevision.items.length * + definitionRevision.items.length } pairs cached`} {!fullyCached && ( )} {isEmpty ? null : ( )} diff --git a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/RelativeValuesModelLayout.tsx b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/RelativeValuesModelLayout.tsx index 328a74d7e1..8307bd20fc 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/RelativeValuesModelLayout.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/RelativeValuesModelLayout.tsx @@ -2,126 +2,32 @@ import { FC, PropsWithChildren, useEffect, useState } from "react"; import Skeleton from "react-loading-skeleton"; -import { graphql, useFragment } from "react-relay"; import { result } from "@quri/squiggle-lang"; -import { - Bars4Icon, - LinkIcon, - ScaleIcon, - ScatterPlotIcon, - TableCellsIcon, -} from "@quri/ui"; -import { StyledLink } from "@/components/ui/StyledLink"; -import { StyledTabLink } from "@/components/ui/StyledTabLink"; -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { RelativeValuesDefinitionRevisionFragment } from "@/relative-values/components/RelativeValuesDefinitionRevision"; import { RelativeValuesProvider } from "@/relative-values/components/views/RelativeValuesProvider"; import { ModelEvaluator } from "@/relative-values/values/ModelEvaluator"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; -import { - modelForRelativeValuesExportRoute, - relativeValuesRoute, -} from "@/routes"; - -import { CacheMenu } from "./CacheMenu"; -import { RelativeValuesExport } from "./RelativeValuesExport"; - -import { RelativeValuesDefinitionRevision$key } from "@/__generated__/RelativeValuesDefinitionRevision.graphql"; -import { RelativeValuesExport$key } from "@/__generated__/RelativeValuesExport.graphql"; -import { RelativeValuesModelLayoutQuery } from "@/__generated__/RelativeValuesModelLayoutQuery.graphql"; +import { RelativeValuesDefinitionFullDTO } from "@/server/relative-values/data/full"; +import { RelativeValuesExportFullDTO } from "@/server/relative-values/data/fullExport"; export const RelativeValuesModelLayout: FC< PropsWithChildren<{ - query: SerializablePreloadedQuery; + code: string; variableName: string; + cache: RelativeValuesExportFullDTO["cache"]; + definitionRevision: RelativeValuesDefinitionFullDTO["currentRevision"]; }> -> = ({ query, variableName, children }) => { - const [{ model: result }] = usePageQuery( - graphql` - # used in ModelEvaluator - query RelativeValuesModelLayoutQuery( - $input: QueryModelInput! - $forRelativeValues: ModelRevisionForRelativeValuesInput! - ) { - model(input: $input) { - __typename - ... on Model { - id - slug - isEditable - ...EditRelativeValueExports_Model - owner { - slug - } - currentRevision { - id - content { - __typename - ... on SquiggleSnippet { - id - code - version - } - } - - forRelativeValues(input: $forRelativeValues) { - __typename - ... on BaseError { - message - } - ... on RelativeValuesExport { - ...RelativeValuesExport - } - } - } - } - } - } - `, - query - ); - const model = extractFromGraphqlErrorUnion(result, "Model"); - const revision = model.currentRevision; - - const content = extractFromGraphqlErrorUnion( - revision.content, - "SquiggleSnippet" - ); - - const forRelativeValuesKey = extractFromGraphqlErrorUnion( - revision.forRelativeValues, - "RelativeValuesExport" - ); - - const forRelativeValues = useFragment( - RelativeValuesExport, - forRelativeValuesKey - ); - - const definition = forRelativeValues.definition; - - const definitionRevision = useFragment( - RelativeValuesDefinitionRevisionFragment, - forRelativeValues.definition.currentRevision - ); - +> = ({ code, variableName, cache, definitionRevision, children }) => { const [evaluatorResult, setEvaluatorResult] = useState< result | undefined >(); useEffect(() => { // ModelEvaluator.create is async because SqProject.run is async - ModelEvaluator.create( - content.code, - variableName, - forRelativeValues.cache - ).then(setEvaluatorResult); - }, [content.code, variableName, forRelativeValues]); + ModelEvaluator.create(code, variableName, cache).then(setEvaluatorResult); + }, [code, variableName, cache]); - const body = evaluatorResult ? ( + return evaluatorResult ? ( evaluatorResult.ok ? ( ); - - const definitionLink = ( - - - {`${definition.owner.slug}/${definition.slug}`} - - ); - - return ( -
-
-
- -
- {variableName} -
-
-
- {definitionLink} - - - - - - -
-
- {body} -
- ); }; diff --git a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/Tabs.tsx b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/Tabs.tsx new file mode 100644 index 0000000000..e2194ca7e5 --- /dev/null +++ b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/Tabs.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { FC } from "react"; + +import { Bars4Icon, ScatterPlotIcon, TableCellsIcon } from "@quri/ui"; + +import { StyledTabLink } from "@/components/ui/StyledTabLink"; +import { modelForRelativeValuesExportRoute } from "@/routes"; +import { ModelCardDTO } from "@/server/models/data/cards"; + +// must be a client component because we can't pass icons from server components to client components +export const RelativeValuesTabs: FC<{ + model: ModelCardDTO; + variableName: string; +}> = ({ model, variableName }) => { + return ( + + + + + + ); +}; diff --git a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/layout.tsx b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/layout.tsx index 9a888646db..d2e2824e0e 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/layout.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/layout.tsx @@ -1,12 +1,18 @@ +import { notFound } from "next/navigation"; import { PropsWithChildren } from "react"; -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { LinkIcon, ScaleIcon } from "@quri/ui"; -import { RelativeValuesModelLayout } from "./RelativeValuesModelLayout"; +import { StyledLink } from "@/components/ui/StyledLink"; +import { relativeValuesRoute } from "@/routes"; +import { loadModelCard } from "@/server/models/data/cards"; +import { isModelEditable } from "@/server/models/data/helpers"; +import { loadRelativeValuesDefinitionFull } from "@/server/relative-values/data/full"; +import { loadRelativeValuesExportFullFromModelRevision } from "@/server/relative-values/data/fullExport"; -import QueryNode, { - RelativeValuesModelLayoutQuery, -} from "@/__generated__/RelativeValuesModelLayoutQuery.graphql"; +import { CacheMenu } from "./CacheMenu"; +import { RelativeValuesModelLayout } from "./RelativeValuesModelLayout"; +import { RelativeValuesTabs } from "./Tabs"; export default async function Layout({ params, @@ -15,19 +21,83 @@ export default async function Layout({ params: Promise<{ owner: string; slug: string; variableName: string }>; }>) { const { owner, slug, variableName } = await params; - const query = await loadPageQuery(QueryNode, { - input: { - owner, - slug, - }, - forRelativeValues: { + + // sleep + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const model = await loadModelCard({ owner, slug }); + if (!model) { + notFound(); + } + + const isEditable = await isModelEditable(model); + + const revision = model.currentRevision; + + const relativeValuesExport = + await loadRelativeValuesExportFullFromModelRevision({ + modelRevisionId: revision.id, variableName, - }, + }); + + if (!relativeValuesExport) { + notFound(); + } + + const definition = await loadRelativeValuesDefinitionFull({ + owner: relativeValuesExport.definition.owner, + slug: relativeValuesExport.definition.slug, }); + if (!definition) { + notFound(); + } + + const content = revision.squiggleSnippet; + if (!content) { + throw new Error("No SquiggleSnippet content"); + } + + const definitionLink = ( + + + {`${definition.owner.slug}/${definition.slug}`} + + ); + return ( - - {children} - +
+
+
+ +
+ {variableName} +
+
+
+ {definitionLink} + + +
+
+ + {children} + +
); } diff --git a/packages/hub/src/app/models/[owner]/[slug]/revisions/ModelRevisionsList.tsx b/packages/hub/src/app/models/[owner]/[slug]/revisions/ModelRevisionsList.tsx index 3f0d6d0ac6..bbd84dadcb 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/revisions/ModelRevisionsList.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/revisions/ModelRevisionsList.tsx @@ -8,7 +8,7 @@ import { UsernameLink } from "@/components/UsernameLink"; import { usePaginator } from "@/hooks/usePaginator"; import { commonDateFormat } from "@/lib/common"; import { modelRevisionRoute } from "@/routes"; -import { ModelCardDTO } from "@/server/models/data/card"; +import { ModelCardDTO } from "@/server/models/data/cards"; import { ModelRevisionDTO } from "@/server/models/data/revisions"; import { Paginated } from "@/server/types"; diff --git a/packages/hub/src/app/models/[owner]/[slug]/revisions/page.tsx b/packages/hub/src/app/models/[owner]/[slug]/revisions/page.tsx index fcade354bc..f4e688cfdf 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/revisions/page.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/revisions/page.tsx @@ -1,6 +1,6 @@ import { notFound } from "next/navigation"; -import { loadModelCard } from "@/server/models/data/card"; +import { loadModelCard } from "@/server/models/data/cards"; import { loadModelRevisions } from "@/server/models/data/revisions"; import { ModelRevisionsList } from "./ModelRevisionsList"; diff --git a/packages/hub/src/app/models/[owner]/[slug]/useFixModelUrlCasing.ts b/packages/hub/src/app/models/[owner]/[slug]/useFixModelUrlCasing.ts index a2441096c0..7028701e21 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/useFixModelUrlCasing.ts +++ b/packages/hub/src/app/models/[owner]/[slug]/useFixModelUrlCasing.ts @@ -1,7 +1,7 @@ import { usePathname, useRouter } from "next/navigation"; import { patchModelRoute } from "@/routes"; -import { ModelCardDTO } from "@/server/models/data/card"; +import { ModelCardDTO } from "@/server/models/data/cards"; export function useFixModelUrlCasing(model: ModelCardDTO) { const router = useRouter(); diff --git a/packages/hub/src/app/models/[owner]/[slug]/view/ViewSquiggleSnippet.tsx b/packages/hub/src/app/models/[owner]/[slug]/view/ViewSquiggleSnippet.tsx index cc5c46a77f..7ec61298ee 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/view/ViewSquiggleSnippet.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/view/ViewSquiggleSnippet.tsx @@ -6,7 +6,7 @@ import { versionedSquigglePackages, } from "@quri/versioned-squiggle-components"; -import { ModelCardDTO } from "@/server/models/data/card"; +import { ModelCardDTO } from "@/server/models/data/cards"; import { sqProjectWithHubLinker } from "@/squiggle/components/linker"; type Props = { diff --git a/packages/hub/src/app/models/[owner]/[slug]/view/page.tsx b/packages/hub/src/app/models/[owner]/[slug]/view/page.tsx index c6ea463f97..a4e61e1605 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/view/page.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/view/page.tsx @@ -1,7 +1,7 @@ import { notFound } from "next/navigation"; import { ViewSquiggleSnippet } from "@/app/models/[owner]/[slug]/view/ViewSquiggleSnippet"; -import { loadModelCard } from "@/server/models/data/card"; +import { loadModelCard } from "@/server/models/data/cards"; type Props = { params: Promise<{ owner: string; slug: string }>; diff --git a/packages/hub/src/app/relative-values/[owner]/[slug]/DefinitionLayout.tsx b/packages/hub/src/app/relative-values/[owner]/[slug]/DefinitionLayout.tsx index 27f687de3d..b818ed22f6 100644 --- a/packages/hub/src/app/relative-values/[owner]/[slug]/DefinitionLayout.tsx +++ b/packages/hub/src/app/relative-values/[owner]/[slug]/DefinitionLayout.tsx @@ -1,6 +1,5 @@ "use client"; import { FC, PropsWithChildren } from "react"; -import { graphql } from "relay-runtime"; import { Cog8ToothIcon, @@ -13,54 +12,25 @@ import { import { EntityInfo } from "@/components/EntityInfo"; import { EntityLayout, EntityNode } from "@/components/EntityLayout"; import { EntityTab } from "@/components/ui/EntityTab"; -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; import { ownerRoute, relativeValuesEditRoute, relativeValuesRoute, } from "@/routes"; +import { RelativeValuesDefinitionCardDTO } from "@/server/relative-values/data/cards"; import { DeleteDefinitionAction } from "./DeleteRelativeValuesDefinitionAction"; -import { DefinitionLayoutQuery } from "@/__generated__/DefinitionLayoutQuery.graphql"; - type Props = PropsWithChildren<{ - queryRef: SerializablePreloadedQuery; + definition: RelativeValuesDefinitionCardDTO; + isEditable: boolean; }>; -export const DefinitionLayout: FC = ({ queryRef, children }) => { - const [{ result }] = usePageQuery( - graphql` - query DefinitionLayoutQuery($input: QueryRelativeValuesDefinitionInput!) { - result: relativeValuesDefinition(input: $input) { - __typename - ... on BaseError { - message - } - ... on NotFoundError { - message - } - ... on RelativeValuesDefinition { - id - slug - isEditable - owner { - __typename - slug - } - } - } - } - `, - queryRef - ); - - const definition = extractFromGraphqlErrorUnion( - result, - "RelativeValuesDefinition" - ); +export const DefinitionLayout: FC = ({ + definition, + isEditable, + children, +}) => { const slug = definition.slug; const nodes: EntityNode[] = [ @@ -76,7 +46,7 @@ export const DefinitionLayout: FC = ({ queryRef, children }) => { } headerRight={ - definition.isEditable ? ( + isEditable ? ( = ({ exportRef }) => { - const modelExport = useFragment( - graphql` - fragment RelativeValuesDefinitionPage_export on RelativeValuesExport { - id - variableName - modelRevision { - model { - slug - isPrivate - owner { - slug - } - } - } - } - `, - exportRef - ); - + modelExport: RelativeValuesExportCardDTO; +}> = ({ modelExport }) => { return (
; -}> = ({ query }) => { - const [{ relativeValuesDefinition: result }] = usePageQuery( - RelativeValuesDefinitionPageQuery, - query - ); - - const definitionRef = extractFromGraphqlErrorUnion( - result, - "RelativeValuesDefinition" - ); - - const definition = useFragment( - RelativeValuesDefinitionPageFragment, - definitionRef - ); - + definition: RelativeValuesDefinitionFullDTO; + modelExports: RelativeValuesExportCardDTO[]; +}> = ({ definition, modelExports }) => { return (
- {definition.modelExports.length ? ( + {modelExports.length ? (

Implemented by:

- {definition.modelExports.map((row) => ( - + {modelExports.map((row) => ( + ))}
) : null}
- +
); }; diff --git a/packages/hub/src/app/relative-values/[owner]/[slug]/edit/EditRelativeValuesDefinition.tsx b/packages/hub/src/app/relative-values/[owner]/[slug]/edit/EditRelativeValuesDefinition.tsx index 83b2f16bb3..e66d8a69a7 100644 --- a/packages/hub/src/app/relative-values/[owner]/[slug]/edit/EditRelativeValuesDefinition.tsx +++ b/packages/hub/src/app/relative-values/[owner]/[slug]/edit/EditRelativeValuesDefinition.tsx @@ -1,49 +1,19 @@ "use client"; import { useRouter } from "next/navigation"; import { FC } from "react"; -import { useFragment } from "react-relay"; -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; import { RelativeValuesDefinitionForm } from "@/relative-values/components/RelativeValuesDefinitionForm"; import { FormShape } from "@/relative-values/components/RelativeValuesDefinitionForm/FormShape"; -import { RelativeValuesDefinitionRevisionFragment } from "@/relative-values/components/RelativeValuesDefinitionRevision"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; import { relativeValuesRoute } from "@/routes"; import { updateRelativeValuesDefinitionAction } from "@/server/relative-values/actions/updateRelativeValuesDefinitionAction"; - -import { - RelativeValuesDefinitionPageFragment, - RelativeValuesDefinitionPageQuery, -} from "../RelativeValuesDefinitionPage"; - -import { RelativeValuesDefinitionPage$key } from "@/__generated__/RelativeValuesDefinitionPage.graphql"; -import { RelativeValuesDefinitionPageQuery as QueryType } from "@/__generated__/RelativeValuesDefinitionPageQuery.graphql"; -import { RelativeValuesDefinitionRevision$key } from "@/__generated__/RelativeValuesDefinitionRevision.graphql"; +import { RelativeValuesDefinitionFullDTO } from "@/server/relative-values/data/full"; export const EditRelativeValuesDefinition: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [{ relativeValuesDefinition: result }] = usePageQuery( - RelativeValuesDefinitionPageQuery, - query - ); - - const definitionRef = extractFromGraphqlErrorUnion( - result, - "RelativeValuesDefinition" - ); - + definition: RelativeValuesDefinitionFullDTO; +}> = ({ definition }) => { const router = useRouter(); - const definition = useFragment( - RelativeValuesDefinitionPageFragment, - definitionRef - ); - const revision = useFragment( - RelativeValuesDefinitionRevisionFragment, - definition.currentRevision - ); + const revision = definition.currentRevision; const save = async (data: FormShape) => { await updateRelativeValuesDefinitionAction({ @@ -73,9 +43,15 @@ export const EditRelativeValuesDefinition: FC<{ defaultValues={{ slug: "", // unused but necessary for types title: revision.title, - items: revision.items, - clusters: revision.clusters, - recommendedUnit: revision.recommendedUnit, + items: revision.items.map((item) => ({ + ...item, + clusterId: item.clusterId ?? null, + })), + clusters: revision.clusters.map((cluster) => ({ + ...cluster, + recommendedUnit: cluster.recommendedUnit ?? null, + })), + recommendedUnit: revision.recommendedUnit ?? null, }} withoutSlug save={save} diff --git a/packages/hub/src/app/relative-values/[owner]/[slug]/edit/page.tsx b/packages/hub/src/app/relative-values/[owner]/[slug]/edit/page.tsx index 8d031f779e..45a0f63340 100644 --- a/packages/hub/src/app/relative-values/[owner]/[slug]/edit/page.tsx +++ b/packages/hub/src/app/relative-values/[owner]/[slug]/edit/page.tsx @@ -1,10 +1,9 @@ -import QueryNode, { - RelativeValuesDefinitionPageQuery, -} from "@gen/RelativeValuesDefinitionPageQuery.graphql"; +import { notFound } from "next/navigation"; import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; -import { WithAuth } from "@/components/WithAuth"; -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { controlsOwnerId } from "@/server/owners/auth"; +import { loadRelativeValuesDefinitionFull } from "@/server/relative-values/data/full"; +import { getSessionUserOrRedirect } from "@/server/users/auth"; import { EditRelativeValuesDefinition } from "./EditRelativeValuesDefinition"; @@ -12,26 +11,23 @@ type Props = { params: Promise<{ owner: string; slug: string }>; }; -async function InnerPage({ params }: Props) { +export default async function Page({ params }: Props) { + // if we're not signed in, we can't edit + await getSessionUserOrRedirect(); + const { owner, slug } = await params; - const query = await loadPageQuery( - QueryNode, - { - input: { owner, slug }, - } - ); + const definition = await loadRelativeValuesDefinitionFull({ owner, slug }); + if (!definition) { + notFound(); + } + + if (!(await controlsOwnerId(definition.owner.id))) { + throw new Error("Unauthorized"); + } return ( - + ); } - -export default async function Page({ params }: Props) { - return ( - - - - ); -} diff --git a/packages/hub/src/app/relative-values/[owner]/[slug]/layout.tsx b/packages/hub/src/app/relative-values/[owner]/[slug]/layout.tsx index 5b7685db70..a5be319b27 100644 --- a/packages/hub/src/app/relative-values/[owner]/[slug]/layout.tsx +++ b/packages/hub/src/app/relative-values/[owner]/[slug]/layout.tsx @@ -1,24 +1,30 @@ import { Metadata } from "next"; +import { notFound } from "next/navigation"; import { PropsWithChildren } from "react"; -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { controlsOwnerId } from "@/server/owners/auth"; +import { loadRelativeValuesDefinitionCard } from "@/server/relative-values/data/cards"; import { DefinitionLayout } from "./DefinitionLayout"; -import QueryNode, { - DefinitionLayoutQuery, -} from "@/__generated__/DefinitionLayoutQuery.graphql"; - type Props = PropsWithChildren<{ params: Promise<{ owner: string; slug: string }>; }>; export default async function Layout({ params, children }: Props) { const { owner, slug } = await params; - const query = await loadPageQuery(QueryNode, { - input: { owner, slug }, - }); - return {children}; + const definition = await loadRelativeValuesDefinitionCard({ owner, slug }); + + if (!definition) { + notFound(); + } + + const isEditable = await controlsOwnerId(definition.owner.id); + return ( + + {children} + + ); } export async function generateMetadata({ params }: Props): Promise { diff --git a/packages/hub/src/app/relative-values/[owner]/[slug]/page.tsx b/packages/hub/src/app/relative-values/[owner]/[slug]/page.tsx index c7daff7641..66c2ca6495 100644 --- a/packages/hub/src/app/relative-values/[owner]/[slug]/page.tsx +++ b/packages/hub/src/app/relative-values/[owner]/[slug]/page.tsx @@ -1,8 +1,7 @@ -import QueryNode, { - RelativeValuesDefinitionPageQuery, -} from "@gen/RelativeValuesDefinitionPageQuery.graphql"; +import { notFound } from "next/navigation"; -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { loadRelativeValuesExportCardsFromDefinition } from "@/server/relative-values/data/exports"; +import { loadRelativeValuesDefinitionFull } from "@/server/relative-values/data/full"; import { RelativeValuesDefinitionPage } from "./RelativeValuesDefinitionPage"; @@ -12,12 +11,19 @@ type Props = { export default async function OuterDefinitionPage({ params }: Props) { const { owner, slug } = await params; - const query = await loadPageQuery( - QueryNode, - { - input: { owner, slug }, - } - ); + const definition = await loadRelativeValuesDefinitionFull({ owner, slug }); + + if (!definition) { + notFound(); + } - return ; + const modelExports = + await loadRelativeValuesExportCardsFromDefinition(definition); + + return ( + + ); } diff --git a/packages/hub/src/app/users/[username]/definitions/page.tsx b/packages/hub/src/app/users/[username]/definitions/page.tsx index 524b321b99..327a82593a 100644 --- a/packages/hub/src/app/users/[username]/definitions/page.tsx +++ b/packages/hub/src/app/users/[username]/definitions/page.tsx @@ -1,7 +1,7 @@ import { Metadata } from "next"; import { RelativeValuesDefinitionList } from "@/relative-values/components/RelativeValuesDefinitionList"; -import { loadDefinitionCards } from "@/server/relative-values/data"; +import { loadDefinitionCards } from "@/server/relative-values/data/cards"; type Props = { params: Promise<{ username: string }>; diff --git a/packages/hub/src/app/users/[username]/page.tsx b/packages/hub/src/app/users/[username]/page.tsx index ee5c78b43b..81a4f5e53d 100644 --- a/packages/hub/src/app/users/[username]/page.tsx +++ b/packages/hub/src/app/users/[username]/page.tsx @@ -1,7 +1,7 @@ import { Metadata } from "next"; import { ModelList } from "@/models/components/ModelList"; -import { loadModelCards } from "@/server/models/data/card"; +import { loadModelCards } from "@/server/models/data/cards"; type Props = { params: Promise<{ username: string }>; diff --git a/packages/hub/src/components/exports/EditRelativeValueExports.tsx b/packages/hub/src/components/exports/EditRelativeValueExports.tsx index 8738afcdae..cf3ae1efd0 100644 --- a/packages/hub/src/components/exports/EditRelativeValueExports.tsx +++ b/packages/hub/src/components/exports/EditRelativeValueExports.tsx @@ -8,16 +8,14 @@ import { relativeValuesRoute, } from "@/routes"; import { ModelFullDTO } from "@/server/models/data/full"; +import { FindRelativeValuesForSelectResult } from "@/server/relative-values/data/findRelativeValuesForSelect"; import { SelectOwner, SelectOwnerOption } from "../SelectOwner"; import { FormModal } from "../ui/FormModal"; import { H2 } from "../ui/Headers"; import { StyledDefinitionLink } from "../ui/StyledDefinitionLink"; import { StyledLink } from "../ui/StyledLink"; -import { - SelectRelativeValuesDefinition, - SelectRelativeValuesDefinitionOption, -} from "./SelectRelativeValuesDefinition"; +import { SelectRelativeValuesDefinition } from "./SelectRelativeValuesDefinition"; import { RelativeValuesExportInput } from "@/__generated__/EditSquiggleSnippetModelMutation.graphql"; @@ -28,7 +26,7 @@ const CreateVariableWithDefinitionModal: FC<{ type FormShape = { variableName: string; owner: SelectOwnerOption | null; - definition: SelectRelativeValuesDefinitionOption | null; + definition: FindRelativeValuesForSelectResult | null; }; type ValidatedFormShape = { diff --git a/packages/hub/src/components/exports/SelectRelativeValuesDefinition.tsx b/packages/hub/src/components/exports/SelectRelativeValuesDefinition.tsx index 8e2cc3646b..6643e7c4d4 100644 --- a/packages/hub/src/components/exports/SelectRelativeValuesDefinition.tsx +++ b/packages/hub/src/components/exports/SelectRelativeValuesDefinition.tsx @@ -1,36 +1,14 @@ import { FC, useEffect } from "react"; import { FieldPathByValue, FieldValues, useFormContext } from "react-hook-form"; -import { useRelayEnvironment } from "react-relay"; -import { fetchQuery, graphql } from "relay-runtime"; +import { z } from "zod"; import { SelectFormField } from "@quri/ui"; -import { SelectOwnerOption } from "../SelectOwner"; - -import { - SelectRelativeValuesDefinitionQuery, - SelectRelativeValuesDefinitionQuery$data, -} from "@/__generated__/SelectRelativeValuesDefinitionQuery.graphql"; - -const Query = graphql` - query SelectRelativeValuesDefinitionQuery( - $input: RelativeValuesDefinitionsQueryInput! - ) { - relativeValuesDefinitions(input: $input) { - edges { - node { - id - slug - } - } - } - } -`; +import { FindRelativeValuesForSelectResult } from "@/server/relative-values/data/findRelativeValuesForSelect"; -export type SelectRelativeValuesDefinitionOption = - SelectRelativeValuesDefinitionQuery$data["relativeValuesDefinitions"]["edges"][number]["node"]; +import { SelectOwnerOption } from "../SelectOwner"; -const DefinitionInfo: FC<{ option: SelectRelativeValuesDefinitionOption }> = ({ +const DefinitionInfo: FC<{ option: FindRelativeValuesForSelectResult }> = ({ option, }) =>
{option.slug}
; @@ -41,36 +19,40 @@ export function SelectRelativeValuesDefinition< label, ownerFieldName, }: { - name: FieldPathByValue; + name: FieldPathByValue; label?: string; ownerFieldName: FieldPathByValue; }) { const { watch, resetField } = useFormContext(); - const environment = useRelayEnvironment(); const owner: SelectOwnerOption | null = watch(ownerFieldName); const loadOptions = async ( inputValue: string - ): Promise => { + ): Promise => { if (!owner) { return []; } - const result = await fetchQuery( - environment, - Query, - { - input: { - owner: owner.slug, - slugContains: inputValue, - }, - } - ).toPromise(); + const result = await fetch( + `/api/find-relative-values?${new URLSearchParams({ + owner: owner.slug, + slugContains: inputValue, + })}` + ).then((r) => r.json()); + + const data = z + .array( + z.object({ + id: z.string(), + slug: z.string(), + }) + ) + .parse(result); if (!result) { return []; } - return result.relativeValuesDefinitions.edges.map((edge) => edge.node); + return result; }; useEffect(() => { @@ -78,7 +60,7 @@ export function SelectRelativeValuesDefinition< }, [name, owner, resetField]); return ( - + key={owner?.slug ?? " "} // re-render the component when owner changes; this helps with default select options on initial open name={name} label={label} diff --git a/packages/hub/src/components/ui/CloseDropdownOnInvariantChange.tsx b/packages/hub/src/components/ui/CloseDropdownOnInvariantChange.tsx new file mode 100644 index 0000000000..28ea1bf2ed --- /dev/null +++ b/packages/hub/src/components/ui/CloseDropdownOnInvariantChange.tsx @@ -0,0 +1,21 @@ +import { FC, useEffect, useState } from "react"; + +import { useCloseDropdown } from "@quri/ui"; + +export const useCloseDropdownOnInvariantChange = (invariant: unknown) => { + const close = useCloseDropdown(); + const [initialInvariant] = useState(invariant); + useEffect(() => { + if (invariant !== initialInvariant) { + close(); + } + }, [invariant, initialInvariant, close]); +}; + +export const CloseDropdownOnInvariantChange: FC<{ + invariant: unknown; +}> = ({ invariant }) => { + useCloseDropdownOnInvariantChange(invariant); + + return null; +}; diff --git a/packages/hub/src/components/ui/ServerActionDropdownAction.tsx b/packages/hub/src/components/ui/ServerActionDropdownAction.tsx index 028aa704fc..79c390cff4 100644 --- a/packages/hub/src/components/ui/ServerActionDropdownAction.tsx +++ b/packages/hub/src/components/ui/ServerActionDropdownAction.tsx @@ -1,35 +1,40 @@ -import { FC, useEffect, useState, useTransition } from "react"; +import { FC, useTransition } from "react"; import { DropdownMenuActionItem, IconProps, useCloseDropdown } from "@quri/ui"; +import { useCloseDropdownOnInvariantChange } from "./CloseDropdownOnInvariantChange"; + export const ServerActionDropdownAction: FC<{ title: string; icon?: FC; act: () => Promise; - // If set, the dropdown will close only when the invariant changes. + // If set, the dropdown will close only after the invariant changes. + // This is useful, because server action returns before it sends back the revalidated UI. + // Re-rendering the new UI might take a while (it's async), so we don't want to close the dropdown immediately. + // This is an ugly workaround; see also: https://github.com/vercel/next.js/discussions/53206 + // Discussion in QURI Slack: https://quri.slack.com/archives/C059EEU0HMM/p1732810277978719 + // + // Also note that in some cases even `invariant` is not enough. Consider the scenario where the list of items in the dropdown is based on the component props. + // In this case, this action would be unmounted before it would get the chance to close the dropdown. + // In that scenario you might prefer to use `` instead. + // (The example of this is ``.) invariant?: unknown; }> = ({ title, icon, act: originalAct, invariant }) => { - const [initialInvariant] = useState(invariant); const close = useCloseDropdown(); const [isPending, startTransition] = useTransition(); const act = () => { startTransition(async () => { await originalAct(); + + // if there's no invariant, close the dropdown immediately if (invariant === undefined) { close(); } }); }; - // We can't just call `close()` in the transition; server action finishes before it sends back the revalidated UI. - // This is an ugly workaround; see also: https://github.com/vercel/next.js/discussions/53206 - // Discussion in QURI Slack: https://quri.slack.com/archives/C059EEU0HMM/p1732810277978719 - useEffect(() => { - if (invariant !== initialInvariant) { - close(); - } - }, [invariant, initialInvariant, close]); + useCloseDropdownOnInvariantChange(invariant); return ( - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("BuildRelativeValuesCacheResult", { - fields: (t) => ({ - relativeValuesExport: t.field({ type: RelativeValuesExport }), - }), - }), - errors: {}, - input: { - exportId: t.input.string({ required: true }), - }, - resolve: async (_, { input }, { session }) => { - const exportId = decodeGlobalIdWithTypename( - input.exportId, - "RelativeValuesExport" - ); - - const relativeValuesExport = - await getRelativeValuesExportForWriteableModel({ - exportId, - session, - }); - - const { modelRevision } = relativeValuesExport; - - if (modelRevision.contentType !== "SquiggleSnippet") { - throw new Error("Unsupported model revision content type"); - } - - const squiggleSnippet = modelRevision.squiggleSnippet; - if (!squiggleSnippet) { - throw new Error("Model content not found"); - } - - const evaluatorResult = await ModelEvaluator.create( - squiggleSnippet.code, - relativeValuesExport.variableName - ); - if (!evaluatorResult.ok) { - throw new Error( - `Failed to create evaluator: ${evaluatorResult.value.toString()}` - ); - } - const evaluator = evaluatorResult.value; - - const definitionRevision = - relativeValuesExport.definition.currentRevision; - if (!definitionRevision) { - throw new Error("Definition revision not found"); - } - - const items = relativeValuesItemsSchema.parse(definitionRevision.items); - const itemIds = items.map((item) => item.id); - - const existingCacheItems = await prisma.relativeValuesPairCache.findMany({ - where: { - exportId, - }, - select: { - firstItem: true, - secondItem: true, - }, - }); - - const seen: Record> = {}; - for (const row of existingCacheItems) { - seen[row.firstItem] ??= {}; - seen[row.firstItem][row.secondItem] = true; - } - - for (const [firstItem, secondItem] of cartesianProduct( - itemIds, - itemIds - )) { - if (seen[firstItem]?.[secondItem]) { - continue; // already cached - } - const result = evaluator.compareWithoutCache(firstItem, secondItem); - await prisma.relativeValuesPairCache.create({ - data: { - exportId, - firstItem, - secondItem, - ...(result.ok - ? { result: result.value } - : { error: result.value.toString() }), - }, - }); - } - - const updatedRelativeValuesExport = - await prisma.relativeValuesExport.findUniqueOrThrow({ - where: { id: exportId }, - }); - return { relativeValuesExport: updatedRelativeValuesExport }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/clearRelativeValuesCache.ts b/packages/hub/src/graphql/mutations/clearRelativeValuesCache.ts deleted file mode 100644 index e5a7e5f400..0000000000 --- a/packages/hub/src/graphql/mutations/clearRelativeValuesCache.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { - getRelativeValuesExportForWriteableModel, - RelativeValuesExport, -} from "../types/RelativeValuesExport"; -import { decodeGlobalIdWithTypename } from "../utils"; - -builder.mutationField("clearRelativeValuesCache", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("ClearRelativeValuesCacheResult", { - fields: (t) => ({ - relativeValuesExport: t.field({ type: RelativeValuesExport }), - }), - }), - errors: {}, - input: { - exportId: t.input.string({ required: true }), - }, - resolve: async (_, { input }, { session }) => { - const exportId = decodeGlobalIdWithTypename( - input.exportId, - "RelativeValuesExport" - ); - - await getRelativeValuesExportForWriteableModel({ - exportId, - session, - }); - - await prisma.relativeValuesPairCache.deleteMany({ - where: { exportId }, - }); - - const updatedRelativeValuesExport = - await prisma.relativeValuesExport.findUniqueOrThrow({ - where: { id: exportId }, - }); - return { relativeValuesExport: updatedRelativeValuesExport }; - }, - }) -); diff --git a/packages/hub/src/graphql/schema.ts b/packages/hub/src/graphql/schema.ts index b90a94fa97..fc5d361341 100644 --- a/packages/hub/src/graphql/schema.ts +++ b/packages/hub/src/graphql/schema.ts @@ -9,9 +9,9 @@ import "./queries/relativeValuesDefinitions"; import "./queries/userByUsername"; import "./mutations/adminUpdateModelVersion"; import "../server/search/actions/adminRebuildSearchIndexAction"; -import "./mutations/buildRelativeValuesCache"; +import "../server/relative-values/actions/buildRelativeValuesCacheAction"; import "./mutations/cancelGroupInvite"; -import "./mutations/clearRelativeValuesCache"; +import "../server/relative-values/actions/clearRelativeValuesCacheAction"; import "./mutations/updateSquiggleSnippetModel"; import { builder } from "./builder"; diff --git a/packages/hub/src/models/components/ModelCard.tsx b/packages/hub/src/models/components/ModelCard.tsx index 983f84a73e..a7ad20bf2c 100644 --- a/packages/hub/src/models/components/ModelCard.tsx +++ b/packages/hub/src/models/components/ModelCard.tsx @@ -17,7 +17,7 @@ import { VariablesDropdown, } from "@/lib/VariablesDropdown"; import { modelRoute, ownerRoute } from "@/routes"; -import { ModelCardDTO } from "@/server/models/data/card"; +import { ModelCardDTO } from "@/server/models/data/cards"; type Props = { model: ModelCardDTO; diff --git a/packages/hub/src/models/components/ModelList.tsx b/packages/hub/src/models/components/ModelList.tsx index bb6f66b09b..aa50a5fe6d 100644 --- a/packages/hub/src/models/components/ModelList.tsx +++ b/packages/hub/src/models/components/ModelList.tsx @@ -3,7 +3,7 @@ import { FC } from "react"; import { LoadMore } from "@/components/LoadMore"; import { usePaginator } from "@/hooks/usePaginator"; -import { ModelCardDTO } from "@/server/models/data/card"; +import { ModelCardDTO } from "@/server/models/data/cards"; import { Paginated } from "@/server/types"; import { ModelCard } from "./ModelCard"; diff --git a/packages/hub/src/relative-values/components/RelativeValuesDefinitionCard.tsx b/packages/hub/src/relative-values/components/RelativeValuesDefinitionCard.tsx index 182c66196f..f1492eab41 100644 --- a/packages/hub/src/relative-values/components/RelativeValuesDefinitionCard.tsx +++ b/packages/hub/src/relative-values/components/RelativeValuesDefinitionCard.tsx @@ -2,10 +2,10 @@ import { FC } from "react"; import { EntityCard, UpdatedStatus } from "@/components/EntityCard"; import { relativeValuesRoute } from "@/routes"; -import { RelativeValuesDefinitionCardData } from "@/server/relative-values/data"; +import { RelativeValuesDefinitionCardDTO } from "@/server/relative-values/data/cards"; type Props = { - definition: RelativeValuesDefinitionCardData; + definition: RelativeValuesDefinitionCardDTO; showOwner?: boolean; }; diff --git a/packages/hub/src/relative-values/components/RelativeValuesDefinitionList.tsx b/packages/hub/src/relative-values/components/RelativeValuesDefinitionList.tsx index 4d7410d2c4..317beb2050 100644 --- a/packages/hub/src/relative-values/components/RelativeValuesDefinitionList.tsx +++ b/packages/hub/src/relative-values/components/RelativeValuesDefinitionList.tsx @@ -4,13 +4,13 @@ import { FC } from "react"; import { LoadMore } from "@/components/LoadMore"; import { usePaginator } from "@/hooks/usePaginator"; -import { RelativeValuesDefinitionCardData } from "@/server/relative-values/data"; +import { RelativeValuesDefinitionCardDTO } from "@/server/relative-values/data/cards"; import { Paginated } from "@/server/types"; import { RelativeValuesDefinitionCard } from "./RelativeValuesDefinitionCard"; type Props = { - page: Paginated; + page: Paginated; showOwner?: boolean; }; diff --git a/packages/hub/src/relative-values/components/RelativeValuesDefinitionRevision.tsx b/packages/hub/src/relative-values/components/RelativeValuesDefinitionRevision.tsx index 78862f901f..c95a297664 100644 --- a/packages/hub/src/relative-values/components/RelativeValuesDefinitionRevision.tsx +++ b/packages/hub/src/relative-values/components/RelativeValuesDefinitionRevision.tsx @@ -1,45 +1,19 @@ import { FC, Fragment } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; import { StyledTab, StyledTextArea } from "@quri/ui"; import { H2 } from "@/components/ui/Headers"; +import { RelativeValuesDefinitionFullDTO } from "@/server/relative-values/data/full"; import { ClusterInfo } from "./common/ClusterInfo"; -import { RelativeValuesDefinitionRevision$key } from "@/__generated__/RelativeValuesDefinitionRevision.graphql"; - -export const RelativeValuesDefinitionRevisionFragment = graphql` - fragment RelativeValuesDefinitionRevision on RelativeValuesDefinitionRevision { - title - clusters { - id - color - recommendedUnit - } - items { - id - name - description - clusterId - } - recommendedUnit - } -`; - type Props = { - dataRef: RelativeValuesDefinitionRevision$key; + revision: RelativeValuesDefinitionFullDTO["currentRevision"]; }; export const RelativeValuesDefinitionRevision: FC = ({ - dataRef: definitionRef, + revision: content, }) => { - const content = useFragment( - RelativeValuesDefinitionRevisionFragment, - definitionRef - ); - const clusters = Object.fromEntries( content.clusters.map((cluster) => [cluster.id, cluster]) ); diff --git a/packages/hub/src/relative-values/components/views/RelativeValuesProvider.tsx b/packages/hub/src/relative-values/components/views/RelativeValuesProvider.tsx index f6a04eb400..1bdd9b3928 100644 --- a/packages/hub/src/relative-values/components/views/RelativeValuesProvider.tsx +++ b/packages/hub/src/relative-values/components/views/RelativeValuesProvider.tsx @@ -4,11 +4,10 @@ import { FC, PropsWithChildren, Reducer } from "react"; import { generateProvider } from "@quri/ui"; import { ModelEvaluator } from "@/relative-values/values/ModelEvaluator"; +import { RelativeValuesDefinitionFullDTO } from "@/server/relative-values/data/full"; import { Filter } from "./types"; -import { RelativeValuesDefinitionRevision$data } from "@/__generated__/RelativeValuesDefinitionRevision.graphql"; - export type Axis = "rows" | "columns"; export type GridMode = "full" | "half"; @@ -27,7 +26,7 @@ export type AxisConfig = { type ViewContextShape = { evaluator: ModelEvaluator; - definition: RelativeValuesDefinitionRevision$data; + definition: RelativeValuesDefinitionFullDTO["currentRevision"]; gridMode: GridMode; axisConfig: { [k in Axis]: AxisConfig }; }; @@ -167,7 +166,7 @@ const { export const RelativeValuesProvider: FC< PropsWithChildren<{ - definition: RelativeValuesDefinitionRevision$data; + definition: RelativeValuesDefinitionFullDTO["currentRevision"]; evaluator: ModelEvaluator; }> > = ({ evaluator, definition, children }) => { diff --git a/packages/hub/src/server/models/actions/loadModelCardAction.ts b/packages/hub/src/server/models/actions/loadModelCardAction.ts index 23d056499f..108ff8395f 100644 --- a/packages/hub/src/server/models/actions/loadModelCardAction.ts +++ b/packages/hub/src/server/models/actions/loadModelCardAction.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import { makeServerAction, zSlug } from "@/server/utils"; -import { loadModelCard, ModelCardDTO } from "../data/card"; +import { loadModelCard, ModelCardDTO } from "../data/cards"; // Data-fetching action, used in ImportTooltip. // Don't use this for loading models; server actions are discouraged for data fetching. diff --git a/packages/hub/src/server/models/data/card.ts b/packages/hub/src/server/models/data/cards.ts similarity index 85% rename from packages/hub/src/server/models/data/card.ts rename to packages/hub/src/server/models/data/cards.ts index b1e46ab92e..8268f40c18 100644 --- a/packages/hub/src/server/models/data/card.ts +++ b/packages/hub/src/server/models/data/cards.ts @@ -3,6 +3,10 @@ import "server-only"; import { Prisma } from "@prisma/client"; import { prisma } from "@/prisma"; +import { + selectTypedOwner, + toTypedOwnerDTO, +} from "@/server/owners/data/typedOwner"; import { Paginated } from "../../types"; import { modelWhereHasAccess } from "./authHelpers"; @@ -21,23 +25,10 @@ function toDTO(dbModel: DbModelCard) { } check(dbModel); - const ownerToGraphqlCompatible = (owner: { - id: string; - slug: string; - user: { id: string } | null; - group: { id: string } | null; - }) => { - const __typename = owner.user ? "User" : "Group"; - return { - id: owner.id, - slug: owner.slug, - __typename, - }; - }; - return { + // FIXME - process each field separately ...dbModel, - owner: ownerToGraphqlCompatible(dbModel.owner), + owner: toTypedOwnerDTO(dbModel.owner), }; } @@ -46,16 +37,7 @@ const select = { slug: true, updatedAt: true, owner: { - select: { - id: true, - slug: true, - user: { - select: { id: true }, - }, - group: { - select: { id: true }, - }, - }, + select: selectTypedOwner, }, isPrivate: true, variables: { @@ -71,6 +53,7 @@ const select = { }, currentRevision: { select: { + id: true, contentType: true, squiggleSnippet: { select: { diff --git a/packages/hub/src/server/models/data/helpers.ts b/packages/hub/src/server/models/data/helpers.ts index 1876ed4511..18d4127758 100644 --- a/packages/hub/src/server/models/data/helpers.ts +++ b/packages/hub/src/server/models/data/helpers.ts @@ -1,6 +1,6 @@ import { controlsOwnerId } from "@/server/owners/auth"; -import { ModelCardDTO } from "./card"; +import { ModelCardDTO } from "./cards"; export async function isModelEditable(model: ModelCardDTO): Promise { return controlsOwnerId(model.owner.id); diff --git a/packages/hub/src/server/owners/data.ts b/packages/hub/src/server/owners/data/findOwners.ts similarity index 100% rename from packages/hub/src/server/owners/data.ts rename to packages/hub/src/server/owners/data/findOwners.ts diff --git a/packages/hub/src/server/owners/data/typedOwner.ts b/packages/hub/src/server/owners/data/typedOwner.ts new file mode 100644 index 0000000000..2dcb22e6d2 --- /dev/null +++ b/packages/hub/src/server/owners/data/typedOwner.ts @@ -0,0 +1,38 @@ +import { Prisma } from "@prisma/client"; + +import { prisma } from "@/prisma"; + +export type TypedOwner = { + __typename: "User" | "Group"; + id: string; + slug: string; +}; + +export const selectTypedOwner = { + id: true, + slug: true, + user: { + select: { id: true }, + }, + group: { + select: { id: true }, + }, +} satisfies Prisma.OwnerSelect; + +type DbTypedOwner = NonNullable< + Awaited< + ReturnType< + typeof prisma.owner.findFirst<{ select: typeof selectTypedOwner }> + > + > +>; + +// compatible with old GraphQL format +export function toTypedOwnerDTO(dbOwner: DbTypedOwner): TypedOwner { + const __typename = dbOwner.user ? "User" : "Group"; + return { + __typename, + id: dbOwner.id, + slug: dbOwner.slug, + }; +} diff --git a/packages/hub/src/server/relative-values/actions/buildRelativeValuesCacheAction.ts b/packages/hub/src/server/relative-values/actions/buildRelativeValuesCacheAction.ts new file mode 100644 index 0000000000..d7ec240120 --- /dev/null +++ b/packages/hub/src/server/relative-values/actions/buildRelativeValuesCacheAction.ts @@ -0,0 +1,113 @@ +"use server"; +import { revalidatePath } from "next/cache"; +import { z } from "zod"; + +import { prisma } from "@/prisma"; +import { cartesianProduct } from "@/relative-values/lib/utils"; +import { relativeValuesItemsSchema } from "@/relative-values/types"; +import { ModelEvaluator } from "@/relative-values/values/ModelEvaluator"; +import { modelForRelativeValuesExportRoute } from "@/routes"; +import { getSessionOrRedirect } from "@/server/users/auth"; +import { makeServerAction } from "@/server/utils"; + +import { getRelativeValuesExportForWriteableModel } from "../../../graphql/types/RelativeValuesExport"; + +export const buildRelativeValuesCacheAction = makeServerAction( + z.object({ + exportId: z.string(), + }), + async (input): Promise => { + const session = await getSessionOrRedirect(); + + const exportId = input.exportId; + + const relativeValuesExport = await getRelativeValuesExportForWriteableModel( + { + exportId, + session, + } + ); + + const { modelRevision } = relativeValuesExport; + + if (modelRevision.contentType !== "SquiggleSnippet") { + throw new Error("Unsupported model revision content type"); + } + + const squiggleSnippet = modelRevision.squiggleSnippet; + if (!squiggleSnippet) { + throw new Error("Model content not found"); + } + + const evaluatorResult = await ModelEvaluator.create( + squiggleSnippet.code, + relativeValuesExport.variableName + ); + if (!evaluatorResult.ok) { + throw new Error( + `Failed to create evaluator: ${evaluatorResult.value.toString()}` + ); + } + const evaluator = evaluatorResult.value; + + const definitionRevision = relativeValuesExport.definition.currentRevision; + if (!definitionRevision) { + throw new Error("Definition revision not found"); + } + + const items = relativeValuesItemsSchema.parse(definitionRevision.items); + const itemIds = items.map((item) => item.id); + + const existingCacheItems = await prisma.relativeValuesPairCache.findMany({ + where: { + exportId, + }, + select: { + firstItem: true, + secondItem: true, + }, + }); + + const seen: Record> = {}; + for (const row of existingCacheItems) { + seen[row.firstItem] ??= {}; + seen[row.firstItem][row.secondItem] = true; + } + + for (const [firstItem, secondItem] of cartesianProduct(itemIds, itemIds)) { + if (seen[firstItem]?.[secondItem]) { + continue; // already cached + } + const result = evaluator.compareWithoutCache(firstItem, secondItem); + await prisma.relativeValuesPairCache.create({ + data: { + exportId, + firstItem, + secondItem, + ...(result.ok + ? { result: result.value } + : { error: result.value.toString() }), + }, + }); + } + + // check that the export still exists - is this useful? + await prisma.relativeValuesExport.findUniqueOrThrow({ + where: { id: exportId }, + select: { + id: true, + }, + }); + + // sleep + await new Promise((resolve) => setTimeout(resolve, 1000)); + + revalidatePath( + modelForRelativeValuesExportRoute({ + owner: relativeValuesExport.modelRevision.model.owner.slug, + slug: relativeValuesExport.modelRevision.model.slug, + variableName: relativeValuesExport.variableName, + }) + ); + } +); diff --git a/packages/hub/src/server/relative-values/actions/clearRelativeValuesCacheAction.ts b/packages/hub/src/server/relative-values/actions/clearRelativeValuesCacheAction.ts new file mode 100644 index 0000000000..18c1d8151c --- /dev/null +++ b/packages/hub/src/server/relative-values/actions/clearRelativeValuesCacheAction.ts @@ -0,0 +1,58 @@ +"use server"; +import { revalidatePath } from "next/cache"; +import { z } from "zod"; + +import { prisma } from "@/prisma"; +import { modelForRelativeValuesExportRoute } from "@/routes"; + +import { getRelativeValuesExportForWriteableModel } from "../../../graphql/types/RelativeValuesExport"; +import { getSessionOrRedirect } from "../../users/auth"; +import { makeServerAction } from "../../utils"; + +export const clearRelativeValuesCacheAction = makeServerAction( + z.object({ + exportId: z.string(), + }), + async (input): Promise => { + const session = await getSessionOrRedirect(); + + const exportId = input.exportId; + + await getRelativeValuesExportForWriteableModel({ + exportId, + session, + }); + + await prisma.relativeValuesPairCache.deleteMany({ + where: { exportId }, + }); + + const relativeValuesExport = + await prisma.relativeValuesExport.findUniqueOrThrow({ + where: { id: exportId }, + select: { + modelRevision: { + select: { + model: { + select: { + owner: { + select: { slug: true }, + }, + slug: true, + }, + }, + }, + }, + variableName: true, + }, + }); + + revalidatePath( + modelForRelativeValuesExportRoute({ + owner: relativeValuesExport.modelRevision.model.owner.slug, + slug: relativeValuesExport.modelRevision.model.slug, + variableName: relativeValuesExport.variableName, + }) + ); + } +); diff --git a/packages/hub/src/server/relative-values/data.ts b/packages/hub/src/server/relative-values/data/cards.ts similarity index 56% rename from packages/hub/src/server/relative-values/data.ts rename to packages/hub/src/server/relative-values/data/cards.ts index af760f2874..5c1d755638 100644 --- a/packages/hub/src/server/relative-values/data.ts +++ b/packages/hub/src/server/relative-values/data/cards.ts @@ -3,17 +3,20 @@ import "server-only"; import { Prisma } from "@prisma/client"; import { prisma } from "@/prisma"; +import { + selectTypedOwner, + toTypedOwnerDTO, + TypedOwner, +} from "@/server/owners/data/typedOwner"; -import { Paginated } from "../types"; +import { Paginated } from "../../types"; -const definitionCardSelect = { +export const definitionCardSelect = { id: true, slug: true, updatedAt: true, owner: { - select: { - slug: true, - }, + select: selectTypedOwner, }, } satisfies Prisma.RelativeValuesDefinitionSelect; @@ -27,19 +30,26 @@ type DbDefinitionCard = NonNullable< > >; -export function dbDefinitionToDefinitionCard(dbDefinition: DbDefinitionCard) { +type DefinitionCardDTO = { + id: string; + slug: string; + owner: TypedOwner; + updatedAt: Date; +}; + +export function toDefinitionCardDTO( + dbDefinition: DbDefinitionCard +): DefinitionCardDTO { return { id: dbDefinition.id, slug: dbDefinition.slug, - owner: { - slug: dbDefinition.owner.slug, - }, + owner: toTypedOwnerDTO(dbDefinition.owner), updatedAt: dbDefinition.updatedAt, }; } -export type RelativeValuesDefinitionCardData = ReturnType< - typeof dbDefinitionToDefinitionCard +export type RelativeValuesDefinitionCardDTO = ReturnType< + typeof toDefinitionCardDTO >; export async function loadDefinitionCards( @@ -48,7 +58,7 @@ export async function loadDefinitionCards( cursor?: string; limit?: number; } = {} -): Promise> { +): Promise> { const limit = params.limit ?? 20; const dbDefinitions = await prisma.relativeValuesDefinition.findMany({ @@ -65,7 +75,7 @@ export async function loadDefinitionCards( take: limit + 1, }); - const definitions = dbDefinitions.map(dbDefinitionToDefinitionCard); + const definitions = dbDefinitions.map(toDefinitionCardDTO); const nextCursor = definitions[definitions.length - 1]?.id; @@ -79,3 +89,20 @@ export async function loadDefinitionCards( loadMore: definitions.length > limit ? loadMore : undefined, }; } + +export async function loadRelativeValuesDefinitionCard(params: { + owner: string; + slug: string; +}): Promise { + const dbDefinition = await prisma.relativeValuesDefinition.findFirst({ + select: definitionCardSelect, + where: { + owner: { + slug: params.owner, + }, + slug: params.slug, + }, + }); + + return dbDefinition ? toDefinitionCardDTO(dbDefinition) : null; +} diff --git a/packages/hub/src/server/relative-values/data/exports.ts b/packages/hub/src/server/relative-values/data/exports.ts new file mode 100644 index 0000000000..8c2e970626 --- /dev/null +++ b/packages/hub/src/server/relative-values/data/exports.ts @@ -0,0 +1,85 @@ +import { Prisma } from "@prisma/client"; + +import { auth } from "@/auth"; +import { modelWhereHasAccess } from "@/graphql/helpers/modelHelpers"; +import { prisma } from "@/prisma"; + +import { RelativeValuesDefinitionFullDTO } from "./full"; + +const select = { + id: true, + variableName: true, + modelRevision: { + select: { + model: { + select: { + slug: true, + isPrivate: true, + owner: { select: { slug: true } }, + }, + }, + }, + }, +} satisfies Prisma.RelativeValuesExportSelect; + +export type RelativeValuesExportCardDTO = { + id: string; + variableName: string; + modelRevision: { + model: { + slug: string; + isPrivate: boolean; + owner: { slug: string }; + }; + }; +}; + +function toDTO( + row: Prisma.RelativeValuesExportGetPayload<{ + select: typeof select; + }> +): RelativeValuesExportCardDTO { + return { + id: row.id, + variableName: row.variableName, + modelRevision: { + model: { + slug: row.modelRevision.model.slug, + isPrivate: row.modelRevision.model.isPrivate, + owner: { slug: row.modelRevision.model.owner.slug }, + }, + }, + }; +} + +export async function loadRelativeValuesExportCardsFromDefinition( + definition: RelativeValuesDefinitionFullDTO +): Promise { + const session = await auth(); + const models = await prisma.model.findMany({ + where: { + currentRevision: { + relativeValuesExports: { + some: { + definitionId: definition.id, + }, + }, + }, + ...modelWhereHasAccess(session), + }, + }); + + const rows = await prisma.relativeValuesExport.findMany({ + where: { + modelRevisionId: { + in: models + .map((model) => model.currentRevisionId) + .filter((id) => id !== null), + }, + definitionId: definition.id, + }, + select, + }); + + return rows.map(toDTO); +} diff --git a/packages/hub/src/server/relative-values/data/findRelativeValuesForSelect.ts b/packages/hub/src/server/relative-values/data/findRelativeValuesForSelect.ts new file mode 100644 index 0000000000..bf85cb8819 --- /dev/null +++ b/packages/hub/src/server/relative-values/data/findRelativeValuesForSelect.ts @@ -0,0 +1,31 @@ +import { prisma } from "@/prisma"; + +export type FindRelativeValuesForSelectResult = { + id: string; + slug: string; +}; + +export async function findRelativeValuesForSelect({ + owner, + slugContains, +}: { + owner: string; + slugContains: string; +}): Promise { + const rows = await prisma.relativeValuesDefinition.findMany({ + where: { + slug: { + contains: slugContains, + mode: "insensitive", + }, + owner: { slug: owner }, + }, + orderBy: { updatedAt: "desc" }, + take: 20, + }); + + return rows.map((row) => ({ + id: row.id, + slug: row.slug, + })); +} diff --git a/packages/hub/src/server/relative-values/data/full.ts b/packages/hub/src/server/relative-values/data/full.ts new file mode 100644 index 0000000000..eca472f4ed --- /dev/null +++ b/packages/hub/src/server/relative-values/data/full.ts @@ -0,0 +1,101 @@ +import { Prisma } from "@prisma/client"; +import { z } from "zod"; + +import { prisma } from "@/prisma"; +import { + relativeValuesClustersSchema, + relativeValuesItemsSchema, +} from "@/relative-values/types"; + +import { + definitionCardSelect, + RelativeValuesDefinitionCardDTO, + toDefinitionCardDTO, +} from "./cards"; + +const select = { + ...definitionCardSelect, + currentRevision: { + select: { + id: true, + title: true, + items: true, + clusters: true, + recommendedUnit: true, + }, + }, +} satisfies Prisma.RelativeValuesDefinitionSelect; + +type DbDefinitionFull = NonNullable< + Awaited< + ReturnType< + typeof prisma.relativeValuesDefinition.findFirst<{ + select: typeof select; + }> + > + > +>; + +type NoUndefinedInObjectList = { + [K in keyof T[number]]-?: Exclude; +}[]; + +export type RelativeValuesDefinitionFullDTO = + RelativeValuesDefinitionCardDTO & { + currentRevision: { + id: string; + title: string; + items: NoUndefinedInObjectList>; + clusters: NoUndefinedInObjectList< + z.infer + >; + recommendedUnit: string | null; + }; + }; + +function toDefinitionFullDTO( + row: DbDefinitionFull +): RelativeValuesDefinitionFullDTO { + if (!row.currentRevision) { + // should never happen + throw new Error("Current revision is required"); + } + + return { + ...toDefinitionCardDTO(row), + currentRevision: { + id: row.currentRevision.id, + title: row.currentRevision.title, + items: relativeValuesItemsSchema + .parse(row.currentRevision.items) + .map((item) => ({ + ...item, + clusterId: item.clusterId ?? null, + })), + clusters: relativeValuesClustersSchema + .parse(row.currentRevision.clusters) + .map((cluster) => ({ + ...cluster, + recommendedUnit: cluster.recommendedUnit ?? null, + })), + recommendedUnit: row.currentRevision.recommendedUnit, + }, + }; +} + +export async function loadRelativeValuesDefinitionFull(params: { + owner: string; + slug: string; +}): Promise { + const row = await prisma.relativeValuesDefinition.findFirst({ + where: { + owner: { slug: params.owner }, + slug: params.slug, + }, + select, + }); + if (!row) { + return null; + } + return toDefinitionFullDTO(row); +} diff --git a/packages/hub/src/server/relative-values/data/fullExport.ts b/packages/hub/src/server/relative-values/data/fullExport.ts new file mode 100644 index 0000000000..2276c879d8 --- /dev/null +++ b/packages/hub/src/server/relative-values/data/fullExport.ts @@ -0,0 +1,106 @@ +import { Prisma } from "@prisma/client"; + +import { prisma } from "@/prisma"; + +const select = { + id: true, + variableName: true, + modelRevision: { + select: { + model: { + select: { + slug: true, + isPrivate: true, + owner: { select: { slug: true } }, + }, + }, + }, + }, + cache: { + select: { + firstItem: true, + secondItem: true, + result: true, + error: true, + }, + }, + definition: { + select: { + owner: { select: { slug: true } }, + slug: true, + }, + }, +} satisfies Prisma.RelativeValuesExportSelect; + +export type RelativeValuesExportFullDTO = { + id: string; + variableName: string; + modelRevision: { + model: { + slug: string; + isPrivate: boolean; + owner: { slug: string }; + }; + }; + cache: { + firstItem: string; + secondItem: string; + resultJSON: string; + errorString: string | null; + }[]; + definition: { + owner: string; + slug: string; + }; +}; + +function toDTO( + row: Prisma.RelativeValuesExportGetPayload<{ + select: typeof select; + }> +): RelativeValuesExportFullDTO { + return { + id: row.id, + variableName: row.variableName, + modelRevision: { + model: { + slug: row.modelRevision.model.slug, + isPrivate: row.modelRevision.model.isPrivate, + owner: { slug: row.modelRevision.model.owner.slug }, + }, + }, + cache: row.cache.map((c) => ({ + firstItem: c.firstItem, + secondItem: c.secondItem, + resultJSON: JSON.stringify(c.result), + errorString: c.error, + })), + definition: { + owner: row.definition.owner.slug, + slug: row.definition.slug, + }, + }; +} + +export async function loadRelativeValuesExportFullFromModelRevision(params: { + modelRevisionId: string; + variableName: string; +}): Promise { + const exports = await prisma.relativeValuesExport.findMany({ + where: { + modelRevisionId: params.modelRevisionId, + variableName: params.variableName, + }, + select, + }); + + if (exports.length > 1) { + throw new Error("Ambiguous input, multiple variables match it"); + } + + if (exports.length === 0) { + return null; + } + + return toDTO(exports[0]); +} diff --git a/packages/hub/src/squiggle/components/ImportTooltip.tsx b/packages/hub/src/squiggle/components/ImportTooltip.tsx index 70b73f015c..43bb20429f 100644 --- a/packages/hub/src/squiggle/components/ImportTooltip.tsx +++ b/packages/hub/src/squiggle/components/ImportTooltip.tsx @@ -4,7 +4,7 @@ import Skeleton from "react-loading-skeleton"; import { ModelCard } from "@/models/components/ModelCard"; import { loadModelCardAction } from "@/server/models/actions/loadModelCardAction"; -import { ModelCardDTO } from "@/server/models/data/card"; +import { ModelCardDTO } from "@/server/models/data/cards"; import { parseSourceId } from "./linker"; From b8db470958825a07f3b8ccebd4f3c493d27641e3 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 29 Nov 2024 17:23:54 -0300 Subject: [PATCH 43/68] user layout --- packages/hub/esbuild.cjs | 2 +- packages/hub/package.json | 3 +- .../app/users/[username]/NewModelButton.tsx | 33 ++++ .../src/app/users/[username]/UserLayout.tsx | 142 ------------------ .../hub/src/app/users/[username]/layout.tsx | 71 +++++++-- .../hub/src/server/users/data/layoutUser.ts | 99 ++++++++++++ pnpm-lock.yaml | 86 +---------- 7 files changed, 200 insertions(+), 236 deletions(-) create mode 100644 packages/hub/src/app/users/[username]/NewModelButton.tsx delete mode 100644 packages/hub/src/app/users/[username]/UserLayout.tsx create mode 100644 packages/hub/src/server/users/data/layoutUser.ts diff --git a/packages/hub/esbuild.cjs b/packages/hub/esbuild.cjs index 457ee46e08..f9cf0be4cc 100644 --- a/packages/hub/esbuild.cjs +++ b/packages/hub/esbuild.cjs @@ -39,6 +39,6 @@ await (async () => { `, }, outfile: `./dist/scripts/${name}.mjs`, - external: ["server-only"], + external: ["server-only", "@opentelemetry/api"], }); } diff --git a/packages/hub/package.json b/packages/hub/package.json index b8a03074fd..1459fbcc1a 100644 --- a/packages/hub/package.json +++ b/packages/hub/package.json @@ -23,7 +23,7 @@ "lint": "prettier --check . && next lint", "format": "prettier --write .", "test:manual": "dotenv -e .env.test -- jest -i", - "build-last-revision": "pnpm build:esbuild && node ./dist/scripts/buildRecentModelRevision/main.mjs" + "build-last-revision": "pnpm build:esbuild && NODE_OPTIONS=--conditions=react-server node ./dist/scripts/buildRecentModelRevision/main.mjs" }, "dependencies": { "@auth/prisma-adapter": "^2.7.4", @@ -66,6 +66,7 @@ "relay-runtime": "^16.2.0", "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.0", + "server-only": "^0.0.1", "zod": "^3.23.8" }, "devDependencies": { diff --git a/packages/hub/src/app/users/[username]/NewModelButton.tsx b/packages/hub/src/app/users/[username]/NewModelButton.tsx new file mode 100644 index 0000000000..be7f2d3f5e --- /dev/null +++ b/packages/hub/src/app/users/[username]/NewModelButton.tsx @@ -0,0 +1,33 @@ +"use client"; +import { useRouter, useSelectedLayoutSegment } from "next/navigation"; +import { FC } from "react"; + +import { Button, PlusIcon } from "@quri/ui"; + +import { newDefinitionRoute, newGroupRoute, newModelRoute } from "@/routes"; + +export const NewModelButton: FC = () => { + const segment = useSelectedLayoutSegment(); + + let link = newModelRoute(); + let text = "New Model"; + + if (segment === "groups") { + link = newGroupRoute(); + text = "New Group"; + } else if (segment === "definitions") { + link = newDefinitionRoute(); + text = "New Definition"; + } + + const router = useRouter(); + + return ( + + ); +}; diff --git a/packages/hub/src/app/users/[username]/UserLayout.tsx b/packages/hub/src/app/users/[username]/UserLayout.tsx deleted file mode 100644 index ccadad9ff2..0000000000 --- a/packages/hub/src/app/users/[username]/UserLayout.tsx +++ /dev/null @@ -1,142 +0,0 @@ -"use client"; -import { useRouter, useSelectedLayoutSegment } from "next/navigation"; -import { FC, PropsWithChildren } from "react"; -import { graphql } from "relay-runtime"; - -import { Button, PlusIcon, UserIcon } from "@quri/ui"; - -import { H1 } from "@/components/ui/Headers"; -import { StyledTabLink } from "@/components/ui/StyledTabLink"; -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; -import { - newDefinitionRoute, - newGroupRoute, - newModelRoute, - userDefinitionsRoute, - userGroupsRoute, - userRoute, - userVariablesRoute, -} from "@/routes"; - -import { UserLayoutQuery } from "@/__generated__/UserLayoutQuery.graphql"; - -const Query = graphql` - query UserLayoutQuery($username: String!) { - userByUsername(username: $username) { - __typename - ... on BaseError { - message - } - ... on NotFoundError { - message - } - ... on User { - username - isMe - # fields for count (empty/non-empty) - # TODO: implement "totalCount" field instead - models(first: 1) { - edges { - __typename - } - } - variables(first: 1) { - edges { - __typename - } - } - relativeValuesDefinitions(first: 1) { - edges { - __typename - } - } - groups(first: 1) { - edges { - __typename - } - } - } - } - } -`; - -const NewButton: FC = () => { - const segment = useSelectedLayoutSegment(); - - let link = newModelRoute(); - let text = "New Model"; - - if (segment === "groups") { - link = newGroupRoute(); - text = "New Group"; - } else if (segment === "definitions") { - link = newDefinitionRoute(); - text = "New Definition"; - } - - const router = useRouter(); - - return ( - - ); -}; - -export const UserLayout: FC< - PropsWithChildren<{ - query: SerializablePreloadedQuery; - }> -> = ({ query, children }) => { - const [{ userByUsername: result }] = usePageQuery(Query, query); - - const user = extractFromGraphqlErrorUnion(result, "User"); - - const isMe = user.isMe; - - return ( -
-

-
- - {user.username} -
-

-
- - {isMe || user.models.edges.length ? ( - - ) : null} - {isMe || user.variables.edges.length ? ( - - ) : null} - {isMe || user.relativeValuesDefinitions.edges.length ? ( - - ) : null} - {isMe || user.groups.edges.length ? ( - - ) : null} - - {isMe && } -
-
{children}
-
- ); -}; diff --git a/packages/hub/src/app/users/[username]/layout.tsx b/packages/hub/src/app/users/[username]/layout.tsx index 657eb9dbf4..bd9e1e7147 100644 --- a/packages/hub/src/app/users/[username]/layout.tsx +++ b/packages/hub/src/app/users/[username]/layout.tsx @@ -1,14 +1,25 @@ import { Metadata } from "next"; +import { notFound } from "next/navigation"; import { PropsWithChildren } from "react"; -import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { UserIcon } from "@quri/ui"; -import { UserLayout } from "./UserLayout"; +import { auth } from "@/auth"; +import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; +import { H1 } from "@/components/ui/Headers"; +import { + StyledTabLink, + StyledTabLinkList, +} from "@/components/ui/StyledTabLink"; +import { + userDefinitionsRoute, + userGroupsRoute, + userRoute, + userVariablesRoute, +} from "@/routes"; +import { loadLayoutUser } from "@/server/users/data/layoutUser"; -import QueryNode, { - UserLayoutQuery, -} from "@/__generated__/UserLayoutQuery.graphql"; +import { NewModelButton } from "./NewModelButton"; type Props = { params: Promise<{ username: string }>; @@ -19,13 +30,53 @@ export default async function OuterUserLayout({ children, }: PropsWithChildren) { const { username } = await params; - const query = await loadPageQuery(QueryNode, { - username, - }); + const user = await loadLayoutUser(username); + if (!user) { + notFound(); + } + const session = await auth(); + const isMe = user.id === session?.user.id; return ( - {children} +
+

+
+ + {user.username} +
+

+
+ + {isMe || user.hasModels ? ( + + ) : null} + {isMe || user.hasVariables ? ( + + ) : null} + {isMe || user.hasDefinitions ? ( + + ) : null} + {isMe || user.hasGroups ? ( + + ) : null} + + {isMe && } +
+
{children}
+
); } diff --git a/packages/hub/src/server/users/data/layoutUser.ts b/packages/hub/src/server/users/data/layoutUser.ts new file mode 100644 index 0000000000..5e86bf652e --- /dev/null +++ b/packages/hub/src/server/users/data/layoutUser.ts @@ -0,0 +1,99 @@ +import { Prisma } from "@prisma/client"; + +import { prisma } from "@/prisma"; +import { modelWhereHasAccess } from "@/server/models/data/authHelpers"; + +const getSelect = async () => + ({ + id: true, + asOwner: { + select: { + slug: true, + // count models + models: { + take: 1, + where: { OR: await modelWhereHasAccess() }, + select: { id: true }, + }, + // count definitions + relativeValuesDefinitions: { + take: 1, + select: { id: true }, + }, + }, + }, + // count groups + memberships: { + take: 1, + select: { id: true }, + }, + }) satisfies Prisma.UserSelect; + +type Row = NonNullable< + Awaited< + ReturnType< + typeof prisma.user.findFirst<{ + select: Awaited>; + }> + > + > +>; + +export type UserLayoutDTO = { + id: string; + username: string; + hasModels: boolean; + hasDefinitions: boolean; + hasGroups: boolean; + hasVariables: boolean; +}; + +function toDTO( + row: Row, + { hasVariables }: { hasVariables: boolean } +): UserLayoutDTO { + return { + id: row.id, + username: row.asOwner?.slug ?? "", + hasModels: !!row.asOwner?.models.length, + hasDefinitions: !!row.asOwner?.relativeValuesDefinitions?.length, + hasGroups: !!row.memberships.length, + hasVariables, + }; +} + +export async function loadLayoutUser( + username: string +): Promise { + const row = await prisma.user.findFirst({ + where: { + asOwner: { + slug: username, + }, + }, + select: await getSelect(), + }); + + if (!row) { + return null; + } + + const hasVariables = !!(await prisma.variable.findFirst({ + select: { id: true }, + where: { + model: { + // variables from this user's models + owner: { + slug: username, + }, + // that the viewer has access to + OR: await modelWhereHasAccess(), + }, + }, + take: 1, + })); + + return toDTO(row, { + hasVariables, + }); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1275e5f8e2..8bdf6c2a60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -529,6 +529,9 @@ importers: remark-gfm: specifier: ^4.0.0 version: 4.0.0 + server-only: + specifier: ^0.0.1 + version: 0.0.1 zod: specifier: ^3.23.8 version: 3.23.8 @@ -699,7 +702,7 @@ importers: version: 29.5.14 jest: specifier: ^29.7.0 - version: 29.7.0(babel-plugin-macros@3.1.0) + version: 29.7.0(@types/node@22.9.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.9.0)(typescript@5.6.3)) typescript: specifier: ^5.6.3 version: 5.6.3 @@ -16115,41 +16118,6 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(babel-plugin-macros@3.1.0)': - dependencies: - '@jest/console': 29.7.0 - '@jest/reporters': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.9.0 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 3.8.0 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.9.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.9.0)(typescript@5.6.3)) - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-resolve-dependencies: 29.7.0 - jest-runner: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - jest-watcher: 29.7.0 - micromatch: 4.0.5 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - ts-node - '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.6.3))': dependencies: '@jest/console': 29.7.0 @@ -20959,21 +20927,6 @@ snapshots: - supports-color - ts-node - create-jest@29.7.0(babel-plugin-macros@3.1.0): - dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.9.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.9.0)(typescript@5.6.3)) - jest-util: 29.7.0 - prompts: 2.4.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - create-require@1.1.1: {} crelt@1.0.5: {} @@ -23652,25 +23605,6 @@ snapshots: - supports-color - ts-node - jest-cli@29.7.0(babel-plugin-macros@3.1.0): - dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0) - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - chalk: 4.1.2 - create-jest: 29.7.0(babel-plugin-macros@3.1.0) - exit: 0.1.2 - import-local: 3.1.0 - jest-config: 29.7.0(@types/node@22.9.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.9.0)(typescript@5.6.3)) - jest-util: 29.7.0 - jest-validate: 29.7.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - jest-config@29.7.0(@types/node@20.12.7)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.6.3)): dependencies: '@babel/core': 7.26.0 @@ -24018,18 +23952,6 @@ snapshots: - supports-color - ts-node - jest@29.7.0(babel-plugin-macros@3.1.0): - dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0) - '@jest/types': 29.6.3 - import-local: 3.1.0 - jest-cli: 29.7.0(babel-plugin-macros@3.1.0) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - jiti@1.20.0: {} jiti@1.21.0: {} From ca1f23d3b06845ab9f654f7df86bcbdc0d61c7dc Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 29 Nov 2024 17:53:58 -0300 Subject: [PATCH 44/68] revision page --- .../upgrade-versions/UpgradeableModel.tsx | 4 - .../[slug]/EditSquiggleSnippetModel.tsx | 3 + .../[revisionId]/ModelRevisionView.tsx | 91 ++------------- .../[slug]/revisions/[revisionId]/page.tsx | 54 +++++++-- .../[owner]/[slug]/revisions/layout.tsx | 12 -- .../[owner]/[slug]/revisions/loading.tsx | 5 - .../models/[owner]/[slug]/revisions/page.tsx | 20 +++- .../hub/src/server/groups/data/members.ts | 1 + packages/hub/src/server/models/data/full.ts | 63 ++-------- .../src/server/models/data/fullRevision.ts | 109 ++++++++++++++++++ .../hub/src/server/models/data/revisions.ts | 3 +- 11 files changed, 196 insertions(+), 169 deletions(-) delete mode 100644 packages/hub/src/app/models/[owner]/[slug]/revisions/layout.tsx delete mode 100644 packages/hub/src/app/models/[owner]/[slug]/revisions/loading.tsx create mode 100644 packages/hub/src/server/models/data/fullRevision.ts diff --git a/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx b/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx index 3033bc2d17..45be5dc150 100644 --- a/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx +++ b/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx @@ -20,10 +20,6 @@ const InnerUpgradeableModel: FC<{ }> = ({ model }) => { const currentRevision = model.currentRevision; - if (currentRevision.contentType !== "SquiggleSnippet") { - throw new Error("Wrong content type"); - } - const code = currentRevision.squiggleSnippet.code; const version = useAdjustSquiggleVersion( diff --git a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx index 9cf6e21c11..41f8f56cd5 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx @@ -139,6 +139,9 @@ export const EditSquiggleSnippetModel: FC = ({ const router = useRouter(); const content = revision.squiggleSnippet; + if (!content) { + throw new Error("Unknown model type"); + } const seed = content.seed; diff --git a/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/ModelRevisionView.tsx b/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/ModelRevisionView.tsx index 898e8badb1..eb58c05166 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/ModelRevisionView.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/ModelRevisionView.tsx @@ -1,103 +1,28 @@ "use client"; -import { ModelRevisionViewQuery } from "@gen/ModelRevisionViewQuery.graphql"; -import { format } from "date-fns"; import { FC, use } from "react"; -import { graphql } from "relay-runtime"; -import { CommentIcon } from "@quri/ui"; import { useAdjustSquiggleVersion, versionedSquigglePackages, } from "@quri/versioned-squiggle-components"; -import { StyledLink } from "@/components/ui/StyledLink"; -import { commonDateFormat } from "@/lib/common"; -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; -import { modelRoute } from "@/routes"; +import { ModelRevisionFullDTO } from "@/server/models/data/fullRevision"; import { getHubLinker } from "@/squiggle/components/linker"; -const Query = graphql` - query ModelRevisionViewQuery($input: QueryModelInput!, $revisionId: ID!) { - result: model(input: $input) { - __typename - ... on BaseError { - message - } - ... on NotFoundError { - message - } - ... on Model { - id - slug - owner { - slug - } - revision(id: $revisionId) { - comment - createdAtTimestamp - content { - __typename - ... on SquiggleSnippet { - code - version - } - } - } - } - } - } -`; - export const ModelRevisionView: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const [{ result }] = usePageQuery(Query, query); - const model = extractFromGraphqlErrorUnion(result, "Model"); - - const modelUrl = modelRoute({ - owner: model.owner.slug, - slug: model.slug, - }); - - const typename = model.revision.content.__typename; - if (typename !== "SquiggleSnippet") { - throw new Error(`Unknown model type ${typename}`); - } - + revision: ModelRevisionFullDTO; +}> = ({ revision }) => { const checkedVersion = useAdjustSquiggleVersion( - model.revision.content.version + revision.squiggleSnippet.version ); const squiggle = use(versionedSquigglePackages(checkedVersion)); return ( -
-
-
-
- Version from{" "} - {format(model.revision.createdAtTimestamp, commonDateFormat)}.{" "} - Squiggle{" "} - {model.revision.content.version}. -
-
- Go to latest version -
- {model.revision.comment ? ( -
- -
{model.revision.comment}
-
- ) : null} -
-
- -
+ ); }; diff --git a/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/page.tsx b/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/page.tsx index 439962781b..c21ac83eb3 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/page.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/page.tsx @@ -1,10 +1,14 @@ -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { format } from "date-fns"; +import { notFound } from "next/navigation"; -import { ModelRevisionView } from "./ModelRevisionView"; +import { CommentIcon } from "@quri/ui"; + +import { StyledLink } from "@/components/ui/StyledLink"; +import { commonDateFormat } from "@/lib/common"; +import { modelRoute } from "@/routes"; +import { loadModelRevisionFull } from "@/server/models/data/fullRevision"; -import QueryNode, { - ModelRevisionViewQuery, -} from "@/__generated__/ModelRevisionViewQuery.graphql"; +import { ModelRevisionView } from "./ModelRevisionView"; export default async function ModelPage({ params, @@ -12,10 +16,44 @@ export default async function ModelPage({ params: Promise<{ owner: string; slug: string; revisionId: string }>; }) { const { owner, slug, revisionId } = await params; - const query = await loadPageQuery(QueryNode, { - input: { owner, slug }, + + const revision = await loadModelRevisionFull({ + owner, + slug, revisionId, }); - return ; + if (!revision) { + notFound(); + } + + const modelUrl = modelRoute({ + owner, + slug, + }); + + return ( +
+
+
+
+ Version from{" "} + {format(revision.createdAt, commonDateFormat)}.{" "} + Squiggle{" "} + {revision.squiggleSnippet.version}. +
+
+ Go to latest version +
+ {revision.comment ? ( +
+ +
{revision.comment}
+
+ ) : null} +
+
+ +
+ ); } diff --git a/packages/hub/src/app/models/[owner]/[slug]/revisions/layout.tsx b/packages/hub/src/app/models/[owner]/[slug]/revisions/layout.tsx deleted file mode 100644 index b13f47b14b..0000000000 --- a/packages/hub/src/app/models/[owner]/[slug]/revisions/layout.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { PropsWithChildren } from "react"; - -import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; - -export default function ModelRevisionsLayout({ children }: PropsWithChildren) { - return ( - -
Revision history
- {children} -
- ); -} diff --git a/packages/hub/src/app/models/[owner]/[slug]/revisions/loading.tsx b/packages/hub/src/app/models/[owner]/[slug]/revisions/loading.tsx deleted file mode 100644 index 702e460f6c..0000000000 --- a/packages/hub/src/app/models/[owner]/[slug]/revisions/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import Skeleton from "react-loading-skeleton"; - -export default function Loading() { - return ; -} diff --git a/packages/hub/src/app/models/[owner]/[slug]/revisions/page.tsx b/packages/hub/src/app/models/[owner]/[slug]/revisions/page.tsx index f4e688cfdf..7d993c403e 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/revisions/page.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/revisions/page.tsx @@ -1,11 +1,14 @@ import { notFound } from "next/navigation"; +import { Suspense } from "react"; +import Skeleton from "react-loading-skeleton"; +import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; import { loadModelCard } from "@/server/models/data/cards"; import { loadModelRevisions } from "@/server/models/data/revisions"; import { ModelRevisionsList } from "./ModelRevisionsList"; -export default async function ModelPage({ +async function InnerRevisionsPage({ params, }: { params: Promise<{ owner: string; slug: string }>; @@ -19,3 +22,18 @@ export default async function ModelPage({ return ; } + +export default async function ModelPage({ + params, +}: { + params: Promise<{ owner: string; slug: string }>; +}) { + return ( + +
Revision history
+ }> + + +
+ ); +} diff --git a/packages/hub/src/server/groups/data/members.ts b/packages/hub/src/server/groups/data/members.ts index 488fd3b7cd..afaa73b745 100644 --- a/packages/hub/src/server/groups/data/members.ts +++ b/packages/hub/src/server/groups/data/members.ts @@ -64,6 +64,7 @@ export async function loadGroupMembers(params: { }, }, select, + orderBy: { createdAt: "asc" }, take: limit + 1, }); diff --git a/packages/hub/src/server/models/data/full.ts b/packages/hub/src/server/models/data/full.ts index 7120adb520..1ce89c96f5 100644 --- a/packages/hub/src/server/models/data/full.ts +++ b/packages/hub/src/server/models/data/full.ts @@ -6,6 +6,11 @@ import { prisma } from "@/prisma"; import { controlsOwnerId } from "@/server/owners/auth"; import { modelWhereHasAccess } from "./authHelpers"; +import { + ModelRevisionFullDTO, + modelRevisionFullToDTO, + selectModelRevisionFull, +} from "./fullRevision"; const select = { id: true, @@ -17,35 +22,7 @@ const select = { }, }, currentRevision: { - select: { - contentType: true, - squiggleSnippet: { - select: { - id: true, - code: true, - version: true, - seed: true, - autorunMode: true, - sampleCount: true, - xyPointLength: true, - }, - }, - relativeValuesExports: { - select: { - variableName: true, - definition: { - select: { - slug: true, - owner: { - select: { - slug: true, - }, - }, - }, - }, - }, - }, - }, + select: selectModelRevisionFull, }, } satisfies Prisma.ModelSelect; @@ -60,27 +37,7 @@ export type ModelFullDTO = { id: string; slug: string; }; - currentRevision: { - contentType: "SquiggleSnippet"; - squiggleSnippet: { - id: string; - code: string; - version: string; - seed: string; - autorunMode: boolean | null; - sampleCount: number | null; - xyPointLength: number | null; - }; - relativeValuesExports: { - variableName: string; - definition: { - slug: string; - owner: { - slug: string; - }; - }; - }[]; - }; + currentRevision: ModelRevisionFullDTO; isEditable: boolean; lastBuildSeconds: number | null; }; @@ -130,11 +87,7 @@ async function toDTO(row: Row): Promise { id: row.owner.id, slug: row.owner.slug, }, - currentRevision: { - contentType: row.currentRevision.contentType, - squiggleSnippet: row.currentRevision.squiggleSnippet, - relativeValuesExports: row.currentRevision.relativeValuesExports, - }, + currentRevision: await modelRevisionFullToDTO(row.currentRevision), isEditable: await controlsOwnerId(row.owner.id), lastBuildSeconds, }; diff --git a/packages/hub/src/server/models/data/fullRevision.ts b/packages/hub/src/server/models/data/fullRevision.ts new file mode 100644 index 0000000000..e0ed8504a2 --- /dev/null +++ b/packages/hub/src/server/models/data/fullRevision.ts @@ -0,0 +1,109 @@ +import { Prisma } from "@prisma/client"; + +import { prisma } from "@/prisma"; + +import { modelWhereHasAccess } from "./authHelpers"; + +export const selectModelRevisionFull = { + id: true, + createdAt: true, + comment: true, + contentType: true, + squiggleSnippet: { + select: { + id: true, + code: true, + version: true, + seed: true, + autorunMode: true, + sampleCount: true, + xyPointLength: true, + }, + }, + relativeValuesExports: { + select: { + variableName: true, + definition: { + select: { + slug: true, + owner: { + select: { + slug: true, + }, + }, + }, + }, + }, + }, +} satisfies Prisma.ModelRevisionSelect; + +type Row = NonNullable< + Awaited< + ReturnType< + typeof prisma.modelRevision.findFirst<{ + select: typeof selectModelRevisionFull; + }> + > + > +>; + +export type ModelRevisionFullDTO = { + id: string; + createdAt: Date; + comment: string; + contentType: "SquiggleSnippet"; + squiggleSnippet: { + id: string; + code: string; + version: string; + seed: string; + autorunMode: boolean | null; + sampleCount: number | null; + xyPointLength: number | null; + }; + relativeValuesExports: { + variableName: string; + definition: { + slug: string; + owner: { slug: string }; + }; + }[]; +}; + +export async function modelRevisionFullToDTO( + row: Row +): Promise { + // FIXME - validate + if (!row.squiggleSnippet) { + throw new Error("Unknown model type"); + } + return row as typeof row & { squiggleSnippet: typeof row.squiggleSnippet }; +} + +export async function loadModelRevisionFull({ + owner, + slug, + revisionId, +}: { + owner: string; + slug: string; + revisionId: string; +}): Promise { + const dbRevision = await prisma.modelRevision.findFirst({ + where: { + id: revisionId, + model: { + slug, + owner: { slug: owner }, + OR: await modelWhereHasAccess(), + }, + }, + select: selectModelRevisionFull, + }); + + if (!dbRevision) { + return null; + } + + return modelRevisionFullToDTO(dbRevision); +} diff --git a/packages/hub/src/server/models/data/revisions.ts b/packages/hub/src/server/models/data/revisions.ts index e471705b82..1db49ed1f9 100644 --- a/packages/hub/src/server/models/data/revisions.ts +++ b/packages/hub/src/server/models/data/revisions.ts @@ -101,7 +101,6 @@ export async function loadModelRevisions(params: { const limit = params.limit ?? 20; const dbRevisions = await prisma.modelRevision.findMany({ - select, where: { model: { slug: params.slug, @@ -110,6 +109,8 @@ export async function loadModelRevisions(params: { }, }, cursor: params.cursor ? { id: params.cursor } : undefined, + orderBy: { createdAt: "desc" }, + select, take: limit + 1, }); From d8f83f83cb09f19bf8056067789cd6e4cea0cfce Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 29 Nov 2024 18:22:33 -0300 Subject: [PATCH 45/68] migrate model saves --- .../[slug]/EditSquiggleSnippetModel.tsx | 77 +++----- .../graphql/mutations/cancelGroupInvite.ts | 45 ----- .../mutations/updateSquiggleSnippetModel.ts | 187 ------------------ packages/hub/src/graphql/schema.ts | 3 +- packages/hub/src/hooks/useServerActionForm.ts | 10 +- .../updateSquiggleSnippetModelAction.ts | 158 +++++++++++++++ 6 files changed, 197 insertions(+), 283 deletions(-) delete mode 100644 packages/hub/src/graphql/mutations/cancelGroupInvite.ts delete mode 100644 packages/hub/src/graphql/mutations/updateSquiggleSnippetModel.ts create mode 100644 packages/hub/src/server/models/actions/updateSquiggleSnippetModelAction.ts diff --git a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx index 41f8f56cd5..3943d74629 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx @@ -9,7 +9,6 @@ import { useState, } from "react"; import { FormProvider, useFieldArray, useForm } from "react-hook-form"; -import { graphql } from "react-relay"; import { ButtonWithDropdown, @@ -21,6 +20,7 @@ import { LinkIcon, TextAreaFormField, TextTooltip, + useToast, } from "@quri/ui"; import { checkSquiggleVersion, @@ -40,8 +40,9 @@ import { ReactRoot } from "@/components/ReactRoot"; import { FormModal } from "@/components/ui/FormModal"; import { SAMPLE_COUNT_DEFAULT, XY_POINT_LENGTH_DEFAULT } from "@/constants"; import { useAvailableHeight } from "@/hooks/useAvailableHeight"; -import { useMutationForm } from "@/hooks/useMutationForm"; +import { useServerActionForm } from "@/hooks/useServerActionForm"; import { modelRoute, variableRoute } from "@/routes"; +import { updateSquiggleSnippetModelAction } from "@/server/models/actions/updateSquiggleSnippetModelAction"; import { ModelFullDTO } from "@/server/models/data/full"; import { ImportTooltip } from "@/squiggle/components/ImportTooltip"; import { @@ -57,14 +58,15 @@ import { useDraftLocator, } from "./SquiggleSnippetDraftDialog"; -import { - EditSquiggleSnippetModelMutation, - RelativeValuesExportInput, -} from "@/__generated__/EditSquiggleSnippetModelMutation.graphql"; - export type SquiggleSnippetFormShape = { code: string; - relativeValuesExports: RelativeValuesExportInput[]; + relativeValuesExports: { + variableName: string; + definition: { + owner: string; + slug: string; + }; + }[]; }; type OnSubmit = ( @@ -143,6 +145,8 @@ export const EditSquiggleSnippetModel: FC = ({ throw new Error("Unknown model type"); } + const toast = useToast(); + const seed = content.seed; const initialFormValues: SquiggleSnippetFormShape = useMemo(() => { @@ -158,51 +162,32 @@ export const EditSquiggleSnippetModel: FC = ({ }; }, [content, revision.relativeValuesExports]); - const { form, onSubmit, inFlight } = useMutationForm< + const { form, onSubmit, inFlight } = useServerActionForm< SquiggleSnippetFormShape, - EditSquiggleSnippetModelMutation, - "UpdateSquiggleSnippetResult", + typeof updateSquiggleSnippetModelAction, { comment: string } >({ defaultValues: initialFormValues, - mutation: graphql` - mutation EditSquiggleSnippetModelMutation( - $input: MutationUpdateSquiggleSnippetModelInput! - ) { - result: updateSquiggleSnippetModel(input: $input) { - __typename - ... on BaseError { - message - } - ... on UpdateSquiggleSnippetResult { - model { - ...EditSquiggleSnippetModel - } - } - } - } - `, - expectedTypename: "UpdateSquiggleSnippetResult", + action: async (variables) => { + const result = await updateSquiggleSnippetModelAction(variables); + toast("Saved", "confirmation"); + draftUtils.discard(draftLocator); + return result; + }, formDataToVariables: (formData, extraData) => ({ - input: { - content: { - code: formData.code, - version, - seed: seed, - autorunMode: content.autorunMode, - sampleCount: content.sampleCount, - xyPointLength: content.xyPointLength, - }, - relativeValuesExports: formData.relativeValuesExports, - comment: extraData?.comment, - slug: model.slug, - owner: model.owner.slug, + content: { + code: formData.code, + version, + seed: seed, + autorunMode: content.autorunMode, + sampleCount: content.sampleCount, + xyPointLength: content.xyPointLength, }, + relativeValuesExports: formData.relativeValuesExports, + comment: extraData?.comment, + slug: model.slug, + owner: model.owner.slug, }), - confirmation: "Saved", - onCompleted() { - draftUtils.discard(draftLocator); - }, }); // could version picker be part of the form? diff --git a/packages/hub/src/graphql/mutations/cancelGroupInvite.ts b/packages/hub/src/graphql/mutations/cancelGroupInvite.ts deleted file mode 100644 index 36839259fc..0000000000 --- a/packages/hub/src/graphql/mutations/cancelGroupInvite.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { prisma } from "@/prisma"; - -import { builder } from "../builder"; -import { GroupInvite } from "../types/GroupInvite"; -import { decodeGlobalIdWithTypename } from "../utils"; - -builder.mutationField("cancelGroupInvite", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("CancelGroupInviteResult", { - fields: (t) => ({ - invite: t.field({ type: GroupInvite }), - }), - }), - errors: {}, - input: { - inviteId: t.input.string({ required: true }), - }, - resolve: async (_, { input }, { session }) => { - const decodedInviteId = decodeGlobalIdWithTypename(input.inviteId, [ - "UserGroupInvite", - "EmailGroupInvite", - ]); - - const invite = await prisma.groupInvite.update({ - where: { - id: decodedInviteId, - group: { - // permissions check - memberships: { - some: { - user: { email: session.user.email }, - role: "Admin", - }, - }, - }, - }, - data: { - status: "Canceled", - }, - }); - - return { invite }; - }, - }) -); diff --git a/packages/hub/src/graphql/mutations/updateSquiggleSnippetModel.ts b/packages/hub/src/graphql/mutations/updateSquiggleSnippetModel.ts deleted file mode 100644 index 20217210ea..0000000000 --- a/packages/hub/src/graphql/mutations/updateSquiggleSnippetModel.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { RelativeValuesDefinition } from "@prisma/client"; -import { revalidatePath } from "next/cache"; - -import { squiggleVersions } from "@quri/versioned-squiggle-components"; - -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; -import { modelRoute } from "@/routes"; - -import { getWriteableModel } from "../helpers/modelHelpers"; -import { getSelf } from "../helpers/userHelpers"; -import { Model } from "../types/Model"; - -const DefinitionRefInput = builder.inputType("DefinitionRefInput", { - fields: (t) => ({ - owner: t.string({ required: true }), - slug: t.string({ required: true }), - }), -}); - -const RelativeValuesExportInput = builder.inputType( - "RelativeValuesExportInput", - { - fields: (t) => ({ - variableName: t.string({ required: true }), - definition: t.field({ - type: DefinitionRefInput, - required: true, - }), - }), - } -); - -const SquiggleSnippetContentInput = builder.inputType( - "SquiggleSnippetContentInput", - { - fields: (t) => ({ - code: t.string({ required: true }), - version: t.string({ required: true }), - seed: t.string({ required: true }), - autorunMode: t.boolean({ required: false }), - sampleCount: t.int({ required: false }), - xyPointLength: t.int({ required: false }), - }), - } -); - -builder.mutationField("updateSquiggleSnippetModel", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - type: builder.simpleObject("UpdateSquiggleSnippetResult", { - fields: (t) => ({ - model: t.field({ type: Model }), - }), - }), - errors: {}, - input: { - owner: t.input.string({ required: true }), - slug: t.input.string({ required: true }), - relativeValuesExports: t.input.field({ - type: [RelativeValuesExportInput], - }), - content: t.input.field({ - type: SquiggleSnippetContentInput, - required: true, - }), - comment: t.input.string(), - }, - resolve: async (_, { input }, { session }) => { - const existingModel = await getWriteableModel({ - slug: input.slug, - session, - owner: input.owner, - }); - - const version = input.content.version; - if (!(squiggleVersions as readonly string[]).includes(version)) { - throw new Error(`Unknown Squiggle version ${version}`); - } - - const relativeValuesExports = input.relativeValuesExports ?? []; - - const relativeValuesExportsToInsert: { - definitionId: string; - variableName: string; - }[] = []; - - if (relativeValuesExports.length) { - const selectedDefinitions = - await prisma.relativeValuesDefinition.findMany({ - where: { - OR: relativeValuesExports.map((pair) => ({ - slug: pair.definition.slug, - owner: { slug: pair.definition.owner }, - })), - }, - include: { owner: true }, - }); - - // username -> slug -> Definition - let linkedDefinitions: Map< - string, - Map - > = new Map(); - // now we need to match relativeValuesExports with definitions to get ids; I wonder if this could be simplified without sacrificing safety - for (const definition of selectedDefinitions) { - const { slug: ownerSlug } = definition.owner; - if (ownerSlug === null) { - continue; // should never happen - } - if (!linkedDefinitions.has(ownerSlug)) { - linkedDefinitions.set(ownerSlug, new Map()); - } - linkedDefinitions.get(ownerSlug)?.set(definition.slug, definition); - } - for (const pair of relativeValuesExports) { - const definition = linkedDefinitions - .get(pair.definition.owner) - ?.get(pair.definition.slug); - - if (!definition) { - throw new Error( - `Definition with owner ${pair.definition.owner}, slug ${pair.definition.slug} not found` - ); - } - relativeValuesExportsToInsert.push({ - variableName: pair.variableName, - definitionId: definition.id, - }); - } - } - - const self = await getSelf(session); - - const model = await prisma.$transaction(async (tx) => { - const revision = await tx.modelRevision.create({ - data: { - squiggleSnippet: { - create: { - code: input.content.code, - version: input.content.version, - seed: input.content.seed, - autorunMode: input.content.autorunMode ?? null, - sampleCount: input.content.sampleCount ?? null, - xyPointLength: input.content.xyPointLength ?? null, - }, - }, - contentType: "SquiggleSnippet", - comment: input.comment ?? "", - model: { - connect: { id: existingModel.id }, - }, - author: { - connect: { id: self.id }, - }, - relativeValuesExports: { - createMany: { - data: relativeValuesExportsToInsert, - }, - }, - }, - include: { - model: { - select: { - id: true, - }, - }, - }, - }); - const updatedModel = await tx.model.update({ - where: { - id: revision.modelId, - }, - data: { - currentRevisionId: revision.id, - }, - // TODO - optimize with queryFromInfo, https://pothos-graphql.dev/docs/plugins/prisma#optimized-queries-without-tprismafield - }); - - return updatedModel; - }); - - revalidatePath(modelRoute({ owner: input.owner, slug: input.slug })); - - return { model }; - }, - }) -); diff --git a/packages/hub/src/graphql/schema.ts b/packages/hub/src/graphql/schema.ts index fc5d361341..0c04f4d3d7 100644 --- a/packages/hub/src/graphql/schema.ts +++ b/packages/hub/src/graphql/schema.ts @@ -10,9 +10,8 @@ import "./queries/userByUsername"; import "./mutations/adminUpdateModelVersion"; import "../server/search/actions/adminRebuildSearchIndexAction"; import "../server/relative-values/actions/buildRelativeValuesCacheAction"; -import "./mutations/cancelGroupInvite"; import "../server/relative-values/actions/clearRelativeValuesCacheAction"; -import "./mutations/updateSquiggleSnippetModel"; +import "../server/models/actions/updateSquiggleSnippetModelAction"; import { builder } from "./builder"; diff --git a/packages/hub/src/hooks/useServerActionForm.ts b/packages/hub/src/hooks/useServerActionForm.ts index b244014545..5383b906d4 100644 --- a/packages/hub/src/hooks/useServerActionForm.ts +++ b/packages/hub/src/hooks/useServerActionForm.ts @@ -14,6 +14,7 @@ import { useToast } from "@quri/ui"; export function useServerActionForm< FormShape extends FieldValues = never, const Action extends (input: any) => Promise = never, + ExtraData extends Record = Record, ActionVariables = Parameters[0], ActionResult = Awaited>, >({ @@ -28,7 +29,10 @@ export function useServerActionForm< // See also: https://stackoverflow.com/questions/72111571/typescript-exact-return-type-of-function // This could be solved by converting the return type to generic, but I expect that the lack of partial type parameters in TypeScript // would get in the way, so I won't even try. - formDataToVariables: (data: FormShape) => ActionVariables; + formDataToVariables: ( + data: FormShape, + extraData?: ExtraData + ) => ActionVariables; action: (input: ActionVariables) => Promise; onCompleted?: (result: ActionResult) => void | Promise; blockOnSuccess?: boolean; @@ -38,11 +42,11 @@ export function useServerActionForm< const toast = useToast(); const onSubmit = useCallback( - (event?: BaseSyntheticEvent) => + (event?: BaseSyntheticEvent, extraData?: ExtraData) => form.handleSubmit(async (formData) => { // TODO - transition? try { - const result = await action(formDataToVariables(formData)); + const result = await action(formDataToVariables(formData, extraData)); onCompleted?.(result); } catch (error) { toast(String(error), "error"); diff --git a/packages/hub/src/server/models/actions/updateSquiggleSnippetModelAction.ts b/packages/hub/src/server/models/actions/updateSquiggleSnippetModelAction.ts new file mode 100644 index 0000000000..e1292672da --- /dev/null +++ b/packages/hub/src/server/models/actions/updateSquiggleSnippetModelAction.ts @@ -0,0 +1,158 @@ +"use server"; +import { RelativeValuesDefinition } from "@prisma/client"; +import { revalidatePath } from "next/cache"; +import { z } from "zod"; + +import { squiggleVersions } from "@quri/versioned-squiggle-components"; + +import { prisma } from "@/prisma"; +import { modelRoute } from "@/routes"; +import { getSessionOrRedirect } from "@/server/users/auth"; +import { makeServerAction, zSlug } from "@/server/utils"; + +import { getWriteableModel } from "../../../graphql/helpers/modelHelpers"; +import { getSelf } from "../../../graphql/helpers/userHelpers"; + +export const updateSquiggleSnippetModelAction = makeServerAction( + z.object({ + owner: zSlug, + slug: zSlug, + relativeValuesExports: z.array( + z.object({ + variableName: z.string(), + definition: z.object({ + owner: zSlug, + slug: zSlug, + }), + }) + ), + content: z.object({ + code: z.string(), + version: z.string(), + seed: z.string(), + autorunMode: z.boolean().nullable(), + sampleCount: z.number().nullable(), + xyPointLength: z.number().nullable(), + }), + comment: z.string().optional(), + }), + async (input) => { + const session = await getSessionOrRedirect(); + const existingModel = await getWriteableModel({ + slug: input.slug, + session, + owner: input.owner, + }); + + const version = input.content.version; + if (!(squiggleVersions as readonly string[]).includes(version)) { + throw new Error(`Unknown Squiggle version ${version}`); + } + + const relativeValuesExports = input.relativeValuesExports ?? []; + + const relativeValuesExportsToInsert: { + definitionId: string; + variableName: string; + }[] = []; + + if (relativeValuesExports.length) { + const selectedDefinitions = + await prisma.relativeValuesDefinition.findMany({ + where: { + OR: relativeValuesExports.map((pair) => ({ + slug: pair.definition.slug, + owner: { slug: pair.definition.owner }, + })), + }, + include: { owner: true }, + }); + + // username -> slug -> Definition + let linkedDefinitions: Map< + string, + Map + > = new Map(); + // now we need to match relativeValuesExports with definitions to get ids; I wonder if this could be simplified without sacrificing safety + for (const definition of selectedDefinitions) { + const { slug: ownerSlug } = definition.owner; + if (ownerSlug === null) { + continue; // should never happen + } + if (!linkedDefinitions.has(ownerSlug)) { + linkedDefinitions.set(ownerSlug, new Map()); + } + linkedDefinitions.get(ownerSlug)?.set(definition.slug, definition); + } + for (const pair of relativeValuesExports) { + const definition = linkedDefinitions + .get(pair.definition.owner) + ?.get(pair.definition.slug); + + if (!definition) { + throw new Error( + `Definition with owner ${pair.definition.owner}, slug ${pair.definition.slug} not found` + ); + } + relativeValuesExportsToInsert.push({ + variableName: pair.variableName, + definitionId: definition.id, + }); + } + } + + const self = await getSelf(session); + + const model = await prisma.$transaction(async (tx) => { + const revision = await tx.modelRevision.create({ + data: { + squiggleSnippet: { + create: { + code: input.content.code, + version: input.content.version, + seed: input.content.seed, + autorunMode: input.content.autorunMode, + sampleCount: input.content.sampleCount, + xyPointLength: input.content.xyPointLength, + }, + }, + contentType: "SquiggleSnippet", + comment: input.comment ?? "", + model: { + connect: { id: existingModel.id }, + }, + author: { + connect: { id: self.id }, + }, + relativeValuesExports: { + createMany: { + data: relativeValuesExportsToInsert, + }, + }, + }, + include: { + model: { + select: { + id: true, + }, + }, + }, + }); + const updatedModel = await tx.model.update({ + where: { + id: revision.modelId, + }, + data: { + currentRevisionId: revision.id, + }, + // TODO - optimize with queryFromInfo, https://pothos-graphql.dev/docs/plugins/prisma#optimized-queries-without-tprismafield + }); + + return updatedModel; + }); + + revalidatePath(modelRoute({ owner: input.owner, slug: input.slug })); + + return { model }; + } +); From d2ab158bfd25f46d8779e87a0d054b6144bb0508 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 29 Nov 2024 20:40:20 -0300 Subject: [PATCH 46/68] remove graphql entirely --- packages/hub/README.md | 12 - packages/hub/codegen.ts | 14 - packages/hub/docs/relay-pages.md | 72 - packages/hub/graphql.config.yaml | 5 - packages/hub/next.config.mjs | 9 - packages/hub/package.json | 24 +- packages/hub/prisma/schema.prisma | 4 - packages/hub/relay.config.js | 9 - packages/hub/schema.graphql | 781 ------ packages/hub/src/__generated__/.gitkeep | 0 .../src/app/(frontpage)/variables/page.tsx | 2 +- packages/hub/src/app/about/page.tsx | 5 - packages/hub/src/app/admin/layout.tsx | 2 +- .../upgrade-versions/UpgradeVersionsPage.tsx | 39 +- packages/hub/src/app/ai/api/create/route.ts | 2 +- packages/hub/src/app/api/get-source/route.ts | 27 + packages/hub/src/app/api/graphql/route.ts | 31 - packages/hub/src/app/groups/[slug]/hooks.ts | 18 - .../[slug]/members/MembershipRoleButton.tsx | 3 +- .../[slug]/EditSquiggleSnippetModel.tsx | 3 + .../[variableName]/RelativeValuesExport.ts | 25 - .../variables/[variableName]/VariablePage.tsx | 211 -- .../[variableName]/VariableRevisionsPanel.tsx | 87 + .../variables/[variableName]/layout.tsx | 33 + .../[slug]/variables/[variableName]/page.tsx | 38 +- .../[revisionId]/VariableRevisionPage.tsx} | 26 +- .../revisions/[revisionId]/page.tsx | 31 + .../app/users/[username]/variables/page.tsx | 2 +- packages/hub/src/components/OwnerLink.tsx | 30 - packages/hub/src/components/ReactRoot.tsx | 10 +- .../hub/src/components/WithAuth/index.tsx | 2 +- .../exports/EditRelativeValueExports.tsx | 3 +- .../exports/RelativeValuesExportItem.tsx | 60 - .../layout/RootLayout/PageFooter.tsx | 12 +- .../hub/src/components/ui/MutationAction.tsx | 71 - .../hub/src/components/ui/MutationButton.tsx | 63 - .../src/components/ui/MutationModalAction.tsx | 108 - packages/hub/src/graphql/builder.ts | 132 - packages/hub/src/graphql/errors/BaseError.ts | 10 - .../hub/src/graphql/errors/NotFoundError.ts | 14 - .../hub/src/graphql/errors/ValidationError.ts | 34 - packages/hub/src/graphql/errors/common.ts | 9 - .../hub/src/graphql/helpers/groupHelpers.ts | 93 - .../hub/src/graphql/helpers/modelHelpers.ts | 35 +- .../hub/src/graphql/helpers/ownerHelpers.ts | 68 - .../hub/src/graphql/helpers/userHelpers.ts | 30 - .../mutations/adminUpdateModelVersion.ts | 102 - packages/hub/src/graphql/queries/group.ts | 30 - packages/hub/src/graphql/queries/model.ts | 33 - packages/hub/src/graphql/queries/models.ts | 24 - .../queries/relativeValuesDefinition.ts | 30 - .../queries/relativeValuesDefinitions.ts | 46 - .../hub/src/graphql/queries/userByUsername.ts | 31 - packages/hub/src/graphql/queries/variable.ts | 44 - packages/hub/src/graphql/schema.ts | 18 - packages/hub/src/graphql/types/Group.ts | 191 -- packages/hub/src/graphql/types/GroupInvite.ts | 48 - packages/hub/src/graphql/types/Me.ts | 25 - packages/hub/src/graphql/types/Model.ts | 144 - .../hub/src/graphql/types/ModelRevision.ts | 207 -- .../src/graphql/types/ModelRevisionBuild.ts | 15 - packages/hub/src/graphql/types/Owner.ts | 13 - .../graphql/types/RelativeValuesDefinition.ts | 139 - .../hub/src/graphql/types/SquiggleSnippet.ts | 13 - packages/hub/src/graphql/types/User.ts | 146 - packages/hub/src/graphql/types/Variable.ts | 77 - .../hub/src/graphql/types/VariableRevision.ts | 91 - packages/hub/src/graphql/utils.ts | 25 - packages/hub/src/hooks/useAsyncMutation.ts | 114 - packages/hub/src/hooks/useMutationForm.ts | 81 - packages/hub/src/lib/common.ts | 1 - packages/hub/src/prisma.ts | 6 +- .../components/views/ListView/index.tsx | 4 +- .../components/views/PlotView/ForcePlot.tsx | 2 +- .../views/hooks/useFilteredItems.ts | 6 +- packages/hub/src/relative-values/types.ts | 21 +- .../relative-values/values/ModelEvaluator.ts | 6 +- .../src/relay/PatchedQueryResponseCache.ts | 145 - packages/hub/src/relay/environment.ts | 121 - packages/hub/src/relay/loadPageQuery.ts | 37 - packages/hub/src/relay/usePageQuery.ts | 73 - packages/hub/src/routes.ts | 31 +- .../createVariableRevision.ts | 47 + .../scripts/buildRecentModelRevision/main.ts | 6 +- .../buildRecentModelRevision/worker.ts | 8 +- packages/hub/src/scripts/print-schema.ts | 9 - .../acceptReusableGroupInviteTokenAction.ts | 3 +- .../groups/actions/deleteMembershipAction.ts | 3 +- .../hub/src/server/groups/groupHelpers.ts | 46 + .../actions/adminUpdateModelVersionAction.ts | 94 + .../createSquiggleSnippetModelAction.ts | 8 +- .../server/models/actions/moveModelAction.ts | 2 +- .../updateSquiggleSnippetModelAction.ts | 4 +- .../hub/src/server/models/data/revisions.ts | 18 +- packages/hub/src/server/owners/auth.ts | 67 + .../actions/buildRelativeValuesCacheAction.ts | 2 +- .../actions/clearRelativeValuesCacheAction.ts | 2 +- .../createRelativeValuesDefinitionAction.ts | 2 +- .../deleteRelativeValuesDefinitionAction.tsx | 3 +- .../updateRelativeValuesDefinitionAction.ts | 2 +- .../server/relative-values/data/exports.ts | 6 +- .../relative-values/utils.ts} | 31 +- packages/hub/src/server/users/auth.ts | 36 +- .../variables/data/fullVariableRevision.ts | 74 + .../{data.ts => data/variableCards.ts} | 38 +- .../variables/data/variableRevisions.ts | 88 + .../hub/src/squiggle/components/linker.ts | 55 +- .../src/variables/components/VariableCard.tsx | 4 +- .../src/variables/components/VariableList.tsx | 4 +- packages/hub/test/gql-gen/fragment-masking.ts | 70 - packages/hub/test/gql-gen/gql.ts | 128 - packages/hub/test/gql-gen/graphql.ts | 1157 -------- packages/hub/test/gql-gen/index.ts | 2 - packages/hub/test/graphql/commonQueries.ts | 186 -- packages/hub/test/graphql/helpers.ts | 179 -- .../graphql/mutations/createGroup.test.ts | 59 - .../createSquiggleSnippetModel.test.ts | 129 - .../mutations/deleteMembership.test.ts | 163 -- .../graphql/mutations/deleteModel.test.ts | 84 - .../mutations/inviteUserToGroup.test.ts | 155 -- .../graphql/mutations/setUsername.test.ts | 63 - .../hub/test/graphql/queries/groups.test.ts | 76 - packages/hub/test/graphql/queries/me.test.ts | 23 - .../hub/test/graphql/queries/models.test.ts | 120 - .../graphql/queries/userByUsername.test.ts | 51 - .../hub/test/graphql/queries/users.test.ts | 51 - packages/hub/tsconfig.json | 1 - pnpm-lock.yaml | 2404 +---------------- 128 files changed, 802 insertions(+), 9400 deletions(-) delete mode 100644 packages/hub/codegen.ts delete mode 100644 packages/hub/docs/relay-pages.md delete mode 100644 packages/hub/graphql.config.yaml delete mode 100644 packages/hub/relay.config.js delete mode 100644 packages/hub/schema.graphql delete mode 100644 packages/hub/src/__generated__/.gitkeep create mode 100644 packages/hub/src/app/api/get-source/route.ts delete mode 100644 packages/hub/src/app/api/graphql/route.ts delete mode 100644 packages/hub/src/app/groups/[slug]/hooks.ts delete mode 100644 packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/RelativeValuesExport.ts delete mode 100644 packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/VariablePage.tsx create mode 100644 packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/VariableRevisionsPanel.tsx create mode 100644 packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/layout.tsx rename packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/{SquiggleVariableRevisionPage.tsx => revisions/[revisionId]/VariableRevisionPage.tsx} (73%) create mode 100644 packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/revisions/[revisionId]/page.tsx delete mode 100644 packages/hub/src/components/OwnerLink.tsx delete mode 100644 packages/hub/src/components/exports/RelativeValuesExportItem.tsx delete mode 100644 packages/hub/src/components/ui/MutationAction.tsx delete mode 100644 packages/hub/src/components/ui/MutationButton.tsx delete mode 100644 packages/hub/src/components/ui/MutationModalAction.tsx delete mode 100644 packages/hub/src/graphql/builder.ts delete mode 100644 packages/hub/src/graphql/errors/BaseError.ts delete mode 100644 packages/hub/src/graphql/errors/NotFoundError.ts delete mode 100644 packages/hub/src/graphql/errors/ValidationError.ts delete mode 100644 packages/hub/src/graphql/errors/common.ts delete mode 100644 packages/hub/src/graphql/helpers/groupHelpers.ts delete mode 100644 packages/hub/src/graphql/helpers/ownerHelpers.ts delete mode 100644 packages/hub/src/graphql/helpers/userHelpers.ts delete mode 100644 packages/hub/src/graphql/mutations/adminUpdateModelVersion.ts delete mode 100644 packages/hub/src/graphql/queries/group.ts delete mode 100644 packages/hub/src/graphql/queries/model.ts delete mode 100644 packages/hub/src/graphql/queries/models.ts delete mode 100644 packages/hub/src/graphql/queries/relativeValuesDefinition.ts delete mode 100644 packages/hub/src/graphql/queries/relativeValuesDefinitions.ts delete mode 100644 packages/hub/src/graphql/queries/userByUsername.ts delete mode 100644 packages/hub/src/graphql/queries/variable.ts delete mode 100644 packages/hub/src/graphql/schema.ts delete mode 100644 packages/hub/src/graphql/types/Group.ts delete mode 100644 packages/hub/src/graphql/types/GroupInvite.ts delete mode 100644 packages/hub/src/graphql/types/Me.ts delete mode 100644 packages/hub/src/graphql/types/Model.ts delete mode 100644 packages/hub/src/graphql/types/ModelRevision.ts delete mode 100644 packages/hub/src/graphql/types/ModelRevisionBuild.ts delete mode 100644 packages/hub/src/graphql/types/Owner.ts delete mode 100644 packages/hub/src/graphql/types/RelativeValuesDefinition.ts delete mode 100644 packages/hub/src/graphql/types/SquiggleSnippet.ts delete mode 100644 packages/hub/src/graphql/types/User.ts delete mode 100644 packages/hub/src/graphql/types/Variable.ts delete mode 100644 packages/hub/src/graphql/types/VariableRevision.ts delete mode 100644 packages/hub/src/graphql/utils.ts delete mode 100644 packages/hub/src/hooks/useAsyncMutation.ts delete mode 100644 packages/hub/src/hooks/useMutationForm.ts delete mode 100644 packages/hub/src/relay/PatchedQueryResponseCache.ts delete mode 100644 packages/hub/src/relay/environment.ts delete mode 100644 packages/hub/src/relay/loadPageQuery.ts delete mode 100644 packages/hub/src/relay/usePageQuery.ts create mode 100644 packages/hub/src/scripts/buildRecentModelRevision/createVariableRevision.ts delete mode 100644 packages/hub/src/scripts/print-schema.ts create mode 100644 packages/hub/src/server/groups/groupHelpers.ts create mode 100644 packages/hub/src/server/models/actions/adminUpdateModelVersionAction.ts rename packages/hub/src/{graphql/types/RelativeValuesExport.ts => server/relative-values/utils.ts} (56%) create mode 100644 packages/hub/src/server/variables/data/fullVariableRevision.ts rename packages/hub/src/server/variables/{data.ts => data/variableCards.ts} (71%) create mode 100644 packages/hub/src/server/variables/data/variableRevisions.ts delete mode 100644 packages/hub/test/gql-gen/fragment-masking.ts delete mode 100644 packages/hub/test/gql-gen/gql.ts delete mode 100644 packages/hub/test/gql-gen/graphql.ts delete mode 100644 packages/hub/test/gql-gen/index.ts delete mode 100644 packages/hub/test/graphql/commonQueries.ts delete mode 100644 packages/hub/test/graphql/helpers.ts delete mode 100644 packages/hub/test/graphql/mutations/createGroup.test.ts delete mode 100644 packages/hub/test/graphql/mutations/createSquiggleSnippetModel.test.ts delete mode 100644 packages/hub/test/graphql/mutations/deleteMembership.test.ts delete mode 100644 packages/hub/test/graphql/mutations/deleteModel.test.ts delete mode 100644 packages/hub/test/graphql/mutations/inviteUserToGroup.test.ts delete mode 100644 packages/hub/test/graphql/mutations/setUsername.test.ts delete mode 100644 packages/hub/test/graphql/queries/groups.test.ts delete mode 100644 packages/hub/test/graphql/queries/me.test.ts delete mode 100644 packages/hub/test/graphql/queries/models.test.ts delete mode 100644 packages/hub/test/graphql/queries/userByUsername.test.ts delete mode 100644 packages/hub/test/graphql/queries/users.test.ts diff --git a/packages/hub/README.md b/packages/hub/README.md index 6464989d03..48ec19148b 100644 --- a/packages/hub/README.md +++ b/packages/hub/README.md @@ -66,8 +66,6 @@ Another note is that with the correct setup, out of `pnpm gen:prisma`, `pnpm gen ## Other notes -[How to load GraphQL data in Next.js pages](/docs/relay-pages.md) - [Common workflow for updating Prisma schema](https://www.prisma.io/docs/orm/prisma-migrate/workflows/prototyping-your-schema) # Deployment @@ -79,13 +77,3 @@ Squiggle Hub is deployed on [Vercel](https://vercel.com/) automatically when the The production database is migrated by [this GitHub Action](https://github.com/quantified-uncertainty/squiggle/blob/main/.github/workflows/prisma-migrate-prod.yml). **Important: it should be invoked _before_ merging any PR that changes the schema.** - -## Debugging - -If you get an error like: - -``` -PothosSchemaError [GraphQLError]: Ref ObjectRef has not been implemented -``` - -Make sure that any new files in `src/graphql/types` have been added to `src/schema.ts`, or something that references that. diff --git a/packages/hub/codegen.ts b/packages/hub/codegen.ts deleted file mode 100644 index 9b22fafa40..0000000000 --- a/packages/hub/codegen.ts +++ /dev/null @@ -1,14 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import { type CodegenConfig } from "@graphql-codegen/cli"; - -const config: CodegenConfig = { - schema: "./schema.graphql", - documents: ["test/graphql/**/*.ts"], - generates: { - "./test/gql-gen/": { - preset: "client-preset", - }, - }, -}; - -export default config; diff --git a/packages/hub/docs/relay-pages.md b/packages/hub/docs/relay-pages.md deleted file mode 100644 index 6b1f7dfeac..0000000000 --- a/packages/hub/docs/relay-pages.md +++ /dev/null @@ -1,72 +0,0 @@ -# How to load GraphQL data in Next.js pages - -Based on https://github.com/relayjs/relay-examples/tree/main/issue-tracker-next-v13. - -For any new page that needs GraphQL data you'll need two files: - -1. `page.tsx` (usual Next.js page file) -2. `MyPage.tsx` (gobally unique semantic name, like `ModelPage` or `DefinitionPage`) - -## page.tsx - -**Don't** put `"use client"` in `page.tsx`; it should an RSC. - -Call `loadSerializableQuery` to get a query: - -```typescript -// Generated based on a query definition from `MyPage.tsx` by `relay-compiler`. -// You don't have to rename `MyPageQuery` type, but this pattern will be copy-pasted in all pages, -// so it's nice to have the identical type and var names in the following code. -// (There's almost always only a single query per page, so there's no risk of name collisions.) -import QueryNode, { MyPageQuery as QueryType } from from "@/__generated__/MyPageQuery.graphql"; - -export default async function Page() { - const query = await loadSerializableQuery(QueryNode.params, { - ... // query variables - }); - - return ; -} -``` - -## MyPage.tsx - -`MyPage` component must have `"use client"` declaration on top. - -It should contain: - -1. GraphQL query, like this: - -```typescript -// In Relay, query name ("MyPageQuery") must match the file name ("MyPage"). -// So we store this definition here and not in `page.tsx`. -const Query = graphql` - query MyPageQuery { - myField { - ... - } - } -`; -``` - -2. A React component that takes a serializable query as a parameter: - -```typescript -// Same import as in `page.tsx`. -import QueryNode, { MyPageQuery as QueryType } from from "@/__generated__/MyPageQuery.graphql"; - -export const MyPage: FC<{ - query: SerializablePreloadedQuery; -}> = ({ query }) => { - const queryRef = useSerializablePreloadedQuery(query); - const { myField: result } = usePreloadedQuery(Query, queryRef); - - ... -} -``` - -# Notes - -- If you use `useLazyLoadQuery` instead of this pattern, the performance will be worse -- It's ok to use `useLazyLoadQuery` in the components that render on demand (e.g. data that shows when you open a dropdown) -- If you need to load GraphQL data in the Next.js layout, use `layout.tsx` and `MyLayout.tsx`; everything else should be the same diff --git a/packages/hub/graphql.config.yaml b/packages/hub/graphql.config.yaml deleted file mode 100644 index 0754fbf918..0000000000 --- a/packages/hub/graphql.config.yaml +++ /dev/null @@ -1,5 +0,0 @@ -schema: ./schema.graphql -documents: - - src/**/*.tsx - - src/**/*.ts - - test/**/*.ts diff --git a/packages/hub/next.config.mjs b/packages/hub/next.config.mjs index 9d0ed58f98..a3ec7ca6e3 100644 --- a/packages/hub/next.config.mjs +++ b/packages/hub/next.config.mjs @@ -4,15 +4,6 @@ const nextConfig = { experimental: { runtime: "nodejs", }, - compiler: { - relay: { - // This should match relay.config.js - src: "./src", - language: "typescript", - artifactDirectory: "./src/__generated__", - eagerEsModules: false, - }, - }, redirects: async () => [ { source: "/users/:username/models/:slug*", diff --git a/packages/hub/package.json b/packages/hub/package.json index 1459fbcc1a..3adb04f172 100644 --- a/packages/hub/package.json +++ b/packages/hub/package.json @@ -12,9 +12,7 @@ "dev": "next dev -p 3001 --turbo", "start": "__NEXT_PRIVATE_PREBUNDLED_REACT=next next start", "gen:prisma": "PRISMA_HIDE_UPDATE_MESSAGE=1 prisma generate --no-hints", - "gen:relay": "relay-compiler", - "gen:schema": "pnpm build:esbuild && node ./dist/scripts/print-schema.mjs", - "gen": "pnpm gen:prisma && pnpm gen:schema && pnpm gen:relay", + "gen": "pnpm gen:prisma", "gen:watch": "nodemon --watch src --ext ts,tsx,prisma --exec 'pnpm run gen'", "build:esbuild": "node ./esbuild.cjs", "build:ts": "pnpm gen && tsc", @@ -27,14 +25,6 @@ }, "dependencies": { "@auth/prisma-adapter": "^2.7.4", - "@pothos/core": "^3.41.1", - "@pothos/plugin-errors": "^3.11.1", - "@pothos/plugin-prisma": "^3.65.2", - "@pothos/plugin-relay": "^3.46.0", - "@pothos/plugin-scope-auth": "^3.22.0", - "@pothos/plugin-simple-objects": "^3.7.0", - "@pothos/plugin-validation": "^3.10.1", - "@pothos/plugin-with-input": "^3.10.1", "@prisma/client": "5.22.0", "@quri/squiggle-ai": "workspace:*", "@quri/squiggle-components": "workspace:*", @@ -46,8 +36,6 @@ "clsx": "^2.1.1", "d3": "^7.9.0", "date-fns": "^3.6.0", - "graphql": "^16.8.1", - "graphql-yoga": "^5.1.1", "immutable": "^4.3.6", "invariant": "^2.2.4", "lodash": "^4.17.21", @@ -61,18 +49,13 @@ "react-icons": "^5.2.1", "react-loading-skeleton": "^3.4.0", "react-markdown": "^9.0.1", - "react-relay": "^16.2.0", "react-select": "^5.8.0", - "relay-runtime": "^16.2.0", "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.0", "server-only": "^0.0.1", "zod": "^3.23.8" }, "devDependencies": { - "@graphql-codegen/cli": "^5.0.2", - "@graphql-codegen/client-preset": "^4.2.5", - "@graphql-typed-document-node/core": "^3.2.0", "@parcel/watcher": "^2.4.1", "@quri/configs": "workspace:*", "@types/d3": "^7.4.3", @@ -82,21 +65,16 @@ "@types/node": "^20.12.7", "@types/pako": "^2.0.3", "@types/react": "^18.3.3", - "@types/react-relay": "^16.0.6", - "@types/relay-runtime": "^14.1.23", - "babel-plugin-relay": "^16.2.0", "dotenv-cli": "^7.4.2", "esbuild": "^0.20.2", "eslint": "^8.57.0", "eslint-config-next": "^14.2.3", - "graphql": "^16.8.1", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "nodemon": "^3.1.0", "postcss": "^8.4.38", "prettier": "^3.3.3", "prisma": "^5.22.0", - "relay-compiler": "^16.2.0", "tailwindcss": "^3.4.3", "tsx": "^4.19.1", "typescript": "^5.6.3" diff --git a/packages/hub/prisma/schema.prisma b/packages/hub/prisma/schema.prisma index b45c0ee196..0b8e2617f0 100644 --- a/packages/hub/prisma/schema.prisma +++ b/packages/hub/prisma/schema.prisma @@ -6,10 +6,6 @@ generator client { previewFeatures = ["postgresqlExtensions", "fullTextSearch"] } -generator pothos { - provider = "prisma-pothos-types" -} - datasource db { provider = "postgresql" url = env("DATABASE_URL") diff --git a/packages/hub/relay.config.js b/packages/hub/relay.config.js deleted file mode 100644 index b41c6c86c3..0000000000 --- a/packages/hub/relay.config.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = { - src: "./src", - language: "typescript", - artifactDirectory: "./src/__generated__", - schema: "./schema.graphql", - // https://github.com/facebook/relay/releases/tag/v16.0.0 - typescriptExcludeUndefinedFromNullableUnion: true, - exclude: ["**/node_modules/**", "**/__mocks__/**", "**/__generated__/**"], -}; diff --git a/packages/hub/schema.graphql b/packages/hub/schema.graphql deleted file mode 100644 index 418f9fa79e..0000000000 --- a/packages/hub/schema.graphql +++ /dev/null @@ -1,781 +0,0 @@ -type AcceptReusableGroupInviteTokenResult { - membership: UserGroupMembership! -} - -type AddUserToGroupResult { - membership: UserGroupMembership! -} - -type AdminRebuildSearchIndexResult { - ok: Boolean! -} - -type AdminUpdateModelVersionResult { - model: Model! -} - -type BaseError implements Error { - message: String! -} - -type BuildRelativeValuesCacheResult { - relativeValuesExport: RelativeValuesExport! -} - -type CancelGroupInviteResult { - invite: GroupInvite! -} - -type ClearRelativeValuesCacheResult { - relativeValuesExport: RelativeValuesExport! -} - -type CreateGroupResult { - group: Group! -} - -type CreateRelativeValuesDefinitionResult { - definition: RelativeValuesDefinition! -} - -type CreateReusableGroupInviteTokenResult { - group: Group! -} - -type CreateSquiggleSnippetModelResult { - model: Model! -} - -input DefinitionRefInput { - owner: String! - slug: String! -} - -type DeleteMembershipResult { - ok: Boolean! -} - -type DeleteModelResult { - ok: Boolean! -} - -type DeleteRelativeValuesDefinitionResult { - ok: Boolean! -} - -type DeleteReusableGroupInviteTokenResult { - group: Group! -} - -type EmailGroupInvite implements GroupInvite & Node { - email: String! - group: Group! - id: ID! - role: MembershipRole! -} - -interface Error { - message: String! -} - -type GlobalStatistics { - models: Int! - relativeValuesDefinitions: Int! - users: Int! -} - -type Group implements Node & Owner { - createdAtTimestamp: Float! - id: ID! - inviteForMe: GroupInvite - invites(after: String, before: String, first: Int, last: Int): GroupInviteConnection - memberships(after: String, before: String, first: Int, last: Int): UserGroupMembershipConnection! - models(after: String, before: String, first: Int, last: Int): ModelConnection! - myMembership: UserGroupMembership - reusableInviteToken: String - slug: String! - updatedAtTimestamp: Float! - variableRevisions(after: String, before: String, first: Int, last: Int): VariableRevision! -} - -type GroupConnection { - edges: [GroupEdge!]! - pageInfo: PageInfo! -} - -type GroupEdge { - cursor: String! - node: Group! -} - -interface GroupInvite { - group: Group! - id: ID! - role: MembershipRole! -} - -type GroupInviteConnection { - edges: [GroupInviteEdge!]! - pageInfo: PageInfo! -} - -type GroupInviteEdge { - cursor: String! - node: GroupInvite! -} - -enum GroupInviteReaction { - Accept - Decline -} - -input GroupsQueryInput { - """List only groups that you're a member of""" - myOnly: Boolean - slugContains: String -} - -type InviteUserToGroupResult { - invite: GroupInvite! -} - -type Me { - asUser: User! - email: String! - username: String -} - -enum MembershipRole { - Admin - Member -} - -type Model implements Node { - createdAtTimestamp: Float! - currentRevision: ModelRevision! - id: ID! - isEditable: Boolean! - isPrivate: Boolean! - lastRevisionWithBuild: ModelRevision - owner: Owner! - revision(id: ID!): ModelRevision! - revisions(after: String, before: String, first: Int, last: Int): ModelRevisionConnection! - slug: String! - updatedAtTimestamp: Float! - variables: [Variable!]! -} - -type ModelConnection { - edges: [ModelEdge!]! - pageInfo: PageInfo! -} - -union ModelContent = SquiggleSnippet - -type ModelEdge { - cursor: String! - node: Model! -} - -type ModelRevision implements Node { - author: User - buildStatus: ModelRevisionBuildStatus! - comment: String! - content: ModelContent! - createdAtTimestamp: Float! - exportNames: [String!]! - forRelativeValues(input: ModelRevisionForRelativeValuesInput!): ModelRevisionForRelativeValuesResult! - id: ID! - lastBuild: ModelRevisionBuild - model: Model! - relativeValuesExports: [RelativeValuesExport!]! - variableRevisions: [VariableRevision!]! -} - -type ModelRevisionBuild implements Node { - createdAtTimestamp: Float! - errors: [String!]! - id: ID! - modelRevision: ModelRevision! - runSeconds: Float! -} - -enum ModelRevisionBuildStatus { - Failure - Pending - Skipped - Success -} - -type ModelRevisionConnection { - edges: [ModelRevisionEdge!]! - pageInfo: PageInfo! -} - -type ModelRevisionEdge { - cursor: String! - node: ModelRevision! -} - -input ModelRevisionForRelativeValuesInput { - for: ModelRevisionForRelativeValuesSlugOwnerInput - variableName: String! -} - -union ModelRevisionForRelativeValuesResult = BaseError | NotFoundError | RelativeValuesExport - -input ModelRevisionForRelativeValuesSlugOwnerInput { - owner: String! - slug: String! -} - -type ModelsByVersion { - count: Int! - models: [Model!]! - privateCount: Int! - version: String! -} - -type Mutation { - acceptReusableGroupInviteToken(input: MutationAcceptReusableGroupInviteTokenInput!): MutationAcceptReusableGroupInviteTokenResult! - addUserToGroup(input: MutationAddUserToGroupInput!): MutationAddUserToGroupResult! - - """Admin-only query for rebuilding the search index""" - adminRebuildSearchIndex: MutationAdminRebuildSearchIndexResult! - - """Admin-only query for upgrading model versions""" - adminUpdateModelVersion(input: MutationAdminUpdateModelVersionInput!): MutationAdminUpdateModelVersionResult! - buildRelativeValuesCache(input: MutationBuildRelativeValuesCacheInput!): MutationBuildRelativeValuesCacheResult! - cancelGroupInvite(input: MutationCancelGroupInviteInput!): MutationCancelGroupInviteResult! - clearRelativeValuesCache(input: MutationClearRelativeValuesCacheInput!): MutationClearRelativeValuesCacheResult! - createGroup(input: MutationCreateGroupInput!): MutationCreateGroupResult! - createRelativeValuesDefinition(input: MutationCreateRelativeValuesDefinitionInput!): MutationCreateRelativeValuesDefinitionResult! - - """ - Create or replace a reusable invite token for a group, available as `reusableInviteToken` field on group object. - - You must be an admin of the group to call this mutation. Previous invite token, if it existed, will stop working. - """ - createReusableGroupInviteToken(input: MutationCreateReusableGroupInviteTokenInput!): MutationCreateReusableGroupInviteTokenResult! - createSquiggleSnippetModel(input: MutationCreateSquiggleSnippetModelInput!): MutationCreateSquiggleSnippetModelResult! - deleteMembership(input: MutationDeleteMembershipInput!): MutationDeleteMembershipResult! - deleteModel(input: MutationDeleteModelInput!): MutationDeleteModelResult! - deleteRelativeValuesDefinition(input: MutationDeleteRelativeValuesDefinitionInput!): MutationDeleteRelativeValuesDefinitionResult! - - """Disable a reusable invite token for a group.""" - deleteReusableGroupInviteToken(input: MutationDeleteReusableGroupInviteTokenInput!): MutationDeleteReusableGroupInviteTokenResult! - inviteUserToGroup(input: MutationInviteUserToGroupInput!): MutationInviteUserToGroupResult! - reactToGroupInvite(input: MutationReactToGroupInviteInput!): MutationReactToGroupInviteResult! - updateGroupInviteRole(input: MutationUpdateGroupInviteRoleInput!): MutationUpdateGroupInviteRoleResult! - updateMembershipRole(input: MutationUpdateMembershipRoleInput!): MutationUpdateMembershipRoleResult! - updateModelPrivacy(input: MutationUpdateModelPrivacyInput!): MutationUpdateModelPrivacyResult! - updateRelativeValuesDefinition(input: MutationUpdateRelativeValuesDefinitionInput!): MutationUpdateRelativeValuesDefinitionResult! - updateSquiggleSnippetModel(input: MutationUpdateSquiggleSnippetModelInput!): MutationUpdateSquiggleSnippetModelResult! - validateReusableGroupInviteToken(input: MutationValidateReusableGroupInviteTokenInput!): MutationValidateReusableGroupInviteTokenResult! -} - -input MutationAcceptReusableGroupInviteTokenInput { - groupSlug: String! - inviteToken: String! -} - -union MutationAcceptReusableGroupInviteTokenResult = AcceptReusableGroupInviteTokenResult | BaseError - -input MutationAddUserToGroupInput { - group: String! - role: MembershipRole! - username: String! -} - -union MutationAddUserToGroupResult = AddUserToGroupResult | BaseError | ValidationError - -union MutationAdminRebuildSearchIndexResult = AdminRebuildSearchIndexResult | BaseError - -input MutationAdminUpdateModelVersionInput { - modelId: String! - version: String! -} - -union MutationAdminUpdateModelVersionResult = AdminUpdateModelVersionResult | BaseError - -input MutationBuildRelativeValuesCacheInput { - exportId: String! -} - -union MutationBuildRelativeValuesCacheResult = BaseError | BuildRelativeValuesCacheResult - -input MutationCancelGroupInviteInput { - inviteId: String! -} - -union MutationCancelGroupInviteResult = BaseError | CancelGroupInviteResult - -input MutationClearRelativeValuesCacheInput { - exportId: String! -} - -union MutationClearRelativeValuesCacheResult = BaseError | ClearRelativeValuesCacheResult - -input MutationCreateGroupInput { - slug: String! -} - -union MutationCreateGroupResult = BaseError | CreateGroupResult - -input MutationCreateRelativeValuesDefinitionInput { - clusters: [RelativeValuesClusterInput!]! - - """ - Optional, if not set, definition will be created on current user's account - """ - groupSlug: String - items: [RelativeValuesItemInput!]! - recommendedUnit: String - slug: String! - title: String! -} - -union MutationCreateRelativeValuesDefinitionResult = BaseError | CreateRelativeValuesDefinitionResult | ValidationError - -input MutationCreateReusableGroupInviteTokenInput { - slug: String! -} - -union MutationCreateReusableGroupInviteTokenResult = BaseError | CreateReusableGroupInviteTokenResult - -input MutationCreateSquiggleSnippetModelInput { - """Squiggle source code""" - code: String! - - """Optional, if not set, model will be created on current user's account""" - groupSlug: String - - """Defaults to false""" - isPrivate: Boolean - - """A unique seed, used for calculation""" - seed: String! - slug: String! - version: String! -} - -union MutationCreateSquiggleSnippetModelResult = BaseError | CreateSquiggleSnippetModelResult | ValidationError - -input MutationDeleteMembershipInput { - group: String! - user: String! -} - -union MutationDeleteMembershipResult = BaseError | DeleteMembershipResult - -input MutationDeleteModelInput { - owner: String! - slug: String! -} - -union MutationDeleteModelResult = BaseError | DeleteModelResult | NotFoundError | ValidationError - -input MutationDeleteRelativeValuesDefinitionInput { - owner: String! - slug: String! -} - -union MutationDeleteRelativeValuesDefinitionResult = BaseError | DeleteRelativeValuesDefinitionResult - -input MutationDeleteReusableGroupInviteTokenInput { - slug: String! -} - -union MutationDeleteReusableGroupInviteTokenResult = BaseError | DeleteReusableGroupInviteTokenResult - -input MutationInviteUserToGroupInput { - group: String! - role: MembershipRole! - username: String! -} - -union MutationInviteUserToGroupResult = BaseError | InviteUserToGroupResult | ValidationError - -input MutationReactToGroupInviteInput { - action: GroupInviteReaction! - inviteId: String! -} - -union MutationReactToGroupInviteResult = BaseError | ReactToGroupInviteResult - -input MutationUpdateGroupInviteRoleInput { - inviteId: String! - role: MembershipRole! -} - -union MutationUpdateGroupInviteRoleResult = BaseError | UpdateGroupInviteRoleResult - -input MutationUpdateMembershipRoleInput { - group: String! - role: MembershipRole! - user: String! -} - -union MutationUpdateMembershipRoleResult = BaseError | UpdateMembershipRoleResult - -input MutationUpdateModelPrivacyInput { - isPrivate: Boolean! - owner: String! - slug: String! -} - -union MutationUpdateModelPrivacyResult = BaseError | UpdateModelPrivacyResult - -input MutationUpdateRelativeValuesDefinitionInput { - clusters: [RelativeValuesClusterInput!]! - items: [RelativeValuesItemInput!]! - owner: String! - recommendedUnit: String - slug: String! - title: String! -} - -union MutationUpdateRelativeValuesDefinitionResult = BaseError | UpdateRelativeValuesDefinitionResult - -input MutationUpdateSquiggleSnippetModelInput { - comment: String - content: SquiggleSnippetContentInput! - owner: String! - relativeValuesExports: [RelativeValuesExportInput!] - slug: String! -} - -union MutationUpdateSquiggleSnippetModelResult = BaseError | UpdateSquiggleSnippetResult - -input MutationValidateReusableGroupInviteTokenInput { - groupSlug: String! - inviteToken: String! -} - -union MutationValidateReusableGroupInviteTokenResult = BaseError | ValidateReusableGroupInviteTokenResult - -interface Node { - id: ID! -} - -type NotFoundError implements Error { - message: String! -} - -interface Owner { - id: ID! - slug: String! -} - -type PageInfo { - endCursor: String - hasNextPage: Boolean! - hasPreviousPage: Boolean! - startCursor: String -} - -type Query { - globalStatistics: GlobalStatistics! - group(slug: String!): QueryGroupResult! - groups(after: String, before: String, first: Int, input: GroupsQueryInput, last: Int): GroupConnection! - me: Me! - model(input: QueryModelInput!): QueryModelResult! - models(after: String, before: String, first: Int, last: Int): ModelConnection! - - """Admin-only query for listing models in /admin UI""" - modelsByVersion: [ModelsByVersion!]! - node(id: ID!): Node - nodes(ids: [ID!]!): [Node]! - relativeValuesDefinition(input: QueryRelativeValuesDefinitionInput!): QueryRelativeValuesDefinitionResult! - relativeValuesDefinitions(after: String, before: String, first: Int, input: RelativeValuesDefinitionsQueryInput, last: Int): RelativeValuesDefinitionConnection! - runSquiggle(code: String!, seed: String): SquiggleOutput! - search(after: String, before: String, first: Int, last: Int, text: String!): QuerySearchResult! - userByUsername(username: String!): QueryUserByUsernameResult! - variable(input: QueryVariableInput!): QueryVariableResult! - variables(after: String, before: String, first: Int, input: VariableQueryInput, last: Int): VariableConnection! -} - -union QueryGroupResult = BaseError | Group | NotFoundError - -input QueryModelInput { - owner: String! - slug: String! -} - -union QueryModelResult = BaseError | Model | NotFoundError - -input QueryRelativeValuesDefinitionInput { - owner: String! - slug: String! -} - -union QueryRelativeValuesDefinitionResult = BaseError | NotFoundError | RelativeValuesDefinition - -type QuerySearchConnection { - edges: [SearchEdge!]! - pageInfo: PageInfo! -} - -union QuerySearchResult = BaseError | QuerySearchConnection - -union QueryUserByUsernameResult = BaseError | NotFoundError | User - -input QueryVariableInput { - owner: String! - slug: String! - variableName: String! -} - -union QueryVariableResult = BaseError | NotFoundError | Variable - -type ReactToGroupInviteResult { - invite: GroupInvite! - membership: UserGroupMembership -} - -type RelativeValuesCluster { - color: String! - id: String! - recommendedUnit: String -} - -input RelativeValuesClusterInput { - color: String! - id: String! - recommendedUnit: String -} - -type RelativeValuesDefinition implements Node { - createdAtTimestamp: Float! - currentRevision: RelativeValuesDefinitionRevision! - id: ID! - isEditable: Boolean! - modelExports: [RelativeValuesExport!]! - owner: Owner! - slug: String! - updatedAtTimestamp: Float! -} - -type RelativeValuesDefinitionConnection { - edges: [RelativeValuesDefinitionEdge!]! - pageInfo: PageInfo! -} - -type RelativeValuesDefinitionEdge { - cursor: String! - node: RelativeValuesDefinition! -} - -type RelativeValuesDefinitionRevision implements Node { - clusters: [RelativeValuesCluster!]! - id: ID! - items: [RelativeValuesItem!]! - recommendedUnit: String - title: String! -} - -input RelativeValuesDefinitionsQueryInput { - owner: String - slugContains: String -} - -type RelativeValuesExport implements Node { - cache: [RelativeValuesPairCache!]! - definition: RelativeValuesDefinition! - id: ID! - modelRevision: ModelRevision! - variableName: String! -} - -input RelativeValuesExportInput { - definition: DefinitionRefInput! - variableName: String! -} - -type RelativeValuesItem { - clusterId: String - description: String! - id: String! - name: String! -} - -input RelativeValuesItemInput { - clusterId: String - description: String - id: String! - name: String! -} - -type RelativeValuesPairCache implements Node { - errorString: String - firstItem: String! - id: ID! - resultJSON: String! - secondItem: String! -} - -type SearchEdge { - cursor: String! - node: Searchable! - rank: Float! - slugSnippet: String! - textSnippet: String! -} - -type Searchable implements Node { - id: ID! - link: String! - object: SearchableObject! -} - -union SearchableObject = Group | Model | RelativeValuesDefinition | User - -type SquiggleErrorOutput implements SquiggleOutput { - errorString: String! - isCached: Boolean! -} - -type SquiggleOkOutput implements SquiggleOutput { - bindingsJSON: String! - isCached: Boolean! - resultJSON: String! -} - -interface SquiggleOutput { - isCached: Boolean! -} - -type SquiggleSnippet implements Node { - autorunMode: Boolean - code: String! - id: ID! - sampleCount: Int - seed: String! - version: String! - xyPointLength: Int -} - -input SquiggleSnippetContentInput { - autorunMode: Boolean - code: String! - sampleCount: Int - seed: String! - version: String! - xyPointLength: Int -} - -type UpdateGroupInviteRoleResult { - invite: GroupInvite! -} - -type UpdateMembershipRoleResult { - membership: UserGroupMembership! -} - -type UpdateModelPrivacyResult { - model: Model! -} - -type UpdateRelativeValuesDefinitionResult { - definition: RelativeValuesDefinition! -} - -type UpdateSquiggleSnippetResult { - model: Model! -} - -type User implements Node & Owner { - groups(after: String, before: String, first: Int, last: Int): GroupConnection! - id: ID! - isMe: Boolean! - isRoot: Boolean! - models(after: String, before: String, first: Int, last: Int): ModelConnection! - relativeValuesDefinitions(after: String, before: String, first: Int, last: Int): RelativeValuesDefinitionConnection! - slug: String! - username: String! - variables(after: String, before: String, first: Int, last: Int): VariableConnection! -} - -type UserGroupInvite implements GroupInvite & Node { - group: Group! - id: ID! - role: MembershipRole! - user: User! -} - -type UserGroupMembership implements Node { - group: Group! - id: ID! - role: MembershipRole! - user: User! -} - -type UserGroupMembershipConnection { - edges: [UserGroupMembershipEdge!]! - pageInfo: PageInfo! -} - -type UserGroupMembershipEdge { - cursor: String! - node: UserGroupMembership! -} - -type ValidateReusableGroupInviteTokenResult { - ok: Boolean! -} - -type ValidationError implements Error { - issues: [ValidationErrorIssue!]! - message: String! -} - -type ValidationErrorIssue { - message: String! - path: [String!]! -} - -type Variable implements Node { - currentRevision: VariableRevision - id: ID! - model: Model! - owner: Owner! - revisions(after: String, before: String, first: Int, last: Int): VariableRevisionConnection! - variableName: String! -} - -type VariableConnection { - edges: [VariableEdge!]! - pageInfo: PageInfo! -} - -type VariableEdge { - cursor: String! - node: Variable! -} - -input VariableQueryInput { - modelId: String - owner: String - variableName: String - variableType: String -} - -type VariableRevision implements Node { - docstring: String! - id: ID! - modelRevision: ModelRevision! - title: String - variable: Variable! - variableName: String! - variableType: String! -} - -type VariableRevisionConnection { - edges: [VariableRevisionEdge!]! - pageInfo: PageInfo! -} - -type VariableRevisionEdge { - cursor: String! - node: VariableRevision! -} \ No newline at end of file diff --git a/packages/hub/src/__generated__/.gitkeep b/packages/hub/src/__generated__/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/hub/src/app/(frontpage)/variables/page.tsx b/packages/hub/src/app/(frontpage)/variables/page.tsx index a2908df253..bde5166dc3 100644 --- a/packages/hub/src/app/(frontpage)/variables/page.tsx +++ b/packages/hub/src/app/(frontpage)/variables/page.tsx @@ -1,4 +1,4 @@ -import { loadVariableCards } from "@/server/variables/data"; +import { loadVariableCards } from "@/server/variables/data/variableCards"; import { VariableList } from "@/variables/components/VariableList"; export default async function OuterVariablesPage() { diff --git a/packages/hub/src/app/about/page.tsx b/packages/hub/src/app/about/page.tsx index 61b6cc254e..977008f566 100644 --- a/packages/hub/src/app/about/page.tsx +++ b/packages/hub/src/app/about/page.tsx @@ -10,7 +10,6 @@ import { NEWSLETTER_URL, QURI_DONATE_URL, } from "@/lib/common"; -import { graphqlPlaygroundRoute } from "@/routes"; const markdown = ` # About Squiggle Hub @@ -27,7 +26,6 @@ Squiggle Hub is made by the [Quantified Uncertainty Research Institute](https:// ## Key Links - [Squiggle](https://www.squiggle-language.com/) -- [Squiggle API](${graphqlPlaygroundRoute()}) - [Squiggle Discord](${DISCORD_URL}) - [Squiggle Github](${GITHUB_URL}) - [Squiggle Github Discussion (For Ideas and Issues)](${GITHUB_DISCUSSION_URL}) @@ -36,9 +34,6 @@ Squiggle Hub is made by the [Quantified Uncertainty Research Institute](https:// ## Licensing Squiggle Hub, along with Squiggle, is available for free use, with the code being open-source and licensed under MIT. Access it [here](https://github.com/quantified-uncertainty/squiggle). -## Squiggle Hub API -Developed using a GraphQL API, Squiggle Hub can be accessed [here](${graphqlPlaygroundRoute()}). The API is recommended for querying. If you want to run mutations, reach out to us via [Discord](${DISCORD_URL}). Squiggle's JavaScript implementation is available on [NPM](https://www.npmjs.com/package/squiggle-lang). There are several [integrations](https://www.squiggle-language.com/docs/Integrations) like VS Code and Observable. - ## Feature: Relative Values Squiggle Hub currently supports experimental [relative values](https://forum.effectivealtruism.org/posts/EFEwBvuDrTLDndqCt/relative-value-functions-a-flexible-new-format-for-value), with future improvements planned. diff --git a/packages/hub/src/app/admin/layout.tsx b/packages/hub/src/app/admin/layout.tsx index c20592c791..cb43c102ef 100644 --- a/packages/hub/src/app/admin/layout.tsx +++ b/packages/hub/src/app/admin/layout.tsx @@ -6,7 +6,7 @@ import { auth } from "@/auth"; import { FullLayoutWithPadding } from "@/components/layout/FullLayoutWithPadding"; import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; import { H1 } from "@/components/ui/Headers"; -import { isRootEmail } from "@/graphql/helpers/userHelpers"; +import { isRootEmail } from "@/server/users/auth"; export default async function AdminLayout({ children }: PropsWithChildren) { const session = await auth(); diff --git a/packages/hub/src/app/admin/upgrade-versions/UpgradeVersionsPage.tsx b/packages/hub/src/app/admin/upgrade-versions/UpgradeVersionsPage.tsx index 16b9380c87..0bc321eec1 100644 --- a/packages/hub/src/app/admin/upgrade-versions/UpgradeVersionsPage.tsx +++ b/packages/hub/src/app/admin/upgrade-versions/UpgradeVersionsPage.tsx @@ -1,6 +1,5 @@ "use client"; import { FC, useState } from "react"; -import { graphql } from "relay-runtime"; import { Button, @@ -11,15 +10,14 @@ import { import { defaultSquiggleVersion } from "@quri/versioned-squiggle-components"; import { H2 } from "@/components/ui/Headers"; -import { MutationButton } from "@/components/ui/MutationButton"; +import { ServerActionButton } from "@/components/ui/ServerActionButton"; import { StyledLink } from "@/components/ui/StyledLink"; import { modelRoute } from "@/routes"; +import { adminUpdateModelVersionAction } from "@/server/models/actions/adminUpdateModelVersionAction"; import { ModelByVersion } from "@/server/models/data/byVersion"; import { UpgradeableModel } from "./UpgradeableModel"; -import { UpgradeVersionsPage_updateMutation } from "@/__generated__/UpgradeVersionsPage_updateMutation.graphql"; - const ModelList: FC<{ models: ModelByVersion["models"]; }> = ({ models }) => { @@ -43,36 +41,13 @@ const ModelList: FC<{ {model.owner.slug}/{model.slug}
- - expectedTypename="AdminUpdateModelVersionResult" - mutation={graphql` - mutation UpgradeVersionsPage_updateMutation( - $input: MutationAdminUpdateModelVersionInput! - ) { - result: adminUpdateModelVersion(input: $input) { - __typename - ... on BaseError { - message - } - ... on AdminUpdateModelVersionResult { - model { - ...EditSquiggleSnippetModel - } - } - } - } - `} - updater={() => { - window.location.reload(); - }} - variables={{ - input: { + { + await adminUpdateModelVersionAction({ modelId: model.id, version: defaultSquiggleVersion, - }, + }); + window.location.reload(); }} title={`Upgrade to ${defaultSquiggleVersion}`} theme="primary" diff --git a/packages/hub/src/app/ai/api/create/route.ts b/packages/hub/src/app/ai/api/create/route.ts index 17a3e0502a..56e2b23257 100644 --- a/packages/hub/src/app/ai/api/create/route.ts +++ b/packages/hub/src/app/ai/api/create/route.ts @@ -8,9 +8,9 @@ import { } from "@quri/squiggle-ai/server"; import { auth } from "@/auth"; -import { getSelf, isSignedIn } from "@/graphql/helpers/userHelpers"; import { prisma } from "@/prisma"; import { workflowToV2_0Json } from "@/server/ai/v2_0"; +import { getSelf, isSignedIn } from "@/server/users/auth"; import { AiRequestBody, aiRequestBodySchema } from "../../utils"; diff --git a/packages/hub/src/app/api/get-source/route.ts b/packages/hub/src/app/api/get-source/route.ts new file mode 100644 index 0000000000..1b12d62917 --- /dev/null +++ b/packages/hub/src/app/api/get-source/route.ts @@ -0,0 +1,27 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; + +import { loadModelCard } from "@/server/models/data/cards"; +import { zSlug } from "@/server/utils"; + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + + const { owner, slug } = z + .object({ + owner: zSlug, + slug: zSlug, + }) + .parse(Object.fromEntries(searchParams.entries())); + + const model = await loadModelCard({ owner, slug }); + if (!model?.currentRevision.squiggleSnippet) { + return Response.json({ error: "Not found" }, { status: 404 }); + } + + const code = model.currentRevision.squiggleSnippet.code; + + return Response.json({ + code, + }); +} diff --git a/packages/hub/src/app/api/graphql/route.ts b/packages/hub/src/app/api/graphql/route.ts deleted file mode 100644 index faad42fde6..0000000000 --- a/packages/hub/src/app/api/graphql/route.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { createYoga } from "graphql-yoga"; -import { NextRequest, NextResponse } from "next/server"; - -import { auth } from "@/auth"; -import { schema } from "@/graphql/schema"; - -const yoga = createYoga({ - graphqlEndpoint: "/api/graphql", - schema, - context: async () => { - // There's some magic involved here; - // getServerSession() obtains request data through Next.js cookies() and headers() functions - // See also: https://github.com/nextauthjs/next-auth/issues/7355 - const session = await auth(); - return { session }; - }, -}); - -async function handler(request: NextRequest) { - const response = await yoga.fetch(request, { - method: request.method, - headers: request.headers, - body: request.body, - }); - return new NextResponse(response.body, { - headers: response.headers, - status: response.status, - }); -} - -export { handler as GET, handler as POST }; diff --git a/packages/hub/src/app/groups/[slug]/hooks.ts b/packages/hub/src/app/groups/[slug]/hooks.ts deleted file mode 100644 index 6fc0fe7b3b..0000000000 --- a/packages/hub/src/app/groups/[slug]/hooks.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { graphql, useFragment } from "react-relay"; - -import { hooks_useIsGroupAdmin$key } from "@/__generated__/hooks_useIsGroupAdmin.graphql"; - -export function useIsGroupAdmin(groupRef: hooks_useIsGroupAdmin$key) { - const { myMembership } = useFragment( - graphql` - fragment hooks_useIsGroupAdmin on Group { - myMembership { - id - role - } - } - `, - groupRef - ); - return myMembership?.role === "Admin"; -} diff --git a/packages/hub/src/app/groups/[slug]/members/MembershipRoleButton.tsx b/packages/hub/src/app/groups/[slug]/members/MembershipRoleButton.tsx index ee7f3c0fb9..50a35e2101 100644 --- a/packages/hub/src/app/groups/[slug]/members/MembershipRoleButton.tsx +++ b/packages/hub/src/app/groups/[slug]/members/MembershipRoleButton.tsx @@ -1,3 +1,4 @@ +import { type MembershipRole } from "@prisma/client"; import { FC } from "react"; import { Button, Dropdown, DropdownMenu } from "@quri/ui"; @@ -6,8 +7,6 @@ import { GroupMemberDTO } from "@/server/groups/data/members"; import { SetMembershipRoleAction } from "./SetMembershipRoleAction"; -import { MembershipRole } from "@/__generated__/MembershipRoleButton_Membership.graphql"; - type Props = { groupSlug: string; membership: GroupMemberDTO; diff --git a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx index 3943d74629..8b4b4f2064 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx @@ -69,6 +69,9 @@ export type SquiggleSnippetFormShape = { }[]; }; +export type RelativeValuesExportInput = + SquiggleSnippetFormShape["relativeValuesExports"][number]; + type OnSubmit = ( event?: BaseSyntheticEvent, extraData?: { comment: string } diff --git a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/RelativeValuesExport.ts b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/RelativeValuesExport.ts deleted file mode 100644 index 670e011d9e..0000000000 --- a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/RelativeValuesExport.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { graphql } from "relay-runtime"; - -// Used in ModelEvaluator and other relative-values functions. -// Since those functions are not components or hooks, we pass around a RelativeValuesExport$data value. -export const RelativeValuesExport = graphql` - fragment RelativeValuesExport on RelativeValuesExport { - id - ...RelativeValuesExportItem - definition { - slug - owner { - slug - } - currentRevision { - ...RelativeValuesDefinitionRevision - } - } - cache { - firstItem - secondItem - resultJSON - errorString - } - } -`; diff --git a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/VariablePage.tsx b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/VariablePage.tsx deleted file mode 100644 index 3a93c40278..0000000000 --- a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/VariablePage.tsx +++ /dev/null @@ -1,211 +0,0 @@ -"use client"; -import clsx from "clsx"; -import { format } from "date-fns"; -import { FC, useState } from "react"; -import { FaClock, FaMinusCircle } from "react-icons/fa"; -import { graphql, usePaginationFragment } from "react-relay"; -import { FragmentRefs } from "relay-runtime"; - -import { CheckIcon, XIcon } from "@quri/ui"; - -import { LoadMore } from "@/components/LoadMore"; -import { Link } from "@/components/ui/Link"; -import { extractFromGraphqlErrorUnion } from "@/lib/graphqlHelpers"; -import { exportTypeIcon } from "@/lib/typeIcon"; -import { SerializablePreloadedQuery } from "@/relay/loadPageQuery"; -import { usePageQuery } from "@/relay/usePageQuery"; - -import { SquiggleVariableRevisionPage } from "./SquiggleVariableRevisionPage"; - -import { - VariablePage$data, - VariablePage$key, -} from "@/__generated__/VariablePage.graphql"; -import { VariablePageQuery } from "@/__generated__/VariablePageQuery.graphql"; - -const buildStatusIcon = (status: string) => { - switch (status) { - case "Success": - return ; - case "Failure": - return ; - case "Pending": - return ; - case "Skipped": - return ; - } -}; - -type VariableRevisions = VariablePage$data["revisions"]; -const VariableRevisionsPanel: FC<{ - revisions: VariableRevisions; - selected: string; - changeId: (id: string) => void; - loadNext?: (count: number) => void; -}> = ({ revisions, selected, changeId, loadNext }) => { - return ( -
-

- Revisions -

-
- {revisions.edges.map(({ node: revision }) => { - const Icon = exportTypeIcon(revision.variableType || ""); - return ( -
-
- changeId(revision.id)} - > - {format( - new Date(revision.modelRevision.createdAtTimestamp), - "MMM dd h:mm a" - )} - -
-
- -
-
- {buildStatusIcon(revision.modelRevision.buildStatus)} -
-
- ); - })} -
- {loadNext && } -
- ); -}; - -export const VariablePageBody: FC<{ - variableName: string; - result: { - readonly __typename: "Variable"; - readonly id: string; - readonly variableName: string; - readonly " $fragmentSpreads": FragmentRefs<"VariablePage">; - }; -}> = ({ result, variableName }) => { - const variable = extractFromGraphqlErrorUnion(result, "Variable"); - - const { - data: { revisions }, - loadNext, - } = usePaginationFragment( - graphql` - fragment VariablePage on Variable - @argumentDefinitions( - cursor: { type: "String" } - count: { type: "Int", defaultValue: 20 } - ) - @refetchable(queryName: "VariablePagePaginationQuery") { - revisions(first: $count, after: $cursor) - @connection(key: "VariablePage_revisions") { - edges { - node { - id - variableName - variableType - modelRevision { - id - createdAtTimestamp - content { - __typename - ...SquiggleVariableRevisionPage - } - buildStatus - } - } - } - pageInfo { - hasNextPage - } - } - } - `, - variable - ); - - const [selected, changeId] = useState( - revisions.edges.at(0)?.node.id ?? null - ); - - if (selected === null) { - return
No revisions found. They should be built shortly.
; - } - - const content = revisions.edges.find((edge) => edge.node.id === selected) - ?.node.modelRevision.content; - - if (content) { - switch (content.__typename) { - case "SquiggleSnippet": { - return ( -
-
- -
- -
- ); - } - default: - return
Unknown model type {content.__typename}
; - } - } -}; - -export const VariablePage: FC<{ - params: { - owner: string; - slug: string; - variableName: string; - }; - query: SerializablePreloadedQuery; -}> = ({ query, params }) => { - const [{ variable: result }] = usePageQuery( - graphql` - query VariablePageQuery($input: QueryVariableInput!) { - variable(input: $input) { - __typename - ... on Variable { - id - variableName - ...VariablePage - } - } - } - `, - query - ); - - if (result.__typename !== "Variable") { - return
No revisions found. They should be built shortly.
; - } - - return ( - - ); -}; diff --git a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/VariableRevisionsPanel.tsx b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/VariableRevisionsPanel.tsx new file mode 100644 index 0000000000..d17924e705 --- /dev/null +++ b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/VariableRevisionsPanel.tsx @@ -0,0 +1,87 @@ +"use client"; + +import clsx from "clsx"; +import { format } from "date-fns"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { FC } from "react"; +import { FaClock, FaMinusCircle } from "react-icons/fa"; + +import { CheckIcon, XIcon } from "@quri/ui"; + +import { LoadMore } from "@/components/LoadMore"; +import { usePaginator } from "@/hooks/usePaginator"; +import { exportTypeIcon } from "@/lib/typeIcon"; +import { variableRevisionRoute } from "@/routes"; +import { Paginated } from "@/server/types"; +import { VariableRevisionDTO } from "@/server/variables/data/variableRevisions"; + +const buildStatusIcon = (status: string) => { + switch (status) { + case "Success": + return ; + case "Failure": + return ; + case "Pending": + return ; + case "Skipped": + return ; + } +}; + +export const VariableRevisionsPanel: FC<{ + page: Paginated; + owner: string; + modelSlug: string; + variableName: string; +}> = ({ page: initialPage, owner, modelSlug, variableName }) => { + const { items: revisions, loadNext } = usePaginator(initialPage); + + const pathname = usePathname(); + + return ( +
+

+ Revisions +

+
+ {revisions.map((revision) => { + const Icon = exportTypeIcon(revision.variableType); + const link = variableRevisionRoute({ + owner, + modelSlug, + variableName, + revisionId: revision.id, + }); + + return ( +
+
+ + {format(revision.modelRevision.createdAt, "MMM dd h:mm a")} + +
+
+ +
+
+ {buildStatusIcon(revision.modelRevision.buildStatus)} +
+
+ ); + })} +
+ {loadNext && } +
+ ); +}; diff --git a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/layout.tsx b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/layout.tsx new file mode 100644 index 0000000000..f92474dd02 --- /dev/null +++ b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/layout.tsx @@ -0,0 +1,33 @@ +import { PropsWithChildren } from "react"; + +import { loadVariableRevisions } from "@/server/variables/data/variableRevisions"; + +import { VariableRevisionsPanel } from "./VariableRevisionsPanel"; + +type Props = { + params: Promise<{ owner: string; slug: string; variableName: string }>; +}; + +export default async function VariableLayout({ + children, + params, +}: PropsWithChildren) { + const { owner, slug, variableName } = await params; + const revisions = await loadVariableRevisions({ owner, slug, variableName }); + + if (!revisions.items.length) { + return
No revisions found. They should be built shortly.
; + } + + return ( +
+
{children}
+ +
+ ); +} diff --git a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/page.tsx b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/page.tsx index 59b7ee0586..07b3fca0f9 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/page.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/page.tsx @@ -1,10 +1,9 @@ -import { loadPageQuery } from "@/relay/loadPageQuery"; +import { notFound } from "next/navigation"; -import { VariablePage } from "./VariablePage"; +import { loadVariableRevisionFull } from "@/server/variables/data/fullVariableRevision"; +import { loadVariableCard } from "@/server/variables/data/variableCards"; -import QueryNode, { - VariablePageQuery, -} from "@/__generated__/VariablePageQuery.graphql"; +import { VariableRevisionPage } from "./revisions/[revisionId]/VariableRevisionPage"; type Props = { params: Promise<{ owner: string; slug: string; variableName: string }>; @@ -12,17 +11,32 @@ type Props = { export default async function OuterVariablePage({ params }: Props) { const { owner, slug, variableName } = await params; - const query = await loadPageQuery(QueryNode, { - input: { - owner, - slug, - variableName, - }, + + const variable = await loadVariableCard({ + owner, + slug, + variableName, }); + if (!variable?.currentRevision) { + // "currentRevision" check won't happen, layout won't render the page if it's null + notFound(); + } + + const revision = await loadVariableRevisionFull({ + owner, + slug, + variableName, + revisionId: variable.currentRevision.id, + }); + + if (!revision) { + notFound(); + } + return (
- +
); } diff --git a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/SquiggleVariableRevisionPage.tsx b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/revisions/[revisionId]/VariableRevisionPage.tsx similarity index 73% rename from packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/SquiggleVariableRevisionPage.tsx rename to packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/revisions/[revisionId]/VariableRevisionPage.tsx index 7578488d32..ee9b0426f0 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/SquiggleVariableRevisionPage.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/revisions/[revisionId]/VariableRevisionPage.tsx @@ -1,6 +1,5 @@ "use client"; import { FC, use } from "react"; -import { graphql, useFragment } from "react-relay"; import { useAdjustSquiggleVersion, @@ -9,10 +8,9 @@ import { versionSupportsSqPathV2, } from "@quri/versioned-squiggle-components"; +import { VariableRevisionFullDTO } from "@/server/variables/data/fullVariableRevision"; import { sqProjectWithHubLinker } from "@/squiggle/components/linker"; -import { SquiggleVariableRevisionPage$key } from "@/__generated__/SquiggleVariableRevisionPage.graphql"; - type SquiggleProps = { variableName: string; code: string; @@ -51,27 +49,17 @@ const VersionedSquiggleVariableRevisionPage: FC< ); }; -export const SquiggleVariableRevisionPage: FC<{ - variableName: string; - contentRef: SquiggleVariableRevisionPage$key; -}> = ({ variableName, contentRef }) => { - const content = useFragment( - graphql` - fragment SquiggleVariableRevisionPage on SquiggleSnippet { - id - code - version - } - `, - contentRef - ); +export const VariableRevisionPage: FC<{ + revision: VariableRevisionFullDTO; +}> = ({ revision }) => { + const content = revision.modelRevision.squiggleSnippet; const checkedVersion = useAdjustSquiggleVersion(content.version); if (!versionSupportsExports.plain(checkedVersion)) { return (
- Export view pages don't support Squiggle {checkedVersion}. + Variable view pages don't support Squiggle {checkedVersion}.
); } @@ -79,7 +67,7 @@ export const SquiggleVariableRevisionPage: FC<{ return ( ); diff --git a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/revisions/[revisionId]/page.tsx b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/revisions/[revisionId]/page.tsx new file mode 100644 index 0000000000..e3b7f40d9e --- /dev/null +++ b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/revisions/[revisionId]/page.tsx @@ -0,0 +1,31 @@ +import { notFound } from "next/navigation"; + +import { loadVariableRevisionFull } from "@/server/variables/data/fullVariableRevision"; + +import { VariableRevisionPage } from "./VariableRevisionPage"; + +export default async function OuterVariableRevisionPage({ + params, +}: { + params: Promise<{ + revisionId: string; + owner: string; + slug: string; + variableName: string; + }>; +}) { + const { revisionId, owner, slug, variableName } = await params; + + const revision = await loadVariableRevisionFull({ + owner, + slug, + variableName, + revisionId, + }); + + if (!revision) { + notFound(); + } + + return ; +} diff --git a/packages/hub/src/app/users/[username]/variables/page.tsx b/packages/hub/src/app/users/[username]/variables/page.tsx index 413a75b965..0b18b53afa 100644 --- a/packages/hub/src/app/users/[username]/variables/page.tsx +++ b/packages/hub/src/app/users/[username]/variables/page.tsx @@ -1,6 +1,6 @@ import { Metadata } from "next"; -import { loadVariableCards } from "@/server/variables/data"; +import { loadVariableCards } from "@/server/variables/data/variableCards"; import { VariableList } from "@/variables/components/VariableList"; type Props = { diff --git a/packages/hub/src/components/OwnerLink.tsx b/packages/hub/src/components/OwnerLink.tsx deleted file mode 100644 index e0eac5a623..0000000000 --- a/packages/hub/src/components/OwnerLink.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { FC } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { GroupLink } from "./GroupLink"; -import { UsernameLink } from "./UsernameLink"; - -import { OwnerLinkFragment$key } from "@/__generated__/OwnerLinkFragment.graphql"; - -export const OwnerLinkFragment = graphql` - fragment OwnerLinkFragment on Owner { - __typename - id - slug - } -`; - -export const OwnerLink: FC<{ ownerRef: OwnerLinkFragment$key }> = ({ - ownerRef, -}) => { - const owner = useFragment(OwnerLinkFragment, ownerRef); - switch (owner.__typename) { - case "User": - return ; - case "Group": - return ; - default: - throw new Error(`Unknown owner type ${owner.__typename}`); - } -}; diff --git a/packages/hub/src/components/ReactRoot.tsx b/packages/hub/src/components/ReactRoot.tsx index 33fdba82a3..f40fbfbd85 100644 --- a/packages/hub/src/components/ReactRoot.tsx +++ b/packages/hub/src/components/ReactRoot.tsx @@ -1,11 +1,9 @@ "use client"; import { FC, PropsWithChildren } from "react"; -import { RelayEnvironmentProvider } from "react-relay"; import { WithToasts } from "@quri/ui"; import { ErrorBoundary } from "@/components/ErrorBoundary"; -import { getCurrentEnvironment } from "@/relay/environment"; import { ExitConfirmationWrapper } from "./ExitConfirmationWrapper"; @@ -22,8 +20,6 @@ export const ReactRoot: FC = ({ children, confirmationWrapper = true, }) => { - const environment = getCurrentEnvironment(); - let content = ( {children} @@ -34,9 +30,5 @@ export const ReactRoot: FC = ({ content = {content}; } - return ( - - {content} - - ); + return content; }; diff --git a/packages/hub/src/components/WithAuth/index.tsx b/packages/hub/src/components/WithAuth/index.tsx index 536015374c..fcdccc2993 100644 --- a/packages/hub/src/components/WithAuth/index.tsx +++ b/packages/hub/src/components/WithAuth/index.tsx @@ -3,8 +3,8 @@ import "server-only"; import { FC, PropsWithChildren } from "react"; import { auth } from "@/auth"; -import { isRootEmail, isSignedIn } from "@/graphql/helpers/userHelpers"; import { prisma } from "@/prisma"; +import { isRootEmail, isSignedIn } from "@/server/users/auth"; import { RedirectToLogin } from "./RedirectToLogin"; diff --git a/packages/hub/src/components/exports/EditRelativeValueExports.tsx b/packages/hub/src/components/exports/EditRelativeValueExports.tsx index cf3ae1efd0..ed1fc3e510 100644 --- a/packages/hub/src/components/exports/EditRelativeValueExports.tsx +++ b/packages/hub/src/components/exports/EditRelativeValueExports.tsx @@ -3,6 +3,7 @@ import { useForm } from "react-hook-form"; import { Button, TextFormField } from "@quri/ui"; +import { RelativeValuesExportInput } from "@/app/models/[owner]/[slug]/EditSquiggleSnippetModel"; import { modelForRelativeValuesExportRoute, relativeValuesRoute, @@ -17,8 +18,6 @@ import { StyledDefinitionLink } from "../ui/StyledDefinitionLink"; import { StyledLink } from "../ui/StyledLink"; import { SelectRelativeValuesDefinition } from "./SelectRelativeValuesDefinition"; -import { RelativeValuesExportInput } from "@/__generated__/EditSquiggleSnippetModelMutation.graphql"; - const CreateVariableWithDefinitionModal: FC<{ close: () => void; append: (item: RelativeValuesExportInput) => void; diff --git a/packages/hub/src/components/exports/RelativeValuesExportItem.tsx b/packages/hub/src/components/exports/RelativeValuesExportItem.tsx deleted file mode 100644 index c32acd7b80..0000000000 --- a/packages/hub/src/components/exports/RelativeValuesExportItem.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { FC, PropsWithChildren } from "react"; -import { useFragment } from "react-relay"; -import { graphql } from "relay-runtime"; - -import { relativeValuesRoute } from "@/routes"; - -import { StyledDefinitionLink } from "../ui/StyledDefinitionLink"; - -import { RelativeValuesExportItem$key } from "@/__generated__/RelativeValuesExportItem.graphql"; - -export const RelativeValuesExportItemFragment = graphql` - fragment RelativeValuesExportItem on RelativeValuesExport { - id - variableName - definition { - id - owner { - id - slug - } - slug - } - } -`; - -const Container: FC = ({ children }) => ( -
-
- {children} -
-
-); - -const RawItem: FC = () => Raw view; - -type Props = { - itemRef?: RelativeValuesExportItem$key; -}; - -const NonEmptyItem: FC> = ({ itemRef }) => { - const item = useFragment(RelativeValuesExportItemFragment, itemRef); - - return ( - - {item.variableName} →{" "} - - {item.definition.owner.slug}/{item.definition.slug} - - - ); -}; - -export const RelativeValuesExportItem: FC = ({ itemRef }) => { - return itemRef ? : ; -}; diff --git a/packages/hub/src/components/layout/RootLayout/PageFooter.tsx b/packages/hub/src/components/layout/RootLayout/PageFooter.tsx index 230c2d9614..fb062c63b2 100644 --- a/packages/hub/src/components/layout/RootLayout/PageFooter.tsx +++ b/packages/hub/src/components/layout/RootLayout/PageFooter.tsx @@ -1,7 +1,6 @@ import Image from "next/image"; import { FC } from "react"; import { FaDiscord, FaGithub, FaRss } from "react-icons/fa"; -import { SiGraphql } from "react-icons/si"; import { Link } from "@/components/ui/Link"; import { @@ -11,12 +10,7 @@ import { QURI_DONATE_URL, } from "@/lib/common"; import logoPic from "@/public/logo.png"; -import { - aboutRoute, - graphqlPlaygroundRoute, - privacyPolicyRoute, - termsOfServiceRoute, -} from "@/routes"; +import { aboutRoute, privacyPolicyRoute, termsOfServiceRoute } from "@/routes"; const linkClasses = "items-center flex hover:text-gray-900"; @@ -73,10 +67,6 @@ export const PageFooter: FC = () => { Newsletter - - - API -
); diff --git a/packages/hub/src/components/ui/MutationAction.tsx b/packages/hub/src/components/ui/MutationAction.tsx deleted file mode 100644 index 98991e1c1a..0000000000 --- a/packages/hub/src/components/ui/MutationAction.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { FC, ReactNode } from "react"; -import { UseMutationConfig } from "react-relay"; -import { GraphQLTaggedNode, OperationType, VariablesOf } from "relay-runtime"; - -import { DropdownMenuAsyncActionItem, IconProps } from "@quri/ui"; - -import { - CommonMutationParameters, - useAsyncMutation, -} from "@/hooks/useAsyncMutation"; - -type MaybeLazyVariablesOf = - | VariablesOf - // Note that this is less type-safe than variables object; extra values will be ignored. - // TODO: I think this might be possible to fix with more advanced Typescript. - // We could make this type a generic over function's return type and then compare it against - // actual `VariablesOf` and throw `never` if there are extra keys). - // Until that is fixed, plain `VariablesOf` should be preferred. - | (() => VariablesOf); - -function resolveVariables( - variables: MaybeLazyVariablesOf -): VariablesOf { - if (typeof variables === "function") { - return variables(); - } else { - return variables; - } -} - -export function MutationAction< - TMutation extends CommonMutationParameters = never, - const TTypename extends string = never, ->({ - mutation, - variables, - updater, - expectedTypename, - title, - icon, - close, -}: { - mutation: GraphQLTaggedNode; - variables: MaybeLazyVariablesOf; - updater?: UseMutationConfig["updater"]; - expectedTypename: TTypename; - title: string; - icon?: FC; - close: () => void; -}): ReactNode { - const [runMutation] = useAsyncMutation({ - mutation, - expectedTypename, - }); - - const act = async () => { - await runMutation({ - variables: resolveVariables(variables), - updater, - }); - }; - - return ( - - ); -} diff --git a/packages/hub/src/components/ui/MutationButton.tsx b/packages/hub/src/components/ui/MutationButton.tsx deleted file mode 100644 index aa72c15fb3..0000000000 --- a/packages/hub/src/components/ui/MutationButton.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { ReactNode } from "react"; -import { GraphQLTaggedNode } from "relay-runtime"; - -import { Button } from "@quri/ui"; - -import { - CommonMutationParameters, - useAsyncMutation, - UseAsyncMutationAct, -} from "@/hooks/useAsyncMutation"; -import { useAsync } from "react-select/async"; - -/* - * Props for this component include: - * - some props that are passed to ` - ); -} diff --git a/packages/hub/src/components/ui/MutationModalAction.tsx b/packages/hub/src/components/ui/MutationModalAction.tsx deleted file mode 100644 index ef5282c771..0000000000 --- a/packages/hub/src/components/ui/MutationModalAction.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { FC, PropsWithChildren, ReactNode } from "react"; -import { DefaultValues, FieldPath, FieldValues } from "react-hook-form"; -import { GraphQLTaggedNode, VariablesOf } from "relay-runtime"; - -import { DropdownMenuModalActionItem, IconProps } from "@quri/ui"; - -import { FormModal } from "@/components/ui/FormModal"; -import { CommonMutationParameters } from "@/hooks/useAsyncMutation"; -import { useMutationForm } from "@/hooks/useMutationForm"; - -type CommonProps< - TMutation extends CommonMutationParameters, - TFormShape extends FieldValues, - TTypename extends string, -> = { - mutation: GraphQLTaggedNode; - expectedTypename: TTypename; - formDataToVariables: (data: TFormShape) => VariablesOf; - initialFocus?: FieldPath; - defaultValues?: DefaultValues; - submitText: string; - onCompleted?: ( - data: Extract - ) => void; - close: () => void; -}; - -function MutationFormModal< - TMutation extends CommonMutationParameters, - TFormShape extends FieldValues, - const TTypename extends string, ->({ - mutation, - expectedTypename, - formDataToVariables, - initialFocus, - defaultValues, - submitText, - onCompleted, - close, - title, - children, -}: PropsWithChildren> & { - title: string; -}): ReactNode { - const { form, onSubmit, inFlight } = useMutationForm< - TFormShape, - TMutation, - TTypename - >({ - mode: "onChange", - defaultValues, - mutation, - expectedTypename, - formDataToVariables, - onCompleted(data) { - onCompleted?.(data); - close(); - }, - }); - - return ( - - {children} - - ); -} - -export function MutationModalAction< - TMutation extends CommonMutationParameters, - TFormShape extends FieldValues, - const TTypename extends string = string, ->({ - modalTitle, - title, - icon, - children, - ...modalProps -}: CommonProps & { - modalTitle: string; - title: string; - icon?: FC; - children: () => ReactNode; -}): ReactNode { - return ( - ( - - // Note that we pass the same `close` that's responsible for closing the dropdown. - {...modalProps} - title={modalTitle} - > - {children()} - - )} - /> - ); -} diff --git a/packages/hub/src/graphql/builder.ts b/packages/hub/src/graphql/builder.ts deleted file mode 100644 index 78947acdfc..0000000000 --- a/packages/hub/src/graphql/builder.ts +++ /dev/null @@ -1,132 +0,0 @@ -import SchemaBuilder from "@pothos/core"; -import ErrorsPlugin from "@pothos/plugin-errors"; -import PrismaPlugin from "@pothos/plugin-prisma"; -import type PrismaTypes from "@pothos/plugin-prisma/generated"; -import RelayPlugin from "@pothos/plugin-relay"; -import ScopeAuthPlugin from "@pothos/plugin-scope-auth"; -import SimpleObjectsPlugin from "@pothos/plugin-simple-objects"; -import ValidationPlugin from "@pothos/plugin-validation"; -import WithInputPlugin from "@pothos/plugin-with-input"; -import { Session } from "next-auth"; -import { NextRequest } from "next/server"; - -import { prisma } from "@/prisma"; - -import { - getMyMembershipById, - getMyMembershipBySlug, -} from "./helpers/groupHelpers"; -import { isRootEmail } from "./helpers/userHelpers"; - -type Context = { - session: Session | null; - request: NextRequest; -}; - -export type SignedInSession = Session & { - user: NonNullable & { email: string }; -}; - -export type HubSchemaTypes = { - PrismaTypes: PrismaTypes; - DefaultEdgesNullability: false; - Context: Context; - AuthScopes: { - signedIn: boolean; - isRootUser: boolean; - isGroupAdmin: string; - isGroupAdminBySlug: string; - controlsOwnerId: string; - }; - AuthContexts: { - // https://pothos-graphql.dev/docs/plugins/scope-auth#change-context-types-based-on-scopes - signedIn: Context & { - session: SignedInSession; - }; - isRootUser: Context & { - session: SignedInSession; - }; - }; - DefaultAuthStrategy: "all"; -}; - -export const builder = new SchemaBuilder({ - plugins: [ - // this plugin comes before auth plugin; see also: https://github.com/hayes/pothos/issues/464 - ErrorsPlugin, // https://pothos-graphql.dev/docs/plugins/errors - ScopeAuthPlugin, // https://pothos-graphql.dev/docs/plugins/scope-auth - SimpleObjectsPlugin, // https://pothos-graphql.dev/docs/plugins/simple-objects - WithInputPlugin, // https://pothos-graphql.dev/docs/plugins/with-input - PrismaPlugin, // https://pothos-graphql.dev/docs/plugins/prisma - RelayPlugin, // https://pothos-graphql.dev/docs/plugins/relay - ValidationPlugin, // https://pothos-graphql.dev/docs/plugins/validation - ], - prisma: { - client: prisma, - }, - relayOptions: { - clientMutationId: "omit", - cursorType: "String", - edgesFieldOptions: { - nullable: false, - }, - }, - scopeAuthOptions: { - defaultStrategy: "all", - }, - authScopes: async (context) => ({ - signedIn: !!context.session?.user, - isRootUser: () => { - const email = context.session?.user.email; - // Note: there's no emailVerified field in session, is this a problem? Probably not. - // See also: `isRootUser` function in `types/User.ts`. - return !!(email && isRootEmail(email)); - }, - isGroupAdmin: async (groupId) => { - const myMembership = await getMyMembershipById(groupId, context.session); - return myMembership?.role === "Admin"; - }, - isGroupAdminBySlug: async (groupSlug) => { - const myMembership = await getMyMembershipBySlug( - groupSlug, - context.session - ); - return myMembership?.role === "Admin"; - }, - controlsOwnerId: async (ownerId) => { - if (!context.session) { - return false; - } - return Boolean( - await prisma.owner.count({ - where: { - id: ownerId, - OR: [ - { - user: { email: context.session.user.email }, - }, - { - group: { - memberships: { - some: { - user: { - email: context.session.user.email, - }, - }, - }, - }, - }, - ], - }, - }) - ); - }, - }), - errorOptions: { - defaultTypes: [Error], - directResult: true, - }, -}); - -builder.queryType({}); -builder.mutationType({}); diff --git a/packages/hub/src/graphql/errors/BaseError.ts b/packages/hub/src/graphql/errors/BaseError.ts deleted file mode 100644 index ebe7c0315e..0000000000 --- a/packages/hub/src/graphql/errors/BaseError.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { builder } from "@/graphql/builder"; - -import { ErrorInterface } from "./common"; - -// This error is a default, always available when `errors: {}` is enabled. -// See also: https://pothos-graphql.dev/docs/plugins/errors#recommended-usage -builder.objectType(Error, { - name: "BaseError", - interfaces: [ErrorInterface], -}); diff --git a/packages/hub/src/graphql/errors/NotFoundError.ts b/packages/hub/src/graphql/errors/NotFoundError.ts deleted file mode 100644 index 13e6a8160f..0000000000 --- a/packages/hub/src/graphql/errors/NotFoundError.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { builder } from "@/graphql/builder"; - -import { ErrorInterface } from "./common"; - -export class NotFoundError extends Error { - constructor(message?: string) { - super(message); - } -} - -builder.objectType(NotFoundError, { - name: "NotFoundError", - interfaces: [ErrorInterface], -}); diff --git a/packages/hub/src/graphql/errors/ValidationError.ts b/packages/hub/src/graphql/errors/ValidationError.ts deleted file mode 100644 index 14fe63140f..0000000000 --- a/packages/hub/src/graphql/errors/ValidationError.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ZodError, ZodIssue } from "zod"; - -import { builder } from "../builder"; -import { ErrorInterface } from "./common"; - -const ValidationErrorIssue = builder - .objectRef("ValidationErrorIssue") - .implement({ - fields: (t) => ({ - message: t.exposeString("message"), - path: t.stringList({ - resolve: (obj) => obj.path.map((item) => String(item)), - }), - }), - }); - -builder.objectType(ZodError, { - name: "ValidationError", - interfaces: [ErrorInterface], - // TODO - patch ZodError.message to be human-readable - fields: (t) => ({ - issues: t.field({ - type: [ValidationErrorIssue], - resolve: (obj) => obj.issues, - }), - message: t.string({ - resolve: (obj) => { - return obj.issues - .map((issue) => `[${issue.path.join(".")}] ${issue.message}`) - .join("\n"); - }, - }), - }), -}); diff --git a/packages/hub/src/graphql/errors/common.ts b/packages/hub/src/graphql/errors/common.ts deleted file mode 100644 index 285d8b6d3a..0000000000 --- a/packages/hub/src/graphql/errors/common.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { builder } from "@/graphql/builder"; - -export const ErrorInterface = builder.interfaceRef("Error").implement({ - fields: (t) => ({ - message: t.exposeString("message"), - }), -}); - -export { rethrowOnConstraint } from "@/server/utils"; diff --git a/packages/hub/src/graphql/helpers/groupHelpers.ts b/packages/hub/src/graphql/helpers/groupHelpers.ts deleted file mode 100644 index 05445169ea..0000000000 --- a/packages/hub/src/graphql/helpers/groupHelpers.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Session } from "next-auth"; - -import { prisma } from "@/prisma"; - -import { getSelf, isSignedIn } from "./userHelpers"; - -export async function getMyMembershipById( - groupId: string, - session: Session | null -) { - if (!isSignedIn(session)) { - return null; - } - const self = await getSelf(session); - const myMembership = await prisma.userGroupMembership.findUnique({ - where: { - userId_groupId: { - groupId: groupId, - userId: self.id, - }, - }, - }); - return myMembership; -} - -export async function getMyMembershipBySlug( - groupSlug: string, - session: Session | null -) { - if (!isSignedIn(session)) { - return null; - } - const self = await getSelf(session); - const myMembership = await prisma.userGroupMembership.findFirst({ - where: { - userId: self.id, - group: { - asOwner: { - slug: groupSlug, - }, - }, - }, - }); - return myMembership; -} - -export async function getMembership({ - groupSlug, - userSlug, -}: { - groupSlug: string; - userSlug: string; -}) { - const membership = await prisma.userGroupMembership.findFirst({ - where: { - user: { - asOwner: { - slug: userSlug, - }, - }, - group: { - asOwner: { - slug: groupSlug, - }, - }, - }, - }); - return membership; -} - -export async function getMyMembership({ - groupSlug, - session, -}: { - groupSlug: string; - session: Session | null; -}) { - if (!isSignedIn(session)) { - return null; - } - const self = await getSelf(session); - const myMembership = await prisma.userGroupMembership.findFirst({ - where: { - userId: self.id, - group: { - asOwner: { - slug: groupSlug, - }, - }, - }, - }); - return myMembership; -} diff --git a/packages/hub/src/graphql/helpers/modelHelpers.ts b/packages/hub/src/graphql/helpers/modelHelpers.ts index e7a59651a9..df4111b345 100644 --- a/packages/hub/src/graphql/helpers/modelHelpers.ts +++ b/packages/hub/src/graphql/helpers/modelHelpers.ts @@ -3,35 +3,6 @@ import { Session } from "next-auth"; import { prisma } from "@/prisma"; -import { NotFoundError } from "../errors/NotFoundError"; - -export function modelWhereHasAccess( - session: Session | null -): Prisma.ModelWhereInput { - const orParts: Prisma.ModelWhereInput[] = [{ isPrivate: false }]; - if (session) { - orParts.push({ - owner: { - OR: [ - { - user: { email: session.user.email }, - }, - { - group: { - memberships: { - some: { - user: { email: session.user.email }, - }, - }, - }, - }, - ], - }, - }); - } - return { OR: orParts }; -} - export async function getWriteableModel({ session, owner, @@ -68,10 +39,8 @@ export async function getWriteableModel({ include, }); if (!model) { - // FIXME - this will happen if permissions are not sufficient - // It would be better to throw a custom PermissionError - // (Note that we should throw PermissionError only if model is readable, but not writeable; otherwise it should still be "Can't find") - throw new NotFoundError("Can't find model"); + // this might happen if permissions are not sufficient + throw new Error("Can't find model"); } return model; } diff --git a/packages/hub/src/graphql/helpers/ownerHelpers.ts b/packages/hub/src/graphql/helpers/ownerHelpers.ts deleted file mode 100644 index 74eef0510b..0000000000 --- a/packages/hub/src/graphql/helpers/ownerHelpers.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Session } from "next-auth"; - -import { prisma } from "@/prisma"; - -export async function getWriteableOwnerBySlug(session: Session, slug: string) { - const owner = await prisma.owner.findFirst({ - where: { - slug, - OR: [ - { - group: { - memberships: { - some: { - user: { - email: session.user.email, - }, - }, - }, - }, - }, - { - user: { - email: session.user.email, - }, - }, - ], - }, - }); - if (!owner) { - // TODO - better error if membership test failed - throw new Error("Can't find owner"); - } - return owner; -} - -// deprecated, need to migrate to getWriteableOwnerBySlug everywhere -export async function getWriteableOwner( - session: Session, - groupSlug?: string | null | undefined -) { - const owner = await prisma.owner.findFirst({ - where: { - ...(groupSlug - ? { - slug: groupSlug, - group: { - memberships: { - some: { - user: { - email: session.user.email, - }, - }, - }, - }, - } - : { - user: { - email: session.user.email, - }, - }), - }, - }); - if (!owner) { - // TODO - better error if membership test failed - throw new Error("Can't find owner"); - } - return owner; -} diff --git a/packages/hub/src/graphql/helpers/userHelpers.ts b/packages/hub/src/graphql/helpers/userHelpers.ts deleted file mode 100644 index b92d01e977..0000000000 --- a/packages/hub/src/graphql/helpers/userHelpers.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { User } from "@prisma/client"; -import { Session } from "next-auth"; - -import { prisma } from "@/prisma"; - -import { SignedInSession } from "../builder"; - -export function isSignedIn( - session: Session | null -): session is SignedInSession { - return Boolean(session?.user.email); -} - -export async function getSelf(session: SignedInSession) { - const user = await prisma.user.findUniqueOrThrow({ - where: { email: session.user.email }, - }); - return user; -} - -const ROOT_EMAILS = (process.env["ROOT_EMAILS"] ?? "").split(","); - -export function isRootEmail(email: string) { - return ROOT_EMAILS.includes(email); -} - -export async function isRootUser(user: User) { - // see also: `isRootUser` auth scope in builder.ts - return Boolean(user.email && user.emailVerified && isRootEmail(user.email)); -} diff --git a/packages/hub/src/graphql/mutations/adminUpdateModelVersion.ts b/packages/hub/src/graphql/mutations/adminUpdateModelVersion.ts deleted file mode 100644 index 5b23054488..0000000000 --- a/packages/hub/src/graphql/mutations/adminUpdateModelVersion.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { getSelf } from "../helpers/userHelpers"; -import { Model } from "../types/Model"; -import { decodeGlobalIdWithTypename } from "../utils"; - -builder.mutationField("adminUpdateModelVersion", (t) => - t.withAuth({ signedIn: true }).fieldWithInput({ - description: "Admin-only query for upgrading model versions", - type: builder.simpleObject("AdminUpdateModelVersionResult", { - fields: (t) => ({ - model: t.field({ type: Model }), - }), - }), - errors: {}, - authScopes: { - isRootUser: true, - }, - input: { - modelId: t.input.string({ required: true }), - version: t.input.string({ required: true }), - }, - resolve: async (_, { input }, { session }) => { - const decodedModelId = decodeGlobalIdWithTypename(input.modelId, "Model"); - const self = await getSelf(session); - - const model = await prisma.$transaction(async (tx) => { - let model = await prisma.model.findUniqueOrThrow({ - where: { id: decodedModelId }, - include: { - currentRevision: { - include: { - squiggleSnippet: true, - relativeValuesExports: true, - }, - }, - }, - }); - if (!model.currentRevision) { - throw new Error("currentRevision is missing"); - } - if ( - model.currentRevision.contentType !== "SquiggleSnippet" || - !model.currentRevision.squiggleSnippet - ) { - throw new Error("Not a Squiggle model"); - } - - const revision = await tx.modelRevision.create({ - data: { - squiggleSnippet: { - create: { - code: model.currentRevision.squiggleSnippet.code, - version: input.version, - seed: model.currentRevision.squiggleSnippet.seed, - }, - }, - contentType: "SquiggleSnippet", - model: { - connect: { id: model.id }, - }, - author: { - connect: { email: self.email! }, - }, - comment: `Automated upgrade from ${model.currentRevision.squiggleSnippet.version} to ${input.version}`, - relativeValuesExports: { - createMany: { - data: model.currentRevision.relativeValuesExports.map( - (exp) => ({ - variableName: exp.variableName, - definitionId: exp.definitionId, - }) - ), - }, - }, - }, - include: { - model: { - select: { - id: true, - }, - }, - }, - }); - - return await tx.model.update({ - where: { - id: revision.model.id, - }, - data: { - currentRevisionId: revision.id, - updatedAt: model.updatedAt, - }, - // TODO - optimize with queryFromInfo, https://pothos-graphql.dev/docs/plugins/prisma#optimized-queries-without-tprismafield - }); - }); - - return { model }; - }, - }) -); diff --git a/packages/hub/src/graphql/queries/group.ts b/packages/hub/src/graphql/queries/group.ts deleted file mode 100644 index f3bdbe5599..0000000000 --- a/packages/hub/src/graphql/queries/group.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { NotFoundError } from "../errors/NotFoundError"; - -builder.queryField("group", (t) => - t.prismaField({ - type: "Group", - args: { - slug: t.arg.string({ required: true }), - }, - errors: { - types: [NotFoundError], - }, - async resolve(query, _, args) { - const group = await prisma.group.findFirst({ - ...query, - where: { - asOwner: { - slug: args.slug, - }, - }, - }); - if (!group) { - throw new NotFoundError(); - } - return group; - }, - }) -); diff --git a/packages/hub/src/graphql/queries/model.ts b/packages/hub/src/graphql/queries/model.ts deleted file mode 100644 index 534c32d39f..0000000000 --- a/packages/hub/src/graphql/queries/model.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { NotFoundError } from "../errors/NotFoundError"; -import { modelWhereHasAccess } from "../helpers/modelHelpers"; - -builder.queryField("model", (t) => - t.prismaFieldWithInput({ - type: "Model", - input: { - slug: t.input.string({ required: true }), - owner: t.input.string({ required: true }), - }, - errors: { - types: [NotFoundError], - }, - async resolve(query, _, { input }, { session }) { - const model = await prisma.model.findFirst({ - ...query, - where: { - slug: input.slug, - owner: { slug: input.owner }, - // intentionally checking access - see https://github.com/quantified-uncertainty/squiggle/issues/3414 - ...modelWhereHasAccess(session), - }, - }); - if (!model) { - throw new NotFoundError(); - } - return model; - }, - }) -); diff --git a/packages/hub/src/graphql/queries/models.ts b/packages/hub/src/graphql/queries/models.ts deleted file mode 100644 index 8c5d778046..0000000000 --- a/packages/hub/src/graphql/queries/models.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { modelWhereHasAccess } from "../helpers/modelHelpers"; -import { Model, ModelConnection } from "../types/Model"; - -builder.queryField("models", (t) => - t.prismaConnection( - { - type: Model, - cursor: "id", - resolve: (query, _, __, { session }) => { - return prisma.model.findMany({ - ...query, - orderBy: { - updatedAt: "desc", - }, - where: modelWhereHasAccess(session), - }); - }, - }, - ModelConnection - ) -); diff --git a/packages/hub/src/graphql/queries/relativeValuesDefinition.ts b/packages/hub/src/graphql/queries/relativeValuesDefinition.ts deleted file mode 100644 index edc14662e4..0000000000 --- a/packages/hub/src/graphql/queries/relativeValuesDefinition.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { NotFoundError } from "../errors/NotFoundError"; - -builder.queryField("relativeValuesDefinition", (t) => - t.prismaFieldWithInput({ - type: "RelativeValuesDefinition", - input: { - slug: t.input.string({ required: true }), - owner: t.input.string({ required: true }), - }, - errors: { - types: [NotFoundError], - }, - async resolve(query, _, args) { - const definition = await prisma.relativeValuesDefinition.findFirst({ - ...query, - where: { - slug: args.input.slug, - owner: { slug: args.input.owner }, - }, - }); - if (!definition) { - throw new NotFoundError(); - } - return definition; - }, - }) -); diff --git a/packages/hub/src/graphql/queries/relativeValuesDefinitions.ts b/packages/hub/src/graphql/queries/relativeValuesDefinitions.ts deleted file mode 100644 index 2e80d2827a..0000000000 --- a/packages/hub/src/graphql/queries/relativeValuesDefinitions.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { - RelativeValuesDefinition, - RelativeValuesDefinitionConnection, -} from "../types/RelativeValuesDefinition"; - -const RelativeValuesDefinitionsQueryInput = builder.inputType( - "RelativeValuesDefinitionsQueryInput", - { - fields: (t) => ({ - slugContains: t.string(), - owner: t.string(), - }), - } -); - -builder.queryField("relativeValuesDefinitions", (t) => - t.prismaConnection( - { - type: RelativeValuesDefinition, - cursor: "id", - args: { - input: t.arg({ type: RelativeValuesDefinitionsQueryInput }), - }, - resolve: (query, _, args) => - prisma.relativeValuesDefinition.findMany({ - ...query, - where: { - ...(args.input?.slugContains && { - slug: { - contains: args.input.slugContains, - mode: "insensitive", - }, - }), - ...(args.input?.owner && { - owner: { slug: args.input.owner }, - }), - }, - orderBy: { updatedAt: "desc" }, - }), - }, - RelativeValuesDefinitionConnection - ) -); diff --git a/packages/hub/src/graphql/queries/userByUsername.ts b/packages/hub/src/graphql/queries/userByUsername.ts deleted file mode 100644 index a542567b29..0000000000 --- a/packages/hub/src/graphql/queries/userByUsername.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { NotFoundError } from "../errors/NotFoundError"; -import { User } from "../types/User"; - -builder.queryField("userByUsername", (t) => - t.prismaField({ - type: User, - args: { - username: t.arg.string({ required: true }), - }, - errors: { - types: [NotFoundError], - }, - async resolve(query, _, args) { - const user = await prisma.user.findFirst({ - ...query, - where: { - asOwner: { - slug: args.username, - }, - }, - }); - if (!user) { - throw new NotFoundError(`User ${args.username} not found`); - } - return user; - }, - }) -); diff --git a/packages/hub/src/graphql/queries/variable.ts b/packages/hub/src/graphql/queries/variable.ts deleted file mode 100644 index f1d8e888b8..0000000000 --- a/packages/hub/src/graphql/queries/variable.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { NotFoundError } from "../errors/NotFoundError"; - -builder.queryField("variable", (t) => - t.prismaFieldWithInput({ - type: "Variable", - input: { - variableName: t.input.string({ required: true }), - slug: t.input.string({ required: true }), - owner: t.input.string({ required: true }), - }, - errors: { - types: [NotFoundError], - }, - async resolve(query, _, { input }) { - const model = await prisma.model.findFirst({ - where: { - slug: input.slug, - owner: { slug: input.owner }, - }, - }); - - if (!model) { - throw new NotFoundError(); - } - - const variable = await prisma.variable.findFirst({ - ...query, - where: { - variableName: input.variableName, - modelId: model.id, - }, - }); - - if (!variable) { - throw new NotFoundError(); - } - - return variable; - }, - }) -); diff --git a/packages/hub/src/graphql/schema.ts b/packages/hub/src/graphql/schema.ts deleted file mode 100644 index 0c04f4d3d7..0000000000 --- a/packages/hub/src/graphql/schema.ts +++ /dev/null @@ -1,18 +0,0 @@ -import "./errors/BaseError"; -import "./errors/NotFoundError"; -import "./errors/ValidationError"; -import "./queries/group"; -import "./queries/model"; -import "./queries/variable"; -import "./queries/relativeValuesDefinition"; -import "./queries/relativeValuesDefinitions"; -import "./queries/userByUsername"; -import "./mutations/adminUpdateModelVersion"; -import "../server/search/actions/adminRebuildSearchIndexAction"; -import "../server/relative-values/actions/buildRelativeValuesCacheAction"; -import "../server/relative-values/actions/clearRelativeValuesCacheAction"; -import "../server/models/actions/updateSquiggleSnippetModelAction"; - -import { builder } from "./builder"; - -export const schema = builder.toSchema(); diff --git a/packages/hub/src/graphql/types/Group.ts b/packages/hub/src/graphql/types/Group.ts deleted file mode 100644 index 774f51bf70..0000000000 --- a/packages/hub/src/graphql/types/Group.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { prismaConnectionHelpers } from "@pothos/plugin-prisma"; -import { MembershipRole } from "@prisma/client"; - -import { builder } from "../builder"; -import { getMyMembershipById } from "../helpers/groupHelpers"; -import { modelWhereHasAccess } from "../helpers/modelHelpers"; -import { GroupInvite, GroupInviteConnection } from "./GroupInvite"; -import { ModelConnection, modelConnectionHelpers } from "./Model"; -import { Owner } from "./Owner"; -import { variableRevisionConnectionHelpers } from "./VariableRevision"; - -export const MembershipRoleType = builder.enumType(MembershipRole, { - name: "MembershipRole", -}); - -export const UserGroupMembership = builder.prismaNode("UserGroupMembership", { - id: { field: "id" }, - fields: (t) => ({ - role: t.field({ - type: MembershipRoleType, - resolve: (t) => t.role, - }), - user: t.relation("user"), - group: t.relation("group"), - }), -}); - -export const UserGroupMembershipConnection = builder.connectionObject({ - type: UserGroupMembership, - name: "UserGroupMembershipConnection", -}); - -export const Group = builder.prismaNode("Group", { - id: { field: "id" }, - interfaces: [Owner], - fields: (t) => ({ - slug: t.string({ - select: { asOwner: true }, - resolve: (group) => group.asOwner.slug, - }), - createdAtTimestamp: t.float({ - resolve: (group) => group.createdAt.getTime(), - }), - updatedAtTimestamp: t.float({ - resolve: (group) => group.updatedAt.getTime(), - }), - reusableInviteToken: t.exposeString("reusableInviteToken", { - nullable: true, - unauthorizedResolver: () => null, - authScopes: (group) => ({ - isGroupAdmin: group.id, - }), - }), - myMembership: t.field({ - type: UserGroupMembership, - nullable: true, - resolve: async (root, _, { session }) => { - // TODO - `prismaField`? - // TODO - this causes an extra query, optimize somehow? - return getMyMembershipById(root.id, session); - }, - }), - // Note: I also tried `members` field with membership data exposed on the connection edges, but had issues with it: - // 1. Edge ids were not Relay ids - // 2. I couldn't make Pothos accept an interface for edge type, which caused duplication in GraphQL types. - // Maybe Pothos will improve the support for it in the future: https://pothos-graphql.dev/docs/plugins/prisma#indirect-relations-as-connections - memberships: t.relatedConnection( - "memberships", - { cursor: "id" }, - UserGroupMembershipConnection - ), - invites: t.relatedConnection( - "invites", - { - cursor: "id", - nullable: true, // "null" means "forbidden" - authScopes: (group) => ({ - // It would be nice to select membership at top level of Group object, - // since this is also useful for `myMembership` field. - // But unfortunately Prisma doesn't have select aliases: - // https://github.com/prisma/prisma/discussions/14316 - // https://github.com/prisma/prisma/issues/8151 - isGroupAdmin: group.id, - }), - unauthorizedResolver: () => null, - query: () => ({ - orderBy: { - createdAt: "desc", - }, - where: { - status: "Pending", - }, - }), - }, - GroupInviteConnection - ), - inviteForMe: t.withAuth({ signedIn: true }).field({ - type: GroupInvite, - nullable: true, - unauthorizedResolver: () => null, - select: (_, { session }) => ({ - invites: { - where: { - user: { - email: session?.user.email, - }, - status: "Pending", - }, - }, - }), - resolve: (group) => group.invites[0], - }), - // Models are stored on owner.models, wo we have to use indirect relation (https://pothos-graphql.dev/docs/plugins/prisma#indirect-relations-as-connections) - // See also: User.models field. - models: t.connection( - { - type: modelConnectionHelpers.ref, - select: (args, ctx, nestedSelection) => ({ - asOwner: { - select: { - models: { - ...modelConnectionHelpers.getQuery(args, ctx, nestedSelection), - where: modelWhereHasAccess(ctx.session), - orderBy: { updatedAt: "desc" }, - }, - }, - }, - }), - resolve: (user, args, ctx) => - modelConnectionHelpers.resolve(user.asOwner.models, args, ctx), - }, - ModelConnection - ), - variableRevisions: t.connection( - { - type: variableRevisionConnectionHelpers.ref, - select: (args, ctx, nestedSelection) => ({ - asOwner: { - select: { - models: { - select: { - currentRevision: { - select: { - variableRevisions: { - ...variableRevisionConnectionHelpers.getQuery( - args, - ctx, - nestedSelection - ), - }, - }, - }, - }, - }, - }, - }, - }), - resolve: (group, args, ctx) => { - const variableRevisions = - group.asOwner?.models - .map((model) => model.currentRevision?.variableRevisions ?? []) - .flat() ?? []; - return variableRevisionConnectionHelpers.resolve( - variableRevisions, - args, - ctx - ); - }, - }, - variableRevisionConnectionHelpers.ref - ), - }), -}); - -export const GroupConnection = builder.connectionObject({ - type: Group, - name: "GroupConnection", -}); - -// useful when we want to expose `groups` field on a `User`, instead of `memberships` -export const groupFromMembershipConnectionHelpers = prismaConnectionHelpers( - builder, - "UserGroupMembership", - { - cursor: "id", - select: (nodeSelection) => ({ - group: nodeSelection(), - }), - resolveNode: (node) => node.group, - } -); diff --git a/packages/hub/src/graphql/types/GroupInvite.ts b/packages/hub/src/graphql/types/GroupInvite.ts deleted file mode 100644 index f402b2793f..0000000000 --- a/packages/hub/src/graphql/types/GroupInvite.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { builder } from "../builder"; -import { MembershipRoleType } from "./Group"; - -export const GroupInvite = builder.prismaInterface("GroupInvite", { - fields: (t) => ({ - id: t.exposeID("id"), - group: t.relation("group"), - role: t.field({ - type: MembershipRoleType, - resolve: (t) => t.role, - }), - }), - resolveType: (invite) => { - if (invite.userId) { - return "UserGroupInvite"; - } else if (invite.email) { - return "EmailGroupInvite"; - } else { - throw new Error("Invalid invite object"); - } - }, -}); - -builder.prismaNode("GroupInvite", { - variant: "UserGroupInvite", - interfaces: [GroupInvite], - id: { field: "id" }, - fields: (t) => ({ - user: t.relation("user"), - }), -}); - -builder.prismaNode("GroupInvite", { - variant: "EmailGroupInvite", - interfaces: [GroupInvite], - id: { field: "id" }, - fields: (t) => ({ - email: t.exposeString("email", { - // Pothos workaround because we guarantee that email invites have non-null email values - nullable: false as any, - }), - }), -}); - -export const GroupInviteConnection = builder.connectionObject({ - type: GroupInvite, - name: "GroupInviteConnection", -}); diff --git a/packages/hub/src/graphql/types/Me.ts b/packages/hub/src/graphql/types/Me.ts deleted file mode 100644 index 7be70238cd..0000000000 --- a/packages/hub/src/graphql/types/Me.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { User } from "./User"; - -export const Me = builder - .objectRef<{ email: string; username?: string | undefined | null }>("Me") - .implement({ - authScopes: { - signedIn: true, - }, - fields: (t) => ({ - email: t.exposeString("email"), - username: t.exposeString("username", { nullable: true }), - asUser: t.withAuth({ signedIn: true }).prismaField({ - type: User, - resolve: async (query, _, __, { session }) => { - return await prisma.user.findUniqueOrThrow({ - ...query, - where: { email: session.user.email }, - }); - }, - }), - }), - }); diff --git a/packages/hub/src/graphql/types/Model.ts b/packages/hub/src/graphql/types/Model.ts deleted file mode 100644 index d08db10789..0000000000 --- a/packages/hub/src/graphql/types/Model.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { prismaConnectionHelpers } from "@pothos/plugin-prisma"; - -import { builder } from "@/graphql/builder"; - -import { decodeGlobalIdWithTypename } from "../utils"; -import { ModelRevision, ModelRevisionConnection } from "./ModelRevision"; -import { Owner } from "./Owner"; - -export const Model = builder.prismaNode("Model", { - id: { field: "id" }, - - authScopes: (model) => { - if (!model.isPrivate) { - return true; - } - - // This might leak the info that the model exists, but we handle that in `model()` query and return NotFoundError. - // It's probable that we leak this info somewhere else, though. - return { controlsOwnerId: model.ownerId }; - }, - - fields: (t) => ({ - slug: t.exposeString("slug"), - // I'm not yet sure if we'll use custom scalars for datetime encoding, so `createdAtTimestamp` is a precaution; we'll probably switch to `createAt` in the future - createdAtTimestamp: t.float({ - resolve: (model) => model.createdAt.getTime(), - }), - updatedAtTimestamp: t.float({ - resolve: (model) => model.updatedAt.getTime(), - }), - owner: t.field({ - type: Owner, - // TODO - we need to extract fragment data from Owner query and call nestedSelection(...) for optimal performance. - select: { - owner: { - include: { - user: true, - group: true, - }, - }, - }, - resolve: (model) => { - const result = model.owner.user ?? model.owner.group; - if (!result) { - throw new Error("Invalid owner object, missing user or group"); - } - // necessary for Owner type - (result as any)["_owner"] = { - type: model.owner.user ? "User" : "Group", - }; - return result; - }, - }), - isPrivate: t.exposeBoolean("isPrivate"), - isEditable: t.boolean({ - authScopes: (model) => ({ - controlsOwnerId: model.ownerId, - }), - resolve: () => true, - unauthorizedResolver: () => false, - }), - currentRevision: t.relation("currentRevision", { - nullable: false, - }), - - revision: t.field({ - type: ModelRevision, - args: { - id: t.arg.id({ required: true }), - }, - select: (args, _, nestedSelection) => { - const id = decodeGlobalIdWithTypename(String(args.id), "ModelRevision"); - - return { - revisions: nestedSelection({ - // necessary for ModelRevision authScopes - include: { model: true }, - take: 1, - where: { id }, - }), - }; - }, - async resolve(model) { - const revision = model.revisions[0]; - if (!revision) { - throw new Error("Not found"); - } - return revision; - }, - }), - revisions: t.relatedConnection( - "revisions", - { - cursor: "id", - query: () => ({ - orderBy: { - createdAt: "desc", - }, - }), - }, - ModelRevisionConnection - ), - variables: t.relation("variables"), - lastRevisionWithBuild: t.field({ - type: ModelRevision, - nullable: true, - select: (args, ctx, nestedSelection) => ({ - revisions: nestedSelection({ - include: { - // required by ModelRevision authScopes - model: true, - }, - orderBy: { - createdAt: "desc", - }, - where: { - builds: { - some: { - id: { - not: undefined, - }, - }, - }, - }, - take: 1, - }), - }), - async resolve(model) { - return model.revisions[0]; - }, - }), - }), -}); - -export const ModelConnection = builder.connectionObject({ - type: Model, - name: "ModelConnection", -}); - -export const modelConnectionHelpers = prismaConnectionHelpers( - builder, - "Model", - { cursor: "id" } -); diff --git a/packages/hub/src/graphql/types/ModelRevision.ts b/packages/hub/src/graphql/types/ModelRevision.ts deleted file mode 100644 index 4c21907f5f..0000000000 --- a/packages/hub/src/graphql/types/ModelRevision.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { UnionRef } from "@pothos/core"; - -import { ASTNode, parse } from "@quri/squiggle-lang"; - -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -import { NotFoundError } from "../errors/NotFoundError"; -import { ModelRevisionBuild } from "./ModelRevisionBuild"; -import { RelativeValuesExport } from "./RelativeValuesExport"; -import { SquiggleSnippet } from "./SquiggleSnippet"; - -// TODO - turn into interface? -const ModelContent: UnionRef< - { - id: string; - code: string; - version: string; - }, - { - id: string; - code: string; - version: string; - } -> = builder.unionType("ModelContent", { - types: [SquiggleSnippet], - resolveType: () => SquiggleSnippet, -}); - -const ModelRevisionBuildStatus = builder.enumType("ModelRevisionBuildStatus", { - values: ["Skipped", "Pending", "Success", "Failure"], -}); - -function astToVariableNames(ast: ASTNode): string[] { - const exportedVariableNames: string[] = []; - - if (ast.kind === "Program") { - ast.statements.forEach((statement) => { - if ( - (statement.kind === "LetStatement" || - statement.kind === "DefunStatement") && - statement.exported - ) { - exportedVariableNames.push(statement.variable.value); - } - }); - } - - return exportedVariableNames; -} - -export function getExportedVariableNames(code: string): string[] { - const ast = parse(code); - if (ast.ok) { - return astToVariableNames(ast.value); - } else { - return []; - } -} - -export const ModelRevision = builder.prismaNode("ModelRevision", { - id: { field: "id" }, - - include: { model: true }, - authScopes: (revision) => { - if (!revision.model.isPrivate) { - return true; - } - return { controlsOwnerId: revision.model.ownerId }; - }, - - fields: (t) => ({ - createdAtTimestamp: t.float({ - resolve: (revision) => revision.createdAt.getTime(), - }), - // `relatedConnection` would be more principled, and in theory the number of variables with definitions could be high. - // But connection is harder to deal with on the UI side, and since we send all variables back on updates, so it doesn't make much sense there. - relativeValuesExports: t.relation("relativeValuesExports"), - variableRevisions: t.relation("variableRevisions"), - model: t.relation("model"), - - lastBuild: t.field({ - type: ModelRevisionBuild, - nullable: true, - select: { - builds: { - orderBy: { - createdAt: "desc", - }, - take: 1, - }, - }, - async resolve(revision) { - return revision.builds[0]; - }, - }), - author: t.relation("author", { nullable: true }), - comment: t.exposeString("comment"), - buildStatus: t.field({ - type: ModelRevisionBuildStatus, - select: { - builds: { - select: { - errors: true, - }, - orderBy: { - createdAt: "desc", - }, - take: 1, - }, - model: { - select: { - currentRevisionId: true, - }, - }, - }, - async resolve(revision) { - const lastBuild = revision.builds[0]; - - if (lastBuild) { - const errors = lastBuild.errors.filter((e) => e !== ""); - return errors.length === 0 ? "Success" : "Failure"; - } - - return revision.model.currentRevisionId === revision.id - ? "Pending" - : "Skipped"; - }, - }), - content: t.field({ - type: ModelContent, - select: { squiggleSnippet: true }, - async resolve(revision) { - switch (revision.contentType) { - case "SquiggleSnippet": - return revision.squiggleSnippet!; - } - }, - }), - exportNames: t.field({ - type: ["String"], - select: { squiggleSnippet: true }, - async resolve(revision) { - if (revision.contentType === "SquiggleSnippet") { - return getExportedVariableNames(revision.squiggleSnippet!.code); - } else { - return []; - } - }, - }), - forRelativeValues: t.fieldWithInput({ - type: RelativeValuesExport, - errors: { - types: [NotFoundError], - }, - input: { - variableName: t.input.string({ required: true }), - // optional, necessary if the variable is associated with multiple definitions - for: t.input.field({ - type: builder.inputType( - "ModelRevisionForRelativeValuesSlugOwnerInput", - { - fields: (t) => ({ - slug: t.string({ required: true }), - owner: t.string({ required: true }), - }), - } - ), - }), - }, - async resolve(revision, { input }) { - const exports = await prisma.relativeValuesExport.findMany({ - where: { - modelRevisionId: revision.id, - variableName: input.variableName, - ...(input.for - ? { - definition: { - owner: { slug: input.for.owner }, - slug: input.for.slug, - }, - } - : {}), - }, - include: { - definition: true, - }, - }); - - if (exports.length > 1) { - throw new Error("Ambiguous input, multiple variables match it"); - } - - if (exports.length === 0) { - throw new NotFoundError(); - } - - return exports[0]; - }, - }), - }), -}); - -export const ModelRevisionConnection = builder.connectionObject({ - type: ModelRevision, - name: "ModelRevisionConnection", -}); diff --git a/packages/hub/src/graphql/types/ModelRevisionBuild.ts b/packages/hub/src/graphql/types/ModelRevisionBuild.ts deleted file mode 100644 index 56c5d7bb39..0000000000 --- a/packages/hub/src/graphql/types/ModelRevisionBuild.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { builder } from "@/graphql/builder"; - -export const ModelRevisionBuild = builder.prismaNode("ModelRevisionBuild", { - id: { field: "id" }, - fields: (t) => ({ - createdAtTimestamp: t.float({ - resolve: (build) => build.createdAt.getTime(), - }), - modelRevision: t.relation("modelRevision"), - errors: t.stringList({ - resolve: (build) => build.errors.filter((e) => e !== ""), - }), - runSeconds: t.exposeFloat("runSeconds"), - }), -}); diff --git a/packages/hub/src/graphql/types/Owner.ts b/packages/hub/src/graphql/types/Owner.ts deleted file mode 100644 index cd6b43ed0a..0000000000 --- a/packages/hub/src/graphql/types/Owner.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { builder } from "../builder"; - -// Note: Owner table is not mapped to GraphQL type. This interface is a proxy for `User` and `Group`. -// common for User and Group -export const Owner = builder.interfaceRef<{ id: string }>("Owner").implement({ - resolveType: (obj) => { - return (obj as any)._owner.type; - }, - fields: (t) => ({ - id: t.exposeID("id"), - slug: t.string(), // implemented on User and Group - }), -}); diff --git a/packages/hub/src/graphql/types/RelativeValuesDefinition.ts b/packages/hub/src/graphql/types/RelativeValuesDefinition.ts deleted file mode 100644 index ab85964354..0000000000 --- a/packages/hub/src/graphql/types/RelativeValuesDefinition.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { prismaConnectionHelpers } from "@pothos/plugin-prisma"; - -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; -import { - relativeValuesClustersSchema, - relativeValuesItemsSchema, -} from "@/relative-values/types"; - -import { modelWhereHasAccess } from "../helpers/modelHelpers"; -import { Owner } from "./Owner"; -import { RelativeValuesExport } from "./RelativeValuesExport"; - -const RelativeValuesCluster = builder.simpleObject("RelativeValuesCluster", { - fields: (t) => ({ - id: t.string(), - color: t.string(), - recommendedUnit: t.string({ nullable: true }), - }), -}); - -const RelativeValuesItem = builder.simpleObject("RelativeValuesItem", { - fields: (t) => ({ - id: t.string(), - name: t.string(), - description: t.string(), - clusterId: t.string({ nullable: true }), - }), -}); - -export const RelativeValuesDefinition = builder.prismaNode( - "RelativeValuesDefinition", - { - id: { field: "id" }, - fields: (t) => ({ - slug: t.exposeString("slug"), - // FIXME - copy-pasted from Model.ts - owner: t.field({ - type: Owner, - // TODO - we need to extract fragment data from Owner query and call nestedSelection(...) for optimal performance. - select: { - owner: { - include: { - user: true, - group: true, - }, - }, - }, - resolve: (model) => { - const result = model.owner.user ?? model.owner.group; - if (!result) { - throw new Error("Invalid owner object, missing user or group"); - } - (result as any)["_owner"] = { - type: model.owner.user ? "User" : "Group", - }; - return result; - }, - }), - isEditable: t.boolean({ - authScopes: (definition) => ({ - controlsOwnerId: definition.ownerId, - }), - resolve: () => true, - unauthorizedResolver: () => false, - }), - createdAtTimestamp: t.float({ - resolve: (obj) => obj.createdAt.getTime(), - }), - updatedAtTimestamp: t.float({ - resolve: (obj) => obj.updatedAt.getTime(), - }), - modelExports: t.field({ - type: [RelativeValuesExport], - resolve: async (definition, _, { session }) => { - const models = await prisma.model.findMany({ - where: { - currentRevision: { - relativeValuesExports: { - some: { - definitionId: definition.id, - }, - }, - }, - ...modelWhereHasAccess(session), - }, - }); - - return await prisma.relativeValuesExport.findMany({ - where: { - modelRevisionId: { - in: models - .map((model) => model.currentRevisionId) - .filter((id) => id !== null), - }, - definitionId: definition.id, - }, - }); - }, - }), - currentRevision: t.relation("currentRevision", { - nullable: false, - }), - }), - } -); - -export const RelativeValuesDefinitionRevision = builder.prismaNode( - "RelativeValuesDefinitionRevision", - { - id: { field: "id" }, - fields: (t) => ({ - title: t.exposeString("title"), - items: t.field({ - type: [RelativeValuesItem], - resolve(obj) { - return relativeValuesItemsSchema.parse(obj.items); - }, - }), - clusters: t.field({ - type: [RelativeValuesCluster], - resolve(obj) { - return relativeValuesClustersSchema.parse(obj.clusters); - }, - }), - recommendedUnit: t.exposeString("recommendedUnit", { nullable: true }), - }), - } -); - -export const RelativeValuesDefinitionConnection = builder.connectionObject({ - type: RelativeValuesDefinition, - name: "RelativeValuesDefinitionConnection", -}); - -export const relativeValuesDefinitionConnectionHelpers = - prismaConnectionHelpers(builder, "RelativeValuesDefinition", { - cursor: "id", - }); diff --git a/packages/hub/src/graphql/types/SquiggleSnippet.ts b/packages/hub/src/graphql/types/SquiggleSnippet.ts deleted file mode 100644 index c2b2cc49da..0000000000 --- a/packages/hub/src/graphql/types/SquiggleSnippet.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { builder } from "@/graphql/builder"; - -export const SquiggleSnippet = builder.prismaNode("SquiggleSnippet", { - id: { field: "id" }, - fields: (t) => ({ - code: t.exposeString("code"), - version: t.exposeString("version"), - seed: t.exposeString("seed"), - autorunMode: t.exposeBoolean("autorunMode", { nullable: true }), - sampleCount: t.exposeInt("sampleCount", { nullable: true }), - xyPointLength: t.exposeInt("xyPointLength", { nullable: true }), - }), -}); diff --git a/packages/hub/src/graphql/types/User.ts b/packages/hub/src/graphql/types/User.ts deleted file mode 100644 index 82db46789d..0000000000 --- a/packages/hub/src/graphql/types/User.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { builder } from "../builder"; -import { modelWhereHasAccess } from "../helpers/modelHelpers"; -import { isRootUser } from "../helpers/userHelpers"; -import { GroupConnection, groupFromMembershipConnectionHelpers } from "./Group"; -import { ModelConnection, modelConnectionHelpers } from "./Model"; -import { Owner } from "./Owner"; -import { - RelativeValuesDefinitionConnection, - relativeValuesDefinitionConnectionHelpers, -} from "./RelativeValuesDefinition"; -import { VariableConnection, variableConnectionHelpers } from "./Variable"; - -export const User = builder.prismaNode("User", { - id: { field: "id" }, - interfaces: [Owner], - fields: (t) => ({ - slug: t.string({ - select: { asOwner: true }, - resolve(user) { - if (!user.asOwner) { - throw new Error("User has no username"); - } - return user.asOwner.slug; - }, - }), - // legacy, alias for user.slug - username: t.string({ - select: { asOwner: true }, - resolve(user) { - if (!user.asOwner) { - throw new Error("User has no username"); - } - return user.asOwner.slug; - }, - }), - // models are stored on owner.models, wo we have to use indirect relation (https://pothos-graphql.dev/docs/plugins/prisma#indirect-relations-as-connections) - // See also: Group.models field. - models: t.connection( - { - type: modelConnectionHelpers.ref, - select: (args, ctx, nestedSelection) => ({ - asOwner: { - select: { - models: { - ...modelConnectionHelpers.getQuery(args, ctx, nestedSelection), - where: modelWhereHasAccess(ctx.session), - orderBy: { updatedAt: "desc" }, - }, - }, - }, - }), - resolve: (user, args, ctx) => - modelConnectionHelpers.resolve(user.asOwner?.models ?? [], args, ctx), - }, - ModelConnection - ), - variables: t.connection( - { - type: variableConnectionHelpers.ref, - select: (args, ctx, nestedSelection) => ({ - asOwner: { - select: { - models: { - where: modelWhereHasAccess(ctx.session), - select: { - variables: variableConnectionHelpers.getQuery( - args, - ctx, - nestedSelection - ), - }, - }, - }, - }, - }), - resolve: (user, args, ctx) => { - const variables = - user.asOwner?.models.map((model) => model.variables ?? []).flat() ?? - []; - return variableConnectionHelpers.resolve(variables, args, ctx); - }, - }, - VariableConnection - ), - relativeValuesDefinitions: t.connection( - { - type: relativeValuesDefinitionConnectionHelpers.ref, - select: (args, ctx, nestedSelection) => ({ - asOwner: { - select: { - relativeValuesDefinitions: { - ...relativeValuesDefinitionConnectionHelpers.getQuery( - args, - ctx, - nestedSelection - ), - orderBy: { updatedAt: "desc" }, - }, - }, - }, - }), - resolve: (user, args, ctx) => - relativeValuesDefinitionConnectionHelpers.resolve( - user.asOwner?.relativeValuesDefinitions ?? [], - args, - ctx - ), - }, - RelativeValuesDefinitionConnection - ), - groups: t.connection( - { - type: groupFromMembershipConnectionHelpers.ref, - - select: (args, ctx, nestedSelection) => ({ - memberships: groupFromMembershipConnectionHelpers.getQuery( - args, - ctx, - nestedSelection - ), - }), - resolve: (user, args, ctx) => - groupFromMembershipConnectionHelpers.resolve( - user.memberships, - args, - ctx - ), - }, - GroupConnection - ), - isRoot: t.boolean({ - authScopes: async (user, _, { session }) => { - return !!( - user.emailVerified && - user.email && - user.email === session?.user.email - ); - }, - resolve: async (user) => isRootUser(user), - }), - isMe: t.boolean({ - resolve: async (user, _, { session }) => - !!(user.email && user.email === session?.user.email), - }), - }), -}); diff --git a/packages/hub/src/graphql/types/Variable.ts b/packages/hub/src/graphql/types/Variable.ts deleted file mode 100644 index 496c6dcfcf..0000000000 --- a/packages/hub/src/graphql/types/Variable.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { prismaConnectionHelpers } from "@pothos/plugin-prisma"; - -import { builder } from "@/graphql/builder"; - -import { Owner } from "./Owner"; -import { VariableRevisionConnection } from "./VariableRevision"; - -export const Variable = builder.prismaNode("Variable", { - id: { field: "id" }, - - include: { model: true }, - authScopes: (variable) => { - if (!variable.model.isPrivate) { - return true; - } - return { controlsOwnerId: variable.model.ownerId }; - }, - - fields: (t) => ({ - variableName: t.exposeString("variableName"), - model: t.relation("model"), - - owner: t.field({ - type: Owner, - select: { - model: { - select: { - owner: { - select: { - user: true, - group: true, - }, - }, - }, - }, - }, - resolve: async ({ model }) => { - const result = model.owner.user ?? model.owner.group; - if (!result) { - throw new Error("Invalid owner object, missing user or group"); - } - (result as any)["_owner"] = { - type: model.owner.user ? "User" : "Group", - }; - return result; - }, - }), - revisions: t.relatedConnection( - "revisions", - { - cursor: "id", - query: () => ({ - orderBy: { - modelRevision: { - createdAt: "desc", - }, - }, - }), - }, - VariableRevisionConnection - ), - currentRevision: t.relation("currentRevision", { - nullable: true, - }), - }), -}); - -export const VariableConnection = builder.connectionObject({ - type: Variable, - name: "VariableConnection", -}); - -export const variableConnectionHelpers = prismaConnectionHelpers( - builder, - "Variable", - { cursor: "id" } -); diff --git a/packages/hub/src/graphql/types/VariableRevision.ts b/packages/hub/src/graphql/types/VariableRevision.ts deleted file mode 100644 index d751cc5d11..0000000000 --- a/packages/hub/src/graphql/types/VariableRevision.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { prismaConnectionHelpers } from "@pothos/plugin-prisma"; - -import { builder } from "@/graphql/builder"; -import { prisma } from "@/prisma"; - -export const VariableRevision = builder.prismaNode("VariableRevision", { - id: { field: "id" }, - - include: { - modelRevision: { - include: { model: true }, - }, - }, - authScopes: (variableRevision) => { - if (!variableRevision.modelRevision.model.isPrivate) { - return true; - } - return { controlsOwnerId: variableRevision.modelRevision.model.ownerId }; - }, - - fields: (t) => ({ - modelRevision: t.relation("modelRevision"), - variableName: t.exposeString("variableName"), - variableType: t.exposeString("variableType"), - docstring: t.exposeString("docstring"), - title: t.exposeString("title", { nullable: true }), - variable: t.relation("variable"), - }), -}); - -export const variableRevisionConnectionHelpers = prismaConnectionHelpers( - builder, - "VariableRevision", - { cursor: "id" } -); - -export const VariableRevisionConnection = builder.connectionObject({ - type: VariableRevision, - name: "VariableRevisionConnection", -}); - -export type VariableRevisionInput = { - title?: string; - variableName: string; - variableType?: string; - docstring?: string; -}; - -export async function createVariableRevision( - modelId: string, - revisionId: string, - variableData: VariableRevisionInput -) { - const variable = await prisma.variable.findFirst({ - where: { - modelId: modelId, - variableName: variableData.variableName, - }, - }); - - let variableId: string; - if (!variable) { - const createdVariable = await prisma.variable.create({ - data: { - model: { connect: { id: modelId } }, - variableName: variableData.variableName, - }, - }); - variableId = createdVariable.id; - } else { - variableId = variable.id; - } - - const createdVariableRevision = await prisma.variableRevision.create({ - data: { - variableName: variableData.variableName, - variable: { connect: { id: variableId } }, - modelRevision: { connect: { id: revisionId } }, - variableType: variableData.variableType, - title: variableData.title, - docstring: variableData.docstring, - }, - }); - - await prisma.variable.update({ - where: { id: variableId }, - data: { - currentRevisionId: createdVariableRevision.id, - }, - }); -} diff --git a/packages/hub/src/graphql/utils.ts b/packages/hub/src/graphql/utils.ts deleted file mode 100644 index e0df031923..0000000000 --- a/packages/hub/src/graphql/utils.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { decodeGlobalID } from "@pothos/plugin-relay"; -import { ValidationOptions } from "@pothos/plugin-validation"; - -export const validateSlug: ValidationOptions = { - regex: [/^\w[\w\-]*$/, { message: "Must be alphanumerical" }], -}; - -export function decodeGlobalIdWithTypename( - id: string, - typenames: string | string[] // TODO - union based on Pothos types? -) { - // wrapper around decodeGlobalID from Pothos Relay Plugin - const { typename: decodedTypename, id: decodedId } = decodeGlobalID(id); - - if (typeof typenames === "string") { - typenames = [typenames]; - } - if (!typenames.includes(decodedTypename)) { - throw new Error( - `Expected ${typenames.join("|")} id, got: ${decodedTypename}` - ); - } - - return decodedId; -} diff --git a/packages/hub/src/hooks/useAsyncMutation.ts b/packages/hub/src/hooks/useAsyncMutation.ts deleted file mode 100644 index 2b5c0a0436..0000000000 --- a/packages/hub/src/hooks/useAsyncMutation.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { useState } from "react"; -import { useMutation, UseMutationConfig } from "react-relay"; -import { GraphQLTaggedNode, MutationParameters } from "relay-runtime"; - -import { useToast } from "@quri/ui"; - -export type CommonMutationParameters = - MutationParameters & { - response: { - readonly result: - | { - readonly __typename: "BaseError"; - readonly message: string; - } - | { - readonly __typename: TTypename; - } - | { - readonly __typename: "%other"; - }; - }; - }; - -type UseAsyncMutationOkResult< - TMutation extends CommonMutationParameters, - TTypename extends string = string, -> = Extract; - -export type UseAsyncMutationAct< - TMutation extends CommonMutationParameters, - TTypename extends string = string, -> = ( - config: Omit, "onCompleted" | "onError"> & { - onCompleted?: ( - okResult: UseAsyncMutationOkResult - ) => void; - } -) => Promise; - -/** - * Like the basic `useMutation` from Relay, this function returns a `[runMutation, isMutationInFlight]` pair. - * But unlike `useMutation`, returned `runMutation` is async and has more convenient `onCompleted` callback (it receives only an expected fragment, unwrapped from the result union). - * Also, all errors will be displayed as notifications automatically. - */ -export function useAsyncMutation< - TMutation extends CommonMutationParameters, - TTypename extends string = string, ->({ - mutation, - confirmation, - expectedTypename, - blockOnSuccess, -}: { - mutation: GraphQLTaggedNode; - confirmation?: string; - expectedTypename: TTypename; - blockOnSuccess?: boolean; // mark mutation as in flight on success; useful if `onCompleted` callback does `router.push` -}) { - const toast = useToast(); - - const [runMutation, inFlight] = useMutation(mutation); - const [wasCompleted, setWasCompleted] = useState(false); - - type OkResult = UseAsyncMutationOkResult; - - const act: UseAsyncMutationAct = (config) => { - return new Promise((resolve, reject) => { - runMutation({ - ...config, - onCompleted(response) { - if (response.result.__typename === expectedTypename) { - if (confirmation !== undefined) { - toast(confirmation, "confirmation"); - } - setWasCompleted(true); - config.onCompleted?.(response.result as OkResult); - resolve(); - } else if ( - // TODO - would `endsWith('Error')` be ok? - // Or can be ask Relay for all types that implement Error interface somehow? - response.result.__typename === "BaseError" || - response.result.__typename === "ValidationError" - ) { - toast( - // This is more cautious than necessary, simple casting to any would be fine too, but it doesn't hurt. - (response.result as { message?: string })?.message ?? - "Internal error", - "error" - ); - reject(); - } else { - toast("Unexpected response type", "error"); - reject(); - } - }, - updater(store, response) { - if (!response) { - return; - } - if (response.result.__typename !== expectedTypename) { - return; - } - config.updater?.(store, response); - }, - onError(e) { - toast(e.toString(), "error"); - resolve(); - }, - }); - }); - }; - - return [act, inFlight || Boolean(blockOnSuccess && wasCompleted)] as const; -} diff --git a/packages/hub/src/hooks/useMutationForm.ts b/packages/hub/src/hooks/useMutationForm.ts deleted file mode 100644 index 6b8ebd83fc..0000000000 --- a/packages/hub/src/hooks/useMutationForm.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { BaseSyntheticEvent, useCallback } from "react"; -import { FieldValues, useForm, UseFormProps } from "react-hook-form"; -import { VariablesOf } from "relay-runtime"; - -import { CommonMutationParameters, useAsyncMutation } from "./useAsyncMutation"; - -/** - * This hook ties together `useForm` and `useAsyncMutation`, which is a very common pattern for forms that submit data to our backend through mutations. - * It forwards some of its parameters to `useForm`, and others to `useAsyncMutation`, and converts form's data to mutation's variables with `formDataToVariables` function. - * - * See also: - * - `` if your form is available through a Dropdown menu - * - `useAsyncMutation` if your mutation doesn't require a form, and for the details on parameters for this function - * - * All generic type parameters to this function default to `never`, so you'll have to set them explicitly to pass type checks. - */ -export function useMutationForm< - FormShape extends FieldValues = never, - TMutation extends CommonMutationParameters = never, - TTypename extends string = never, - // In some cases, you might want to pass extra data to the mutation, which isn't available in `formDataToVariables`. - // See `SaveButton` implementation in `EditSquiggleSnippetModel` for an example where it's needed. - // Longer comment: https://github.com/quantified-uncertainty/squiggle/pull/2498#discussion_r1385665972 - ExtraData extends Record = Record, ->({ - // useForm params - defaultValues, - mode, - // useAsyncMutation params - mutation, - expectedTypename, - blockOnSuccess, - confirmation, - // runMutation params - onCompleted, - // bridge from form to runMutation - formDataToVariables, -}: { - // This is unfortunately not strictly type-safe: if you return extra variables that are not needed for mutation, TypeScript won't complain. - // See also: https://stackoverflow.com/questions/72111571/typescript-exact-return-type-of-function - // This could be solved by converting the return type to generic, but I expect that the lack of partial type parameters in TypeScript - // would get in the way, so I won't even try. - formDataToVariables: ( - data: FormShape, - extraData?: ExtraData - ) => VariablesOf; -} & Pick, "defaultValues" | "mode"> & - Pick< - Parameters>[0], - // I could just pass everything but I'm worried about potential name collisions, and it's easy to whitelist everything necessary. - // We have to list all these when we pass them to `useAsyncMutation`, anyway. - "mutation" | "expectedTypename" | "blockOnSuccess" | "confirmation" - > & - // TODO - with `useAsyncMutation`, submitted data is available because `onCompleted` is usually a closure over it. - // For `useMutationForm`, it'd be useful to pass it to `onCompleted` explicitly, `onCompleted(result, submittedData)`. - Pick< - Parameters>[0]>[0], - "onCompleted" - >) { - const form = useForm({ defaultValues, mode }); - - const [runMutation, inFlight] = useAsyncMutation({ - mutation, - expectedTypename, - blockOnSuccess, - confirmation, - }); - - const onSubmit = useCallback( - (event?: BaseSyntheticEvent, extraData?: ExtraData) => - form.handleSubmit((formData) => { - runMutation({ - variables: formDataToVariables(formData, extraData), - onCompleted, - }); - })(event), - [form, formDataToVariables, onCompleted, runMutation] - ); - - return { form, onSubmit, inFlight }; -} diff --git a/packages/hub/src/lib/common.ts b/packages/hub/src/lib/common.ts index d3e5142866..2023ae8c9e 100644 --- a/packages/hub/src/lib/common.ts +++ b/packages/hub/src/lib/common.ts @@ -5,7 +5,6 @@ export const DISCORD_URL = "https://discord.gg/nsTnQTgtG6"; export const GITHUB_URL = "https://github.com/quantified-uncertainty/squiggle"; export const GITHUB_DISCUSSION_URL = "https://github.com/quantified-uncertainty/squiggle/discussions"; -export const GRAPHQL_URL = "https://squigglehub.org/api/graphql"; export const NEWSLETTER_URL = "https://quri.substack.com/t/squiggle"; export const QURI_DONATE_URL = "https://quantifieduncertainty.org/donate"; diff --git a/packages/hub/src/prisma.ts b/packages/hub/src/prisma.ts index 7d7daad671..2f04ee9e17 100644 --- a/packages/hub/src/prisma.ts +++ b/packages/hub/src/prisma.ts @@ -1,7 +1,5 @@ -/* - * TODO - it would be good to `import "server-only"` here, as a precaution, but - * this interferes with `tsx ./src/graphql/print-schema.ts`. - */ +import "server-only"; + import { PrismaClient } from "@prisma/client"; declare global { diff --git a/packages/hub/src/relative-values/components/views/ListView/index.tsx b/packages/hub/src/relative-values/components/views/ListView/index.tsx index 1b6c75a336..e7ec94c852 100644 --- a/packages/hub/src/relative-values/components/views/ListView/index.tsx +++ b/packages/hub/src/relative-values/components/views/ListView/index.tsx @@ -19,11 +19,9 @@ import { import { ColumnHeader } from "./ColumnHeader"; import { ItemSideBar } from "./sidebar"; -import { RelativeValuesDefinitionRevision$data } from "@/__generated__/RelativeValuesDefinitionRevision.graphql"; - type TableProps = { model: ModelEvaluator; - items: RelativeValuesDefinitionRevision$data["items"]; + items: Item[]; showDescriptions: boolean; recommendedUnit: Item; sidebarItems: [Item, Item] | undefined; diff --git a/packages/hub/src/relative-values/components/views/PlotView/ForcePlot.tsx b/packages/hub/src/relative-values/components/views/PlotView/ForcePlot.tsx index 80ff04f6df..1f3588ca63 100644 --- a/packages/hub/src/relative-values/components/views/PlotView/ForcePlot.tsx +++ b/packages/hub/src/relative-values/components/views/PlotView/ForcePlot.tsx @@ -47,7 +47,7 @@ export const ForcePlot: FC<{ id: string; x: number; y: number; - clusterId: string | null; + clusterId: string | null | undefined; name: string; }; diff --git a/packages/hub/src/relative-values/components/views/hooks/useFilteredItems.ts b/packages/hub/src/relative-values/components/views/hooks/useFilteredItems.ts index 1e29b4a446..d234a67b54 100644 --- a/packages/hub/src/relative-values/components/views/hooks/useFilteredItems.ts +++ b/packages/hub/src/relative-values/components/views/hooks/useFilteredItems.ts @@ -1,14 +1,14 @@ import { useMemo } from "react"; -import { AxisConfig } from "../RelativeValuesProvider"; +import { Item } from "@/relative-values/types"; -import { RelativeValuesDefinitionRevision$data } from "@/__generated__/RelativeValuesDefinitionRevision.graphql"; +import { AxisConfig } from "../RelativeValuesProvider"; export const useFilteredItems = ({ items, config, }: { - items: RelativeValuesDefinitionRevision$data["items"]; + items: Item[]; config: AxisConfig; }) => { return useMemo(() => { diff --git a/packages/hub/src/relative-values/types.ts b/packages/hub/src/relative-values/types.ts index f12ec64255..e66566b479 100644 --- a/packages/hub/src/relative-values/types.ts +++ b/packages/hub/src/relative-values/types.ts @@ -1,17 +1,5 @@ import { z } from "zod"; -import { RelativeValuesDefinitionRevision$data } from "@/__generated__/RelativeValuesDefinitionRevision.graphql"; - -export type Cluster = RelativeValuesDefinitionRevision$data["clusters"][0]; - -// TODO - should be Map -export type Clusters = { - [k: string]: Cluster; -}; - -export type Item = RelativeValuesDefinitionRevision$data["items"][0]; - -// used in GraphQL schema export const relativeValuesClustersSchema = z.array( z.object({ id: z.string(), @@ -28,3 +16,12 @@ export const relativeValuesItemsSchema = z.array( description: z.string().default(""), }) ); + +export type Cluster = z.infer[number]; + +// TODO - should be Map +export type Clusters = { + [k: string]: Cluster; +}; + +export type Item = z.infer[number]; diff --git a/packages/hub/src/relative-values/values/ModelEvaluator.ts b/packages/hub/src/relative-values/values/ModelEvaluator.ts index d414769624..baf86aecee 100644 --- a/packages/hub/src/relative-values/values/ModelEvaluator.ts +++ b/packages/hub/src/relative-values/values/ModelEvaluator.ts @@ -8,6 +8,8 @@ import { SqStringValue, } from "@quri/squiggle-lang"; +import { RelativeValuesExportFullDTO } from "@/server/relative-values/data/fullExport"; + import { cartesianProduct } from "../lib/utils"; import { RelativeValue, @@ -16,8 +18,6 @@ import { relativeValueSchema, } from "./types"; -import { RelativeValuesExport$data } from "@/__generated__/RelativeValuesExport.graphql"; - export const extractOkValues = (items: result[]): A[] => { return items .filter((item): item is { ok: true; value: A } => item.ok) @@ -101,7 +101,7 @@ export class ModelEvaluator { static async create( modelCode: string, variableName: string, - cache?: RelativeValuesExport$data["cache"] + cache?: RelativeValuesExportFullDTO["cache"] ): Promise> { // TODO - versioned-components const project = new SqProject({ diff --git a/packages/hub/src/relay/PatchedQueryResponseCache.ts b/packages/hub/src/relay/PatchedQueryResponseCache.ts deleted file mode 100644 index fbb244d2d2..0000000000 --- a/packages/hub/src/relay/PatchedQueryResponseCache.ts +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -// Patched for Squiggle Hub with `delete` method and converted to Typescript. - -import invariant from "invariant"; -import { - GraphQLResponse, - GraphQLSingularResponse, - Variables, -} from "relay-runtime"; - -type Response = { - fetchTime: number; - payload: GraphQLResponse; -}; - -/** - * Creates a copy of the provided value, ensuring any nested objects have their - * keys sorted such that equivalent values would have identical JSON.stringify - * results. - */ -function stableCopy(value: unknown): unknown { - if (!value || typeof value !== "object") { - return value; - } - if (Array.isArray(value)) { - return value.map(stableCopy); - } - const keys = Object.keys(value).sort(); - const stable: Record = {}; - for (let i = 0; i < keys.length; i++) { - stable[keys[i]] = stableCopy((value as Record)[keys[i]]); - } - return stable; -} - -/** - * A cache for storing query responses, featuring: - * - `get` with TTL - * - cache size limiting, with least-recently *updated* entries purged first - */ -export class PatchedQueryResponseCache { - _responses: Map; - _size: number; - _ttl: number; - - constructor({ size, ttl }: { size: number; ttl: number }) { - invariant( - size > 0, - "RelayQueryResponseCache: Expected the max cache size to be > 0, got " + - "`%s`.", - size - ); - invariant( - ttl > 0, - "RelayQueryResponseCache: Expected the max ttl to be > 0, got `%s`.", - ttl - ); - this._responses = new Map(); - this._size = size; - this._ttl = ttl; - } - - clear(): void { - this._responses.clear(); - } - - get(queryID: string, variables: Variables): GraphQLResponse | null { - const cacheKey = getCacheKey(queryID, variables); - this._responses.forEach((response, key) => { - if (!isCurrent(response.fetchTime, this._ttl)) { - this._responses.delete(key); - } - }); - const response = this._responses.get(cacheKey); - if (response == null) { - return null; - } - - const { payload } = response; - - if (Array.isArray(payload)) { - return payload.map( - (payloadItem) => - ({ - ...payloadItem, - extensions: { - ...payloadItem.extensions, - cacheTimestamp: response.fetchTime, - }, - }) satisfies GraphQLSingularResponse - ); - } - - const singularPayload = payload as GraphQLSingularResponse; - - return { - ...singularPayload, - extensions: { - ...singularPayload.extensions, - cacheTimestamp: response.fetchTime, - }, - } satisfies GraphQLSingularResponse; - } - - set(queryID: string, variables: Variables, payload: GraphQLResponse): void { - const fetchTime = Date.now(); - const cacheKey = getCacheKey(queryID, variables); - this._responses.delete(cacheKey); // deletion resets key ordering - this._responses.set(cacheKey, { - fetchTime, - payload, - }); - // Purge least-recently updated key when max size reached - if (this._responses.size > this._size) { - const firstKey = this._responses.keys().next(); - if (!firstKey.done) { - this._responses.delete(firstKey.value); - } - } - } - - delete(queryID: string, variables: Variables) { - const cacheKey = getCacheKey(queryID, variables); - this._responses.delete(cacheKey); - } -} - -function getCacheKey(queryID: string, variables: Variables): string { - return JSON.stringify(stableCopy({ queryID, variables })); -} - -/** - * Determine whether a response fetched at `fetchTime` is still valid given - * some `ttl`. - */ -function isCurrent(fetchTime: number, ttl: number): boolean { - return fetchTime + ttl >= Date.now(); -} diff --git a/packages/hub/src/relay/environment.ts b/packages/hub/src/relay/environment.ts deleted file mode 100644 index 6a207e4ce2..0000000000 --- a/packages/hub/src/relay/environment.ts +++ /dev/null @@ -1,121 +0,0 @@ -// based on https://github.com/relayjs/relay-examples/blob/main/issue-tracker-next-v13/src/relay/environment.ts -import { - CacheConfig, - Environment, - EnvironmentConfig, - GraphQLResponse, - Network, - RecordSource, - RequestParameters, - Store, - Variables, -} from "relay-runtime"; - -import { VERCEL_URL } from "@/constants"; - -import { PatchedQueryResponseCache } from "./PatchedQueryResponseCache"; - -const IS_SERVER = typeof window === typeof undefined; - -const HTTP_ENDPOINT = IS_SERVER - ? VERCEL_URL - ? `https://${VERCEL_URL}/api/graphql` - : "http://localhost:3001/api/graphql" - : "/api/graphql"; - -const CACHE_TTL = 5 * 1000; // 5 seconds, to resolve preloaded results - -export async function networkFetch( - request: RequestParameters, - variables: Variables, - cookieHeader?: string -): Promise { - const resp = await fetch(HTTP_ENDPOINT, { - cache: "no-store", - method: "POST", - headers: { - Cookie: cookieHeader ?? "", - Accept: "application/json", - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: request.text, - variables, - }), - }); - const json = await resp.json(); - - // GraphQL returns exceptions (for example, a missing required variable) in the "errors" - // property of the response. If any exceptions occurred when processing the request, - // throw an error to indicate to the developer what went wrong. - if (Array.isArray(json.errors)) { - console.error(json.errors); - throw new Error( - `Error fetching GraphQL query '${ - request.name - }' with variables '${JSON.stringify(variables)}': ${JSON.stringify( - json.errors - )}` - ); - } - - return json; -} - -export function createNetwork(responseCache: PatchedQueryResponseCache) { - const fetchResponse = async ( - params: RequestParameters, - variables: Variables, - cacheConfig: CacheConfig - ): Promise => { - const isQuery = params.operationKind === "query"; - const cacheKey = params.id ?? params.cacheID; - if (isQuery && !cacheConfig.force) { - const fromCache = responseCache.get(cacheKey, variables); - if (fromCache) { - return fromCache; - } - } - - // TODO - on server, it might be better to run GraphQL query directly, instead of going through yoga and HTTP - return await networkFetch(params, variables); - }; - - return Network.create(fetchResponse); -} - -export class EnvironmentWithResponseCache extends Environment { - constructor( - config: EnvironmentConfig, - public responseCache: PatchedQueryResponseCache - ) { - super(config); - } -} - -function createEnvironment() { - // We have response cache even on server; isolation is guaranteed by `getCurrentEnvironment()` logic. - // We expose it as `environment.responseCache` so that `useSerializablePreloadedQuery` could access it. - const responseCache = new PatchedQueryResponseCache({ - size: 100, - ttl: CACHE_TTL, - }); - return new EnvironmentWithResponseCache( - { - network: createNetwork(responseCache), - store: new Store(RecordSource.create()), - isServer: IS_SERVER, - }, - responseCache - ); -} - -let environment: EnvironmentWithResponseCache | undefined; -export function getCurrentEnvironment() { - if (IS_SERVER) { - return createEnvironment(); - } - - environment ??= createEnvironment(); - return environment; -} diff --git a/packages/hub/src/relay/loadPageQuery.ts b/packages/hub/src/relay/loadPageQuery.ts deleted file mode 100644 index 938c4d0cc7..0000000000 --- a/packages/hub/src/relay/loadPageQuery.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { headers } from "next/headers"; -// based on https://github.com/relayjs/relay-examples/blob/main/issue-tracker-next-v13/src/relay/loadSerializableQuery.ts -import { - GraphQLResponse, - OperationType, - RequestParameters, - VariablesOf, -} from "relay-runtime"; -import { ConcreteRequest } from "relay-runtime/lib/util/RelayConcreteNode"; - -import { networkFetch } from "./environment"; - -export interface SerializablePreloadedQuery { - params: RequestParameters; - variables: VariablesOf; - response: GraphQLResponse; -} - -// Call into raw network fetch to get serializable GraphQL query response -// This response will be sent to the client to "warm" the QueryResponseCache -// to avoid the client fetches. -export async function loadPageQuery( - node: ConcreteRequest, - variables: VariablesOf -): Promise> { - const cookie = (await headers()).get("cookie"); - const response = await networkFetch( - node.params, - variables, - cookie ?? undefined - ); - return { - params: node.params, - variables, - response, - }; -} diff --git a/packages/hub/src/relay/usePageQuery.ts b/packages/hub/src/relay/usePageQuery.ts deleted file mode 100644 index 0f3ec8faec..0000000000 --- a/packages/hub/src/relay/usePageQuery.ts +++ /dev/null @@ -1,73 +0,0 @@ -// Convert preloaded query object (with raw GraphQL Response) into Relay's PreloadedQuery. - -import { useEffect, useMemo } from "react"; -import { - PreloadedQuery, - usePreloadedQuery, - useQueryLoader, - useRelayEnvironment, -} from "react-relay"; -import { GraphQLTaggedNode, OperationType } from "relay-runtime"; - -import { EnvironmentWithResponseCache } from "./environment"; -import { SerializablePreloadedQuery } from "./loadPageQuery"; - -// This hook convert serializable preloaded query into Relay's PreloadedQuery object. -// It also writes this serializable preloaded query into QueryResponseCache, so that -// the network layer can use these cache results when fetching data in `usePreloadedQuery`. -// -// In addition to the basic implementation from https://github.com/relayjs/relay-examples/tree/main/issue-tracker-next-v13, this hook: -// - combines `useSerializablePreloadedQuery` and `usePreloadedQuery` into one call -// - returns a `reload()` function that can be used together with `useSubscribeToInvalidationState` to reload page data. -export function usePageQuery( - Query: GraphQLTaggedNode, - rscQueryRef: SerializablePreloadedQuery -) { - const environment = useRelayEnvironment() as EnvironmentWithResponseCache; - - const [initialQueryRef, fetchKey] = useMemo(() => { - const fetchKey = rscQueryRef.params.id ?? rscQueryRef.params.cacheID; - - environment.responseCache.set( - fetchKey, - rscQueryRef.variables, - rscQueryRef.response - ); - - const preloadedQuery = { - environment, - fetchKey, - // On the first page load, it will hit responseCache. - // On the following loads (on client-side navigation), responseCache will be cleaned up when the component is unmounted, - // and the page data will be reloaded. - // Note: in dev, this will cause /api/graphql refetch on the client side, because of React strict mode. But it shouldn't do that in production. - fetchPolicy: "store-and-network", - isDisposed: false, - name: rscQueryRef.params.name, - kind: "PreloadedQuery", - variables: rscQueryRef.variables, - dispose: () => {}, - } as PreloadedQuery; - - return [preloadedQuery, fetchKey]; - }, [rscQueryRef, environment]); - - useEffect(() => { - return () => { - environment.responseCache.delete(fetchKey, rscQueryRef.variables); - }; - }, [fetchKey, environment, initialQueryRef, rscQueryRef.variables]); - - const [queryRef, loadQuery] = useQueryLoader(Query); - const result = usePreloadedQuery(Query, queryRef ?? initialQueryRef); - - const reload = () => loadQuery(rscQueryRef.variables); - - useEffect(() => { - return () => { - initialQueryRef.dispose(); - }; - }, [initialQueryRef]); - - return [result, { reload }] as const; -} diff --git a/packages/hub/src/routes.ts b/packages/hub/src/routes.ts index 5640823bf0..17853192df 100644 --- a/packages/hub/src/routes.ts +++ b/packages/hub/src/routes.ts @@ -89,6 +89,21 @@ export function variableRoute({ return `${modelUrl}/variables/${variableName}`; } +export function variableRevisionRoute({ + owner, + modelSlug, + variableName, + revisionId, +}: { + owner: string; + modelSlug: string; + variableName: string; + revisionId: string; +}) { + const modelUrl = modelRoute({ owner, slug: modelSlug }); + return `${modelUrl}/variables/${variableName}/revisions/${revisionId}`; +} + export function modelViewRoute({ username, slug, @@ -183,22 +198,6 @@ export function newGroupRoute() { return "/new/group"; } -export function graphqlAPIRoute() { - return graphqlPlaygroundRoute(); -} - -export function graphqlPlaygroundRoute(query?: string) { - const paramsString = - query === undefined - ? "" - : "?" + - new URLSearchParams({ - query, - }).toString(); - - return `/api/graphql${paramsString}`; -} - export function aboutRoute() { return "/about"; } diff --git a/packages/hub/src/scripts/buildRecentModelRevision/createVariableRevision.ts b/packages/hub/src/scripts/buildRecentModelRevision/createVariableRevision.ts new file mode 100644 index 0000000000..65bc2b2821 --- /dev/null +++ b/packages/hub/src/scripts/buildRecentModelRevision/createVariableRevision.ts @@ -0,0 +1,47 @@ +import { prisma } from "@/prisma"; + +import { type VariableRevisionInput } from "./worker"; + +export async function createVariableRevision( + modelId: string, + revisionId: string, + variableData: VariableRevisionInput +) { + const variable = await prisma.variable.findFirst({ + where: { + modelId: modelId, + variableName: variableData.variableName, + }, + }); + + let variableId: string; + if (!variable) { + const createdVariable = await prisma.variable.create({ + data: { + model: { connect: { id: modelId } }, + variableName: variableData.variableName, + }, + }); + variableId = createdVariable.id; + } else { + variableId = variable.id; + } + + const createdVariableRevision = await prisma.variableRevision.create({ + data: { + variableName: variableData.variableName, + variable: { connect: { id: variableId } }, + modelRevision: { connect: { id: revisionId } }, + variableType: variableData.variableType, + title: variableData.title, + docstring: variableData.docstring, + }, + }); + + await prisma.variable.update({ + where: { id: variableId }, + data: { + currentRevisionId: createdVariableRevision.id, + }, + }); +} diff --git a/packages/hub/src/scripts/buildRecentModelRevision/main.ts b/packages/hub/src/scripts/buildRecentModelRevision/main.ts index afe0793de9..46a78fb84e 100644 --- a/packages/hub/src/scripts/buildRecentModelRevision/main.ts +++ b/packages/hub/src/scripts/buildRecentModelRevision/main.ts @@ -1,9 +1,7 @@ import { PrismaClient } from "@prisma/client"; import { spawn } from "node:child_process"; -import { createVariableRevision } from "@/graphql/types/VariableRevision"; - -import { NotFoundError } from "../../graphql/errors/NotFoundError"; +import { createVariableRevision } from "./createVariableRevision"; import { WorkerOutput, WorkerRunMessage } from "./worker"; const TIMEOUT_SECONDS = 60; // 60 seconds @@ -104,7 +102,7 @@ async function buildRecentModelVersion(): Promise { } if (!model?.currentRevisionId || !model.currentRevision?.squiggleSnippet) { - throw new NotFoundError( + throw new Error( `Unexpected Error: Model revision didn't have needed information. This should never happen.` ); } diff --git a/packages/hub/src/scripts/buildRecentModelRevision/worker.ts b/packages/hub/src/scripts/buildRecentModelRevision/worker.ts index fdd14f54e2..cef0df6bfb 100644 --- a/packages/hub/src/scripts/buildRecentModelRevision/worker.ts +++ b/packages/hub/src/scripts/buildRecentModelRevision/worker.ts @@ -1,7 +1,13 @@ -import { VariableRevisionInput } from "@/graphql/types/VariableRevision"; import { prisma } from "@/prisma"; import { runSquiggle } from "@/server/runSquiggle"; +export type VariableRevisionInput = { + variableName: string; + variableType: string; + title?: string; + docstring: string; +}; + export type WorkerRunMessage = { type: "run"; data: { diff --git a/packages/hub/src/scripts/print-schema.ts b/packages/hub/src/scripts/print-schema.ts deleted file mode 100644 index 309405f643..0000000000 --- a/packages/hub/src/scripts/print-schema.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { writeFileSync } from "fs"; -import { lexicographicSortSchema, printSchema } from "graphql"; -import path from "path"; - -import { schema } from "../graphql/schema"; - -const schemaAsString = printSchema(lexicographicSortSchema(schema)); - -writeFileSync(path.join(__dirname, "../../schema.graphql"), schemaAsString); diff --git a/packages/hub/src/server/groups/actions/acceptReusableGroupInviteTokenAction.ts b/packages/hub/src/server/groups/actions/acceptReusableGroupInviteTokenAction.ts index d091273314..55975d2671 100644 --- a/packages/hub/src/server/groups/actions/acceptReusableGroupInviteTokenAction.ts +++ b/packages/hub/src/server/groups/actions/acceptReusableGroupInviteTokenAction.ts @@ -3,9 +3,9 @@ import { revalidatePath } from "next/cache"; import { z } from "zod"; -import { getMyMembership } from "@/graphql/helpers/groupHelpers"; import { prisma } from "@/prisma"; import { groupMembersRoute } from "@/routes"; +import { getMyMembership } from "@/server/groups/groupHelpers"; import { getSessionOrRedirect } from "@/server/users/auth"; import { makeServerAction, zSlug } from "@/server/utils"; @@ -34,7 +34,6 @@ export const acceptReusableGroupInviteTokenAction = makeServerAction( const myMembership = await getMyMembership({ groupSlug: input.groupSlug, - session, }); if (myMembership) { throw new Error("You're already a member of this group"); diff --git a/packages/hub/src/server/groups/actions/deleteMembershipAction.ts b/packages/hub/src/server/groups/actions/deleteMembershipAction.ts index 80cbb327d6..8dacf40688 100644 --- a/packages/hub/src/server/groups/actions/deleteMembershipAction.ts +++ b/packages/hub/src/server/groups/actions/deleteMembershipAction.ts @@ -3,9 +3,9 @@ import { revalidatePath } from "next/cache"; import { z } from "zod"; -import { getMembership, getMyMembership } from "@/graphql/helpers/groupHelpers"; import { prisma } from "@/prisma"; import { groupMembersRoute } from "@/routes"; +import { getMembership, getMyMembership } from "@/server/groups/groupHelpers"; import { getSessionOrRedirect } from "@/server/users/auth"; import { makeServerAction, zSlug } from "@/server/utils"; @@ -22,7 +22,6 @@ export const deleteMembershipAction = makeServerAction( // somewhat repetitive compared to `updateMembershipRole`, but with slightly different error messages const myMembership = await getMyMembership({ groupSlug: input.group, - session, }); if (!myMembership) { diff --git a/packages/hub/src/server/groups/groupHelpers.ts b/packages/hub/src/server/groups/groupHelpers.ts new file mode 100644 index 0000000000..70c8b76a34 --- /dev/null +++ b/packages/hub/src/server/groups/groupHelpers.ts @@ -0,0 +1,46 @@ +import { auth } from "@/auth"; +import { prisma } from "@/prisma"; + +import { isSignedIn } from "../users/auth"; + +export async function getMembership({ + groupSlug, + userSlug, +}: { + groupSlug: string; + userSlug: string; +}) { + const membership = await prisma.userGroupMembership.findFirst({ + where: { + user: { + asOwner: { + slug: userSlug, + }, + }, + group: { + asOwner: { + slug: groupSlug, + }, + }, + }, + }); + return membership; +} + +export async function getMyMembership({ groupSlug }: { groupSlug: string }) { + const session = await auth(); + if (!isSignedIn(session)) { + return null; + } + const myMembership = await prisma.userGroupMembership.findFirst({ + where: { + userId: session.user.id, + group: { + asOwner: { + slug: groupSlug, + }, + }, + }, + }); + return myMembership; +} diff --git a/packages/hub/src/server/models/actions/adminUpdateModelVersionAction.ts b/packages/hub/src/server/models/actions/adminUpdateModelVersionAction.ts new file mode 100644 index 0000000000..b3da993f11 --- /dev/null +++ b/packages/hub/src/server/models/actions/adminUpdateModelVersionAction.ts @@ -0,0 +1,94 @@ +"use server"; +import { z } from "zod"; + +import { prisma } from "@/prisma"; +import { + checkRootUser, + getSelf, + getSessionOrRedirect, +} from "@/server/users/auth"; +import { makeServerAction } from "@/server/utils"; + +// Admin-only query for upgrading model versions +export const adminUpdateModelVersionAction = makeServerAction( + z.object({ + modelId: z.string(), + version: z.string(), + }), + async (input) => { + await checkRootUser(); + const session = await getSessionOrRedirect(); + + const self = await getSelf(session); + + const model = await prisma.$transaction(async (tx) => { + let model = await prisma.model.findUniqueOrThrow({ + where: { id: input.modelId }, + include: { + currentRevision: { + include: { + squiggleSnippet: true, + relativeValuesExports: true, + }, + }, + }, + }); + if (!model.currentRevision) { + throw new Error("currentRevision is missing"); + } + if ( + model.currentRevision.contentType !== "SquiggleSnippet" || + !model.currentRevision.squiggleSnippet + ) { + throw new Error("Not a Squiggle model"); + } + + const revision = await tx.modelRevision.create({ + data: { + squiggleSnippet: { + create: { + code: model.currentRevision.squiggleSnippet.code, + version: input.version, + seed: model.currentRevision.squiggleSnippet.seed, + }, + }, + contentType: "SquiggleSnippet", + model: { + connect: { id: model.id }, + }, + author: { + connect: { email: self.email! }, + }, + comment: `Automated upgrade from ${model.currentRevision.squiggleSnippet.version} to ${input.version}`, + relativeValuesExports: { + createMany: { + data: model.currentRevision.relativeValuesExports.map((exp) => ({ + variableName: exp.variableName, + definitionId: exp.definitionId, + })), + }, + }, + }, + include: { + model: { + select: { + id: true, + }, + }, + }, + }); + + return await tx.model.update({ + where: { + id: revision.model.id, + }, + data: { + currentRevisionId: revision.id, + updatedAt: model.updatedAt, + }, + }); + }); + + return { model }; + } +); diff --git a/packages/hub/src/server/models/actions/createSquiggleSnippetModelAction.ts b/packages/hub/src/server/models/actions/createSquiggleSnippetModelAction.ts index f329256a61..7411b20b92 100644 --- a/packages/hub/src/server/models/actions/createSquiggleSnippetModelAction.ts +++ b/packages/hub/src/server/models/actions/createSquiggleSnippetModelAction.ts @@ -2,13 +2,11 @@ import { z } from "zod"; -import { rethrowOnConstraint } from "@/graphql/errors/common"; -import { getWriteableOwner } from "@/graphql/helpers/ownerHelpers"; -import { getSelf } from "@/graphql/helpers/userHelpers"; import { prisma } from "@/prisma"; +import { getWriteableOwner } from "@/server/owners/auth"; import { indexModelId } from "@/server/search/helpers"; -import { getSessionOrRedirect } from "@/server/users/auth"; -import { makeServerAction, zSlug } from "@/server/utils"; +import { getSelf, getSessionOrRedirect } from "@/server/users/auth"; +import { makeServerAction, rethrowOnConstraint, zSlug } from "@/server/utils"; export const createSquiggleSnippetModelAction = makeServerAction( z.object({ diff --git a/packages/hub/src/server/models/actions/moveModelAction.ts b/packages/hub/src/server/models/actions/moveModelAction.ts index eccb32d081..893da4898c 100644 --- a/packages/hub/src/server/models/actions/moveModelAction.ts +++ b/packages/hub/src/server/models/actions/moveModelAction.ts @@ -3,8 +3,8 @@ import { z } from "zod"; import { getWriteableModel } from "@/graphql/helpers/modelHelpers"; -import { getWriteableOwnerBySlug } from "@/graphql/helpers/ownerHelpers"; import { prisma } from "@/prisma"; +import { getWriteableOwnerBySlug } from "@/server/owners/auth"; import { getSessionOrRedirect } from "@/server/users/auth"; import { makeServerAction, zSlug } from "@/server/utils"; diff --git a/packages/hub/src/server/models/actions/updateSquiggleSnippetModelAction.ts b/packages/hub/src/server/models/actions/updateSquiggleSnippetModelAction.ts index e1292672da..a598f65754 100644 --- a/packages/hub/src/server/models/actions/updateSquiggleSnippetModelAction.ts +++ b/packages/hub/src/server/models/actions/updateSquiggleSnippetModelAction.ts @@ -7,11 +7,10 @@ import { squiggleVersions } from "@quri/versioned-squiggle-components"; import { prisma } from "@/prisma"; import { modelRoute } from "@/routes"; -import { getSessionOrRedirect } from "@/server/users/auth"; +import { getSelf, getSessionOrRedirect } from "@/server/users/auth"; import { makeServerAction, zSlug } from "@/server/utils"; import { getWriteableModel } from "../../../graphql/helpers/modelHelpers"; -import { getSelf } from "../../../graphql/helpers/userHelpers"; export const updateSquiggleSnippetModelAction = makeServerAction( z.object({ @@ -145,7 +144,6 @@ export const updateSquiggleSnippetModelAction = makeServerAction( data: { currentRevisionId: revision.id, }, - // TODO - optimize with queryFromInfo, https://pothos-graphql.dev/docs/plugins/prisma#optimized-queries-without-tprismafield }); return updatedModel; diff --git a/packages/hub/src/server/models/data/revisions.ts b/packages/hub/src/server/models/data/revisions.ts index 1db49ed1f9..fb9b1a7018 100644 --- a/packages/hub/src/server/models/data/revisions.ts +++ b/packages/hub/src/server/models/data/revisions.ts @@ -5,7 +5,7 @@ import { Paginated } from "@/server/types"; import { modelWhereHasAccess } from "./authHelpers"; -const select = { +export const selectModelRevision = { id: true, createdAt: true, author: { @@ -37,9 +37,13 @@ const select = { type BuildStatus = "Success" | "Failure" | "Pending" | "Skipped"; -export type DbModelRevision = NonNullable< +type DbModelRevision = NonNullable< Awaited< - ReturnType> + ReturnType< + typeof prisma.modelRevision.findFirst<{ + select: typeof selectModelRevision; + }> + > > >; @@ -67,7 +71,9 @@ function buildToDTO(build: DbModelRevisionBuild): ModelRevisionBuildDTO { }; } -function revisionToDTO(dbRevision: DbModelRevision): ModelRevisionDTO { +export function modelRevisionToDTO( + dbRevision: DbModelRevision +): ModelRevisionDTO { const lastBuild = dbRevision.builds[0]; let buildStatus: BuildStatus = "Pending"; if (lastBuild) { @@ -110,7 +116,7 @@ export async function loadModelRevisions(params: { }, cursor: params.cursor ? { id: params.cursor } : undefined, orderBy: { createdAt: "desc" }, - select, + select: selectModelRevision, take: limit + 1, }); @@ -121,7 +127,7 @@ export async function loadModelRevisions(params: { return loadModelRevisions({ ...params, cursor: nextCursor, limit }); } - const revisions = dbRevisions.map(revisionToDTO); + const revisions = dbRevisions.map(modelRevisionToDTO); return { items: revisions.slice(0, limit), diff --git a/packages/hub/src/server/owners/auth.ts b/packages/hub/src/server/owners/auth.ts index 816bb62fa1..9ad4872d0f 100644 --- a/packages/hub/src/server/owners/auth.ts +++ b/packages/hub/src/server/owners/auth.ts @@ -1,3 +1,5 @@ +import { Session } from "next-auth"; + import { auth } from "@/auth"; import { prisma } from "@/prisma"; @@ -31,3 +33,68 @@ export async function controlsOwnerId(ownerId: string): Promise { }) ); } + +export async function getWriteableOwnerBySlug(session: Session, slug: string) { + const owner = await prisma.owner.findFirst({ + where: { + slug, + OR: [ + { + group: { + memberships: { + some: { + user: { + email: session.user.email, + }, + }, + }, + }, + }, + { + user: { + email: session.user.email, + }, + }, + ], + }, + }); + if (!owner) { + // TODO - better error if membership test failed + throw new Error("Can't find owner"); + } + return owner; +} + +// deprecated, need to migrate to getWriteableOwnerBySlug everywhere +export async function getWriteableOwner( + session: Session, + groupSlug?: string | null | undefined +) { + const owner = await prisma.owner.findFirst({ + where: { + ...(groupSlug + ? { + slug: groupSlug, + group: { + memberships: { + some: { + user: { + email: session.user.email, + }, + }, + }, + }, + } + : { + user: { + email: session.user.email, + }, + }), + }, + }); + if (!owner) { + // TODO - better error if membership test failed + throw new Error("Can't find owner"); + } + return owner; +} diff --git a/packages/hub/src/server/relative-values/actions/buildRelativeValuesCacheAction.ts b/packages/hub/src/server/relative-values/actions/buildRelativeValuesCacheAction.ts index d7ec240120..3ec15d2acf 100644 --- a/packages/hub/src/server/relative-values/actions/buildRelativeValuesCacheAction.ts +++ b/packages/hub/src/server/relative-values/actions/buildRelativeValuesCacheAction.ts @@ -10,7 +10,7 @@ import { modelForRelativeValuesExportRoute } from "@/routes"; import { getSessionOrRedirect } from "@/server/users/auth"; import { makeServerAction } from "@/server/utils"; -import { getRelativeValuesExportForWriteableModel } from "../../../graphql/types/RelativeValuesExport"; +import { getRelativeValuesExportForWriteableModel } from "../utils"; export const buildRelativeValuesCacheAction = makeServerAction( z.object({ diff --git a/packages/hub/src/server/relative-values/actions/clearRelativeValuesCacheAction.ts b/packages/hub/src/server/relative-values/actions/clearRelativeValuesCacheAction.ts index 18c1d8151c..66171fbb21 100644 --- a/packages/hub/src/server/relative-values/actions/clearRelativeValuesCacheAction.ts +++ b/packages/hub/src/server/relative-values/actions/clearRelativeValuesCacheAction.ts @@ -5,9 +5,9 @@ import { z } from "zod"; import { prisma } from "@/prisma"; import { modelForRelativeValuesExportRoute } from "@/routes"; -import { getRelativeValuesExportForWriteableModel } from "../../../graphql/types/RelativeValuesExport"; import { getSessionOrRedirect } from "../../users/auth"; import { makeServerAction } from "../../utils"; +import { getRelativeValuesExportForWriteableModel } from "../utils"; export const clearRelativeValuesCacheAction = makeServerAction( z.object({ diff --git a/packages/hub/src/server/relative-values/actions/createRelativeValuesDefinitionAction.ts b/packages/hub/src/server/relative-values/actions/createRelativeValuesDefinitionAction.ts index 4a9b761ecc..9c342dbf57 100644 --- a/packages/hub/src/server/relative-values/actions/createRelativeValuesDefinitionAction.ts +++ b/packages/hub/src/server/relative-values/actions/createRelativeValuesDefinitionAction.ts @@ -1,7 +1,7 @@ "use server"; -import { getWriteableOwnerBySlug } from "@/graphql/helpers/ownerHelpers"; import { prisma } from "@/prisma"; +import { getWriteableOwnerBySlug } from "@/server/owners/auth"; import { indexDefinitionId } from "@/server/search/helpers"; import { getSessionOrRedirect } from "@/server/users/auth"; import { makeServerAction, rethrowOnConstraint } from "@/server/utils"; diff --git a/packages/hub/src/server/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx b/packages/hub/src/server/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx index ba0b0d575f..00005c768f 100644 --- a/packages/hub/src/server/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx +++ b/packages/hub/src/server/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx @@ -2,11 +2,10 @@ import { z } from "zod"; import { prisma } from "@/prisma"; +import { getWriteableOwnerBySlug } from "@/server/owners/auth"; import { getSessionOrRedirect } from "@/server/users/auth"; import { makeServerAction, zSlug } from "@/server/utils"; -import { getWriteableOwnerBySlug } from "../../../graphql/helpers/ownerHelpers"; - export const deleteRelativeValuesDefinitionAction = makeServerAction( z.object({ owner: zSlug, diff --git a/packages/hub/src/server/relative-values/actions/updateRelativeValuesDefinitionAction.ts b/packages/hub/src/server/relative-values/actions/updateRelativeValuesDefinitionAction.ts index 8df413fb26..6ae19827db 100644 --- a/packages/hub/src/server/relative-values/actions/updateRelativeValuesDefinitionAction.ts +++ b/packages/hub/src/server/relative-values/actions/updateRelativeValuesDefinitionAction.ts @@ -1,9 +1,9 @@ "use server"; import { prisma } from "@/prisma"; +import { getWriteableOwnerBySlug } from "@/server/owners/auth"; import { getSessionOrRedirect } from "@/server/users/auth"; import { makeServerAction } from "@/server/utils"; -import { getWriteableOwnerBySlug } from "../../../graphql/helpers/ownerHelpers"; import { inputSchema, validateRelativeValuesDefinition } from "./common"; export const updateRelativeValuesDefinitionAction = makeServerAction( diff --git a/packages/hub/src/server/relative-values/data/exports.ts b/packages/hub/src/server/relative-values/data/exports.ts index 8c2e970626..f0eb4e6741 100644 --- a/packages/hub/src/server/relative-values/data/exports.ts +++ b/packages/hub/src/server/relative-values/data/exports.ts @@ -1,8 +1,7 @@ import { Prisma } from "@prisma/client"; -import { auth } from "@/auth"; -import { modelWhereHasAccess } from "@/graphql/helpers/modelHelpers"; import { prisma } from "@/prisma"; +import { modelWhereHasAccess } from "@/server/models/data/authHelpers"; import { RelativeValuesDefinitionFullDTO } from "./full"; @@ -55,7 +54,6 @@ function toDTO( export async function loadRelativeValuesExportCardsFromDefinition( definition: RelativeValuesDefinitionFullDTO ): Promise { - const session = await auth(); const models = await prisma.model.findMany({ where: { currentRevision: { @@ -65,7 +63,7 @@ export async function loadRelativeValuesExportCardsFromDefinition( }, }, }, - ...modelWhereHasAccess(session), + OR: await modelWhereHasAccess(), }, }); diff --git a/packages/hub/src/graphql/types/RelativeValuesExport.ts b/packages/hub/src/server/relative-values/utils.ts similarity index 56% rename from packages/hub/src/graphql/types/RelativeValuesExport.ts rename to packages/hub/src/server/relative-values/utils.ts index 2d70ebb9ab..f2c6a17281 100644 --- a/packages/hub/src/graphql/types/RelativeValuesExport.ts +++ b/packages/hub/src/server/relative-values/utils.ts @@ -1,10 +1,8 @@ import { Session } from "next-auth"; -import { builder } from "@/graphql/builder"; +import { getWriteableModel } from "@/graphql/helpers/modelHelpers"; import { prisma } from "@/prisma"; -import { getWriteableModel } from "../helpers/modelHelpers"; - export async function getRelativeValuesExportForWriteableModel({ exportId, session, @@ -45,30 +43,3 @@ export async function getRelativeValuesExportForWriteableModel({ return relativeValuesExport; } - -export const RelativeValuesPairCache = builder.prismaNode( - "RelativeValuesPairCache", - { - id: { field: "id" }, - fields: (t) => ({ - firstItem: t.exposeString("firstItem"), - secondItem: t.exposeString("secondItem"), - resultJSON: t.string({ - resolve(obj) { - return JSON.stringify(obj.result); - }, - }), - errorString: t.exposeString("error", { nullable: true }), - }), - } -); - -export const RelativeValuesExport = builder.prismaNode("RelativeValuesExport", { - id: { field: "id" }, - fields: (t) => ({ - definition: t.relation("definition"), - modelRevision: t.relation("modelRevision"), - variableName: t.exposeString("variableName"), - cache: t.relation("cache"), - }), -}); diff --git a/packages/hub/src/server/users/auth.ts b/packages/hub/src/server/users/auth.ts index 7a83e52973..89aa286043 100644 --- a/packages/hub/src/server/users/auth.ts +++ b/packages/hub/src/server/users/auth.ts @@ -1,13 +1,9 @@ -/* - * Helper functions for server-side React components - * TODO: unify these with `graphql/helpers/*` - * (see https://github.com/quantified-uncertainty/squiggle/issues/3154, we plan to migrate away from GraphQL) - */ import "server-only"; +import { User } from "@prisma/client"; +import { Session } from "next-auth"; import { redirect } from "next/navigation"; -import { isRootEmail, isSignedIn } from "@/graphql/helpers/userHelpers"; import { prisma } from "@/prisma"; import { auth } from "../../auth"; @@ -26,7 +22,6 @@ export async function getSessionUserOrRedirect() { } export async function checkRootUser() { - // TODO - unify with src/graphql/helpers const sessionUser = await getSessionUserOrRedirect(); const user = await prisma.user.findUniqueOrThrow({ where: { email: sessionUser.email }, @@ -35,3 +30,30 @@ export async function checkRootUser() { throw new Error("Unauthorized"); } } + +export type SignedInSession = Session & { + user: NonNullable & { email: string }; +}; + +export function isSignedIn( + session: Session | null +): session is SignedInSession { + return Boolean(session?.user.email); +} + +export async function getSelf(session: SignedInSession) { + const user = await prisma.user.findUniqueOrThrow({ + where: { email: session.user.email }, + }); + return user; +} + +const ROOT_EMAILS = (process.env["ROOT_EMAILS"] ?? "").split(","); + +export function isRootEmail(email: string) { + return ROOT_EMAILS.includes(email); +} + +export async function isRootUser(user: User) { + return Boolean(user.email && user.emailVerified && isRootEmail(user.email)); +} diff --git a/packages/hub/src/server/variables/data/fullVariableRevision.ts b/packages/hub/src/server/variables/data/fullVariableRevision.ts new file mode 100644 index 0000000000..041abe58b0 --- /dev/null +++ b/packages/hub/src/server/variables/data/fullVariableRevision.ts @@ -0,0 +1,74 @@ +import { Prisma } from "@prisma/client"; + +import { prisma } from "@/prisma"; +import { modelWhereHasAccess } from "@/server/models/data/authHelpers"; +import { + ModelRevisionFullDTO, + modelRevisionFullToDTO, + selectModelRevisionFull, +} from "@/server/models/data/fullRevision"; + +const select = { + id: true, + variableType: true, + variableName: true, + modelRevision: { + select: selectModelRevisionFull, + }, +} satisfies Prisma.VariableRevisionSelect; + +type Row = NonNullable< + Awaited< + ReturnType< + typeof prisma.variableRevision.findFirst<{ + select: typeof select; + }> + > + > +>; + +export type VariableRevisionFullDTO = { + id: string; + variableType: string; + variableName: string; + modelRevision: ModelRevisionFullDTO; +}; + +async function toDTO(row: Row): Promise { + return { + id: row.id, + variableType: row.variableType, + variableName: row.variableName, + modelRevision: await modelRevisionFullToDTO(row.modelRevision), + }; +} + +export async function loadVariableRevisionFull({ + owner, + slug, + variableName, + revisionId, +}: { + owner: string; + slug: string; + variableName: string; + revisionId: string; +}): Promise { + const row = await prisma.variableRevision.findFirst({ + where: { + id: revisionId, + modelRevision: { + model: { + OR: await modelWhereHasAccess(), + owner: { + slug: owner, + }, + slug, + }, + }, + }, + select, + }); + + return row ? await toDTO(row) : null; +} diff --git a/packages/hub/src/server/variables/data.ts b/packages/hub/src/server/variables/data/variableCards.ts similarity index 71% rename from packages/hub/src/server/variables/data.ts rename to packages/hub/src/server/variables/data/variableCards.ts index 3e62731e6a..9b91b7e4bb 100644 --- a/packages/hub/src/server/variables/data.ts +++ b/packages/hub/src/server/variables/data/variableCards.ts @@ -4,8 +4,8 @@ import { Prisma } from "@prisma/client"; import { prisma } from "@/prisma"; -import { modelWhereHasAccess } from "../models/data/authHelpers"; -import { Paginated } from "../types"; +import { modelWhereHasAccess } from "../../models/data/authHelpers"; +import { Paginated } from "../../types"; const variableCardSelect = { id: true, @@ -36,7 +36,7 @@ const variableCardSelect = { }, } satisfies Prisma.VariableSelect; -type DbVariableCard = NonNullable< +type Row = NonNullable< Awaited< ReturnType< typeof prisma.variable.findFirst<{ @@ -46,12 +46,12 @@ type DbVariableCard = NonNullable< > >; -export function dbVariableToVariableCard(dbVariable: DbVariableCard) { +export function toDTO(dbVariable: Row) { // TODO - upgrade owner, at least return dbVariable; } -export type VariableCardData = ReturnType; +export type VariableCardDTO = ReturnType; export async function loadVariableCards( params: { @@ -59,7 +59,7 @@ export async function loadVariableCards( cursor?: string; limit?: number; } = {} -): Promise> { +): Promise> { const limit = params.limit ?? 20; const dbVariables = await prisma.variable.findMany({ @@ -87,7 +87,7 @@ export async function loadVariableCards( take: limit + 1, }); - const variables = dbVariables.map(dbVariableToVariableCard); + const variables = dbVariables.map(toDTO); const nextCursor = variables[variables.length - 1]?.id; @@ -101,3 +101,27 @@ export async function loadVariableCards( loadMore: variables.length > limit ? loadMore : undefined, }; } + +export async function loadVariableCard({ + owner, + slug, + variableName, +}: { + owner: string; + slug: string; + variableName: string; +}) { + const row = await prisma.variable.findFirst({ + where: { + model: { + OR: await modelWhereHasAccess(), + owner: { slug: owner }, + slug, + }, + variableName, + }, + select: variableCardSelect, + }); + + return row ? toDTO(row) : null; +} diff --git a/packages/hub/src/server/variables/data/variableRevisions.ts b/packages/hub/src/server/variables/data/variableRevisions.ts new file mode 100644 index 0000000000..10da3e4a15 --- /dev/null +++ b/packages/hub/src/server/variables/data/variableRevisions.ts @@ -0,0 +1,88 @@ +import { Prisma } from "@prisma/client"; + +import { prisma } from "@/prisma"; +import { modelWhereHasAccess } from "@/server/models/data/authHelpers"; +import { + ModelRevisionDTO, + modelRevisionToDTO, + selectModelRevision, +} from "@/server/models/data/revisions"; +import { Paginated } from "@/server/types"; + +const select = { + id: true, + variableType: true, + modelRevision: { + select: selectModelRevision, + }, +} satisfies Prisma.VariableRevisionSelect; + +type Row = NonNullable< + Awaited< + ReturnType< + typeof prisma.variableRevision.findFirst<{ + select: typeof select; + }> + > + > +>; + +export type VariableRevisionDTO = { + id: string; + variableType: string; + modelRevision: ModelRevisionDTO; +}; + +function toDTO(row: Row): VariableRevisionDTO { + return { + id: row.id, + variableType: row.variableType, + modelRevision: modelRevisionToDTO(row.modelRevision), + }; +} + +export async function loadVariableRevisions(params: { + owner: string; + slug: string; + variableName: string; + cursor?: string; + limit?: number; +}): Promise> { + const limit = params.limit ?? 20; + + const rows = await prisma.variableRevision.findMany({ + select, + orderBy: { + modelRevision: { + createdAt: "desc", + }, + }, + cursor: params.cursor ? { id: params.cursor } : undefined, + where: { + modelRevision: { + model: { + OR: await modelWhereHasAccess(), + owner: { + slug: params.owner, + }, + slug: params.slug, + }, + }, + }, + take: limit + 1, + }); + + const variables = rows.map(toDTO); + + const nextCursor = variables[variables.length - 1]?.id; + + async function loadMore(limit: number) { + "use server"; + return loadVariableRevisions({ ...params, cursor: nextCursor, limit }); + } + + return { + items: variables.slice(0, limit), + loadMore: variables.length > limit ? loadMore : undefined, + }; +} diff --git a/packages/hub/src/squiggle/components/linker.ts b/packages/hub/src/squiggle/components/linker.ts index 766c471f85..2177012652 100644 --- a/packages/hub/src/squiggle/components/linker.ts +++ b/packages/hub/src/squiggle/components/linker.ts @@ -1,13 +1,10 @@ -import { fetchQuery, graphql } from "relay-runtime"; +import { z } from "zod"; import { SqLinker, SqModule } from "@quri/squiggle-lang"; -import { versionSupportsSqProjectV2 } from "@quri/versioned-squiggle-components"; - -import { getCurrentEnvironment } from "@/relay/environment"; - -import { versionedSquigglePackages } from "../../../../versioned-components/dist/src/versionedSquigglePackages"; - -import { linkerQuery } from "@/__generated__/linkerQuery.graphql"; +import { + versionedSquigglePackages, + versionSupportsSqProjectV2, +} from "@quri/versioned-squiggle-components"; type ParsedSourceId = { owner: string; @@ -45,46 +42,18 @@ const linker: SqLinker = { async loadModule(sourceId: string) { const { owner, slug } = parseSourceId(sourceId); - const environment = getCurrentEnvironment(); - - const result = await fetchQuery( - environment, - graphql` - query linkerQuery($input: QueryModelInput!) { - model(input: $input) { - __typename - ... on Model { - id - currentRevision { - content { - __typename - ... on SquiggleSnippet { - code - } - } - } - } - } - } - `, - { - input: { owner, slug }, - } - // toPromise is discouraged by Relay docs, but should be fine if we don't do any streaming - ).toPromise(); - - if (!result || result.model.__typename !== "Model") { - throw new Error(`Failed to fetch sources for ${sourceId}`); - } + const data = await fetch( + `/api/get-source?owner=${owner}&slug=${slug}` + ).then((res) => res.json()); - const content = result.model.currentRevision.content; - if (content.__typename !== "SquiggleSnippet") { - throw new Error(`${sourceId} is not a SquiggleSnippet`); + const parsed = z.object({ code: z.string() }).safeParse(data); + if (!parsed.success) { + throw new Error(`Failed to fetch source for ${sourceId}`); } return new SqModule({ name: sourceId, - code: content.code, + code: parsed.data.code, }); }, }; diff --git a/packages/hub/src/variables/components/VariableCard.tsx b/packages/hub/src/variables/components/VariableCard.tsx index 6154c918cc..6727afd54c 100644 --- a/packages/hub/src/variables/components/VariableCard.tsx +++ b/packages/hub/src/variables/components/VariableCard.tsx @@ -14,10 +14,10 @@ import { import { Link } from "@/components/ui/Link"; import { exportTypeIcon } from "@/lib/typeIcon"; import { modelRoute, variableRoute } from "@/routes"; -import { VariableCardData } from "@/server/variables/data"; +import { VariableCardDTO } from "@/server/variables/data/variableCards"; type Props = { - variable: VariableCardData; + variable: VariableCardDTO; }; export const VariableCard: FC = ({ variable }) => { diff --git a/packages/hub/src/variables/components/VariableList.tsx b/packages/hub/src/variables/components/VariableList.tsx index 6cb2dc35e9..7777e78adb 100644 --- a/packages/hub/src/variables/components/VariableList.tsx +++ b/packages/hub/src/variables/components/VariableList.tsx @@ -4,12 +4,12 @@ import { FC } from "react"; import { LoadMore } from "@/components/LoadMore"; import { usePaginator } from "@/hooks/usePaginator"; import { Paginated } from "@/server/types"; -import { VariableCardData } from "@/server/variables/data"; +import { VariableCardDTO } from "@/server/variables/data/variableCards"; import { VariableCard } from "./VariableCard"; type Props = { - page: Paginated; + page: Paginated; }; export const VariableList: FC = ({ page: initialPage }) => { diff --git a/packages/hub/test/gql-gen/fragment-masking.ts b/packages/hub/test/gql-gen/fragment-masking.ts deleted file mode 100644 index 194340763d..0000000000 --- a/packages/hub/test/gql-gen/fragment-masking.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { - DocumentTypeDecoration, - ResultOf, - TypedDocumentNode, -} from "@graphql-typed-document-node/core"; -import { FragmentDefinitionNode } from "graphql"; - -import { Incremental } from "./graphql"; - -export type FragmentType> = TDocumentType extends DocumentTypeDecoration< - infer TType, - any -> - ? [TType] extends [{ ' $fragmentName'?: infer TKey }] - ? TKey extends string - ? { ' $fragmentRefs'?: { [key in TKey]: TType } } - : never - : never - : never; - -// return non-nullable if `fragmentType` is non-nullable -export function useFragment( - _documentNode: DocumentTypeDecoration, - fragmentType: FragmentType> -): TType; -// return nullable if `fragmentType` is nullable -export function useFragment( - _documentNode: DocumentTypeDecoration, - fragmentType: FragmentType> | null | undefined -): TType | null | undefined; -// return array of non-nullable if `fragmentType` is array of non-nullable -export function useFragment( - _documentNode: DocumentTypeDecoration, - fragmentType: ReadonlyArray>> -): ReadonlyArray; -// return array of nullable if `fragmentType` is array of nullable -export function useFragment( - _documentNode: DocumentTypeDecoration, - fragmentType: ReadonlyArray>> | null | undefined -): ReadonlyArray | null | undefined; -export function useFragment( - _documentNode: DocumentTypeDecoration, - fragmentType: FragmentType> | ReadonlyArray>> | null | undefined -): TType | ReadonlyArray | null | undefined { - return fragmentType as any; -} - - -export function makeFragmentData< - F extends DocumentTypeDecoration, - FT extends ResultOf ->(data: FT, _fragment: F): FragmentType { - return data as FragmentType; -} -export function isFragmentReady( - queryNode: DocumentTypeDecoration, - fragmentNode: TypedDocumentNode, - data: FragmentType, any>> | null | undefined -): data is FragmentType { - const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ - ?.deferredFields; - - if (!deferredFields) return true; - - const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined; - const fragName = fragDef?.name?.value; - - const fields = (fragName && deferredFields[fragName]) || []; - return fields.length > 0 && fields.every(field => data && field in data); -} diff --git a/packages/hub/test/gql-gen/gql.ts b/packages/hub/test/gql-gen/gql.ts deleted file mode 100644 index 7f6f52bdd7..0000000000 --- a/packages/hub/test/gql-gen/gql.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/core"; - -/* eslint-disable */ -import * as types from "./graphql"; - -/** - * Map of all GraphQL operations in the project. - * - * This map has several performance disadvantages: - * 1. It is not tree-shakeable, so it will include all operations in the project. - * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle. - * 3. It does not support dead code elimination, so it will add unused operations. - * - * Therefore it is highly recommended to use the babel or swc plugin for production. - */ -const documents = { - "\n query Test_MyMembership($slug: String!) {\n result: group(slug: $slug) {\n __typename\n ... on Group {\n id\n myMembership {\n id\n role\n }\n }\n }\n }\n ": types.Test_MyMembershipDocument, - "\n query Test_Memberships($slug: String!) {\n result: group(slug: $slug) {\n __typename\n ... on Group {\n id\n memberships {\n edges {\n node {\n id\n role\n user {\n slug\n }\n }\n }\n }\n }\n }\n }\n ": types.Test_MembershipsDocument, - "\n mutation Test_CreateGroup($input: MutationCreateGroupInput!) {\n result: createGroup(input: $input) {\n __typename\n ... on Error {\n message\n }\n ... on CreateGroupResult {\n group {\n id\n }\n }\n }\n }\n ": types.Test_CreateGroupDocument, - "\n mutation Test_CreateModel(\n $input: MutationCreateSquiggleSnippetModelInput!\n ) {\n result: createSquiggleSnippetModel(input: $input) {\n __typename\n ... on Error {\n message\n }\n ... on CreateSquiggleSnippetModelResult {\n model {\n id\n }\n }\n }\n }\n ": types.Test_CreateModelDocument, - "\n mutation Test_Invite($input: MutationInviteUserToGroupInput!) {\n result: inviteUserToGroup(input: $input) {\n __typename\n ... on BaseError {\n message\n }\n ... on InviteUserToGroupResult {\n invite {\n id\n }\n }\n }\n }\n ": types.Test_InviteDocument, - "\n mutation Test_AcceptInvite($input: MutationReactToGroupInviteInput!) {\n result: reactToGroupInvite(input: $input) {\n __typename\n ... on BaseError {\n message\n }\n ... on ReactToGroupInviteResult {\n __typename\n }\n }\n }\n ": types.Test_AcceptInviteDocument, - "\n mutation CreateGroupTest {\n result: createGroup(input: { slug: \"testgroup\" }) {\n __typename\n ... on CreateGroupResult {\n group {\n id\n slug\n memberships {\n edges {\n node {\n user {\n username\n }\n role\n }\n }\n }\n }\n }\n ... on BaseError {\n message\n }\n }\n }\n": types.CreateGroupTestDocument, - "\n mutation CreateSquiggleSnippetModelTest(\n $input: MutationCreateSquiggleSnippetModelInput!\n ) {\n result: createSquiggleSnippetModel(input: $input) {\n __typename\n ... on Error {\n message\n }\n ... on ValidationError {\n issues {\n message\n }\n }\n ... on CreateSquiggleSnippetModelResult {\n model {\n id\n slug\n isPrivate\n owner {\n __typename\n slug\n }\n }\n }\n }\n }\n": types.CreateSquiggleSnippetModelTestDocument, - "\n mutation DeleteMembershipTest($group: String!, $user: String!) {\n result: deleteMembership(input: { group: $group, user: $user }) {\n __typename\n ... on BaseError {\n message\n }\n ... on DeleteMembershipResult {\n ok\n }\n }\n }\n ": types.DeleteMembershipTestDocument, - "\n mutation DeleteModelTest($input: MutationDeleteModelInput!) {\n result: deleteModel(input: $input) {\n __typename\n ... on Error {\n message\n }\n ... on NotFoundError {\n message\n }\n ... on DeleteModelResult {\n ok\n }\n }\n }\n": types.DeleteModelTestDocument, - "\n mutation InviteTest($input: MutationInviteUserToGroupInput!) {\n result: inviteUserToGroup(input: $input) {\n __typename\n ... on BaseError {\n message\n }\n ... on InviteUserToGroupResult {\n invite {\n id\n role\n }\n }\n }\n }\n ": types.InviteTestDocument, - "\n mutation SetUsernameTest($username: String!) {\n result: setUsername(username: $username) {\n __typename\n ... on Error {\n message\n }\n ... on ValidationError {\n message\n }\n ... on Me {\n email\n username\n }\n }\n }\n": types.SetUsernameTestDocument, - "\n query TestGroups($input: GroupsQueryInput!) {\n result: groups(first: 10, input: $input) {\n __typename\n edges {\n node {\n id\n slug\n }\n }\n }\n }\n ": types.TestGroupsDocument, - "\n query TestMe {\n me {\n __typename\n email\n username\n }\n }\n ": types.TestMeDocument, - "\n query TestModels {\n models {\n edges {\n node {\n slug\n isEditable\n isPrivate\n owner {\n __typename\n slug\n }\n }\n }\n }\n }\n": types.TestModelsDocument, - "\n mutation TestModels_createModel(\n $input: MutationCreateSquiggleSnippetModelInput!\n ) {\n result: createSquiggleSnippetModel(input: $input) {\n __typename\n }\n }\n": types.TestModels_CreateModelDocument, - "\n query TestUserByUsername($username: String!) {\n result: userByUsername(username: $username) {\n __typename\n ... on User {\n slug\n username\n }\n ... on Error {\n message\n }\n }\n }\n": types.TestUserByUsernameDocument, - "\n query TestUsers($input: UsersQueryInput) {\n result: users(input: $input) {\n edges {\n node {\n username\n }\n }\n }\n }\n": types.TestUsersDocument, -}; - -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - * - * - * @example - * ```ts - * const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`); - * ``` - * - * The query argument is unknown! - * Please regenerate the types. - */ -export function graphql(source: string): unknown; - -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n query Test_MyMembership($slug: String!) {\n result: group(slug: $slug) {\n __typename\n ... on Group {\n id\n myMembership {\n id\n role\n }\n }\n }\n }\n "): (typeof documents)["\n query Test_MyMembership($slug: String!) {\n result: group(slug: $slug) {\n __typename\n ... on Group {\n id\n myMembership {\n id\n role\n }\n }\n }\n }\n "]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n query Test_Memberships($slug: String!) {\n result: group(slug: $slug) {\n __typename\n ... on Group {\n id\n memberships {\n edges {\n node {\n id\n role\n user {\n slug\n }\n }\n }\n }\n }\n }\n }\n "): (typeof documents)["\n query Test_Memberships($slug: String!) {\n result: group(slug: $slug) {\n __typename\n ... on Group {\n id\n memberships {\n edges {\n node {\n id\n role\n user {\n slug\n }\n }\n }\n }\n }\n }\n }\n "]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n mutation Test_CreateGroup($input: MutationCreateGroupInput!) {\n result: createGroup(input: $input) {\n __typename\n ... on Error {\n message\n }\n ... on CreateGroupResult {\n group {\n id\n }\n }\n }\n }\n "): (typeof documents)["\n mutation Test_CreateGroup($input: MutationCreateGroupInput!) {\n result: createGroup(input: $input) {\n __typename\n ... on Error {\n message\n }\n ... on CreateGroupResult {\n group {\n id\n }\n }\n }\n }\n "]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n mutation Test_CreateModel(\n $input: MutationCreateSquiggleSnippetModelInput!\n ) {\n result: createSquiggleSnippetModel(input: $input) {\n __typename\n ... on Error {\n message\n }\n ... on CreateSquiggleSnippetModelResult {\n model {\n id\n }\n }\n }\n }\n "): (typeof documents)["\n mutation Test_CreateModel(\n $input: MutationCreateSquiggleSnippetModelInput!\n ) {\n result: createSquiggleSnippetModel(input: $input) {\n __typename\n ... on Error {\n message\n }\n ... on CreateSquiggleSnippetModelResult {\n model {\n id\n }\n }\n }\n }\n "]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n mutation Test_Invite($input: MutationInviteUserToGroupInput!) {\n result: inviteUserToGroup(input: $input) {\n __typename\n ... on BaseError {\n message\n }\n ... on InviteUserToGroupResult {\n invite {\n id\n }\n }\n }\n }\n "): (typeof documents)["\n mutation Test_Invite($input: MutationInviteUserToGroupInput!) {\n result: inviteUserToGroup(input: $input) {\n __typename\n ... on BaseError {\n message\n }\n ... on InviteUserToGroupResult {\n invite {\n id\n }\n }\n }\n }\n "]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n mutation Test_AcceptInvite($input: MutationReactToGroupInviteInput!) {\n result: reactToGroupInvite(input: $input) {\n __typename\n ... on BaseError {\n message\n }\n ... on ReactToGroupInviteResult {\n __typename\n }\n }\n }\n "): (typeof documents)["\n mutation Test_AcceptInvite($input: MutationReactToGroupInviteInput!) {\n result: reactToGroupInvite(input: $input) {\n __typename\n ... on BaseError {\n message\n }\n ... on ReactToGroupInviteResult {\n __typename\n }\n }\n }\n "]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n mutation CreateGroupTest {\n result: createGroup(input: { slug: \"testgroup\" }) {\n __typename\n ... on CreateGroupResult {\n group {\n id\n slug\n memberships {\n edges {\n node {\n user {\n username\n }\n role\n }\n }\n }\n }\n }\n ... on BaseError {\n message\n }\n }\n }\n"): (typeof documents)["\n mutation CreateGroupTest {\n result: createGroup(input: { slug: \"testgroup\" }) {\n __typename\n ... on CreateGroupResult {\n group {\n id\n slug\n memberships {\n edges {\n node {\n user {\n username\n }\n role\n }\n }\n }\n }\n }\n ... on BaseError {\n message\n }\n }\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n mutation CreateSquiggleSnippetModelTest(\n $input: MutationCreateSquiggleSnippetModelInput!\n ) {\n result: createSquiggleSnippetModel(input: $input) {\n __typename\n ... on Error {\n message\n }\n ... on ValidationError {\n issues {\n message\n }\n }\n ... on CreateSquiggleSnippetModelResult {\n model {\n id\n slug\n isPrivate\n owner {\n __typename\n slug\n }\n }\n }\n }\n }\n"): (typeof documents)["\n mutation CreateSquiggleSnippetModelTest(\n $input: MutationCreateSquiggleSnippetModelInput!\n ) {\n result: createSquiggleSnippetModel(input: $input) {\n __typename\n ... on Error {\n message\n }\n ... on ValidationError {\n issues {\n message\n }\n }\n ... on CreateSquiggleSnippetModelResult {\n model {\n id\n slug\n isPrivate\n owner {\n __typename\n slug\n }\n }\n }\n }\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n mutation DeleteMembershipTest($group: String!, $user: String!) {\n result: deleteMembership(input: { group: $group, user: $user }) {\n __typename\n ... on BaseError {\n message\n }\n ... on DeleteMembershipResult {\n ok\n }\n }\n }\n "): (typeof documents)["\n mutation DeleteMembershipTest($group: String!, $user: String!) {\n result: deleteMembership(input: { group: $group, user: $user }) {\n __typename\n ... on BaseError {\n message\n }\n ... on DeleteMembershipResult {\n ok\n }\n }\n }\n "]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n mutation DeleteModelTest($input: MutationDeleteModelInput!) {\n result: deleteModel(input: $input) {\n __typename\n ... on Error {\n message\n }\n ... on NotFoundError {\n message\n }\n ... on DeleteModelResult {\n ok\n }\n }\n }\n"): (typeof documents)["\n mutation DeleteModelTest($input: MutationDeleteModelInput!) {\n result: deleteModel(input: $input) {\n __typename\n ... on Error {\n message\n }\n ... on NotFoundError {\n message\n }\n ... on DeleteModelResult {\n ok\n }\n }\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n mutation InviteTest($input: MutationInviteUserToGroupInput!) {\n result: inviteUserToGroup(input: $input) {\n __typename\n ... on BaseError {\n message\n }\n ... on InviteUserToGroupResult {\n invite {\n id\n role\n }\n }\n }\n }\n "): (typeof documents)["\n mutation InviteTest($input: MutationInviteUserToGroupInput!) {\n result: inviteUserToGroup(input: $input) {\n __typename\n ... on BaseError {\n message\n }\n ... on InviteUserToGroupResult {\n invite {\n id\n role\n }\n }\n }\n }\n "]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n mutation SetUsernameTest($username: String!) {\n result: setUsername(username: $username) {\n __typename\n ... on Error {\n message\n }\n ... on ValidationError {\n message\n }\n ... on Me {\n email\n username\n }\n }\n }\n"): (typeof documents)["\n mutation SetUsernameTest($username: String!) {\n result: setUsername(username: $username) {\n __typename\n ... on Error {\n message\n }\n ... on ValidationError {\n message\n }\n ... on Me {\n email\n username\n }\n }\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n query TestGroups($input: GroupsQueryInput!) {\n result: groups(first: 10, input: $input) {\n __typename\n edges {\n node {\n id\n slug\n }\n }\n }\n }\n "): (typeof documents)["\n query TestGroups($input: GroupsQueryInput!) {\n result: groups(first: 10, input: $input) {\n __typename\n edges {\n node {\n id\n slug\n }\n }\n }\n }\n "]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n query TestMe {\n me {\n __typename\n email\n username\n }\n }\n "): (typeof documents)["\n query TestMe {\n me {\n __typename\n email\n username\n }\n }\n "]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n query TestModels {\n models {\n edges {\n node {\n slug\n isEditable\n isPrivate\n owner {\n __typename\n slug\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query TestModels {\n models {\n edges {\n node {\n slug\n isEditable\n isPrivate\n owner {\n __typename\n slug\n }\n }\n }\n }\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n mutation TestModels_createModel(\n $input: MutationCreateSquiggleSnippetModelInput!\n ) {\n result: createSquiggleSnippetModel(input: $input) {\n __typename\n }\n }\n"): (typeof documents)["\n mutation TestModels_createModel(\n $input: MutationCreateSquiggleSnippetModelInput!\n ) {\n result: createSquiggleSnippetModel(input: $input) {\n __typename\n }\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n query TestUserByUsername($username: String!) {\n result: userByUsername(username: $username) {\n __typename\n ... on User {\n slug\n username\n }\n ... on Error {\n message\n }\n }\n }\n"): (typeof documents)["\n query TestUserByUsername($username: String!) {\n result: userByUsername(username: $username) {\n __typename\n ... on User {\n slug\n username\n }\n ... on Error {\n message\n }\n }\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n query TestUsers($input: UsersQueryInput) {\n result: users(input: $input) {\n edges {\n node {\n username\n }\n }\n }\n }\n"): (typeof documents)["\n query TestUsers($input: UsersQueryInput) {\n result: users(input: $input) {\n edges {\n node {\n username\n }\n }\n }\n }\n"]; - -export function graphql(source: string) { - return (documents as any)[source] ?? {}; -} - -export type DocumentType> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never; \ No newline at end of file diff --git a/packages/hub/test/gql-gen/graphql.ts b/packages/hub/test/gql-gen/graphql.ts deleted file mode 100644 index 135050c3b3..0000000000 --- a/packages/hub/test/gql-gen/graphql.ts +++ /dev/null @@ -1,1157 +0,0 @@ -/* eslint-disable */ -import { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/core"; - -export type Maybe = T | null; -export type InputMaybe = Maybe; -export type Exact = { [K in keyof T]: T[K] }; -export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; -export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; -export type MakeEmpty = { [_ in K]?: never }; -export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; -/** All built-in and custom scalars, mapped to their actual values */ -export type Scalars = { - ID: { input: string; output: string; } - String: { input: string; output: string; } - Boolean: { input: boolean; output: boolean; } - Int: { input: number; output: number; } - Float: { input: number; output: number; } -}; - -export type AcceptReusableGroupInviteTokenResult = { - __typename?: 'AcceptReusableGroupInviteTokenResult'; - membership: UserGroupMembership; -}; - -export type AdminUpdateModelVersionResult = { - __typename?: 'AdminUpdateModelVersionResult'; - model: Model; -}; - -export type BaseError = Error & { - __typename?: 'BaseError'; - message: Scalars['String']['output']; -}; - -export type BuildRelativeValuesCacheResult = { - __typename?: 'BuildRelativeValuesCacheResult'; - relativeValuesExport: RelativeValuesExport; -}; - -export type CancelGroupInviteResult = { - __typename?: 'CancelGroupInviteResult'; - invite: GroupInvite; -}; - -export type ClearRelativeValuesCacheResult = { - __typename?: 'ClearRelativeValuesCacheResult'; - relativeValuesExport: RelativeValuesExport; -}; - -export type CreateGroupResult = { - __typename?: 'CreateGroupResult'; - group: Group; -}; - -export type CreateRelativeValuesDefinitionResult = { - __typename?: 'CreateRelativeValuesDefinitionResult'; - definition: RelativeValuesDefinition; -}; - -export type CreateReusableGroupInviteTokenResult = { - __typename?: 'CreateReusableGroupInviteTokenResult'; - group: Group; -}; - -export type CreateSquiggleSnippetModelResult = { - __typename?: 'CreateSquiggleSnippetModelResult'; - model: Model; -}; - -export type DefinitionRefInput = { - owner: Scalars['String']['input']; - slug: Scalars['String']['input']; -}; - -export type DeleteMembershipResult = { - __typename?: 'DeleteMembershipResult'; - ok: Scalars['Boolean']['output']; -}; - -export type DeleteModelResult = { - __typename?: 'DeleteModelResult'; - ok: Scalars['Boolean']['output']; -}; - -export type DeleteRelativeValuesDefinitionResult = { - __typename?: 'DeleteRelativeValuesDefinitionResult'; - ok: Scalars['Boolean']['output']; -}; - -export type DeleteReusableGroupInviteTokenResult = { - __typename?: 'DeleteReusableGroupInviteTokenResult'; - group: Group; -}; - -export type EmailGroupInvite = GroupInvite & Node & { - __typename?: 'EmailGroupInvite'; - email: Scalars['String']['output']; - group: Group; - id: Scalars['ID']['output']; - role: MembershipRole; -}; - -export type Error = { - message: Scalars['String']['output']; -}; - -export type GlobalStatistics = { - __typename?: 'GlobalStatistics'; - models: Scalars['Int']['output']; - relativeValuesDefinitions: Scalars['Int']['output']; - users: Scalars['Int']['output']; -}; - -export type Group = Node & Owner & { - __typename?: 'Group'; - createdAtTimestamp: Scalars['Float']['output']; - id: Scalars['ID']['output']; - inviteForMe?: Maybe; - invites?: Maybe; - memberships: UserGroupMembershipConnection; - models: ModelConnection; - myMembership?: Maybe; - reusableInviteToken?: Maybe; - slug: Scalars['String']['output']; - updatedAtTimestamp: Scalars['Float']['output']; -}; - - -export type GroupInvitesArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -export type GroupMembershipsArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -export type GroupModelsArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - -export type GroupConnection = { - __typename?: 'GroupConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type GroupEdge = { - __typename?: 'GroupEdge'; - cursor: Scalars['String']['output']; - node: Group; -}; - -export type GroupInvite = { - group: Group; - id: Scalars['ID']['output']; - role: MembershipRole; -}; - -export type GroupInviteConnection = { - __typename?: 'GroupInviteConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type GroupInviteEdge = { - __typename?: 'GroupInviteEdge'; - cursor: Scalars['String']['output']; - node: GroupInvite; -}; - -export enum GroupInviteReaction { - Accept = 'Accept', - Decline = 'Decline' -} - -export type GroupsQueryInput = { - /** List only groups that you're a member of */ - myOnly?: InputMaybe; - slugContains?: InputMaybe; -}; - -export type InviteUserToGroupResult = { - __typename?: 'InviteUserToGroupResult'; - invite: GroupInvite; -}; - -export type Me = { - __typename?: 'Me'; - asUser: User; - email: Scalars['String']['output']; - username?: Maybe; -}; - -export enum MembershipRole { - Admin = 'Admin', - Member = 'Member' -} - -export type Model = Node & { - __typename?: 'Model'; - createdAtTimestamp: Scalars['Float']['output']; - currentRevision: ModelRevision; - id: Scalars['ID']['output']; - isEditable: Scalars['Boolean']['output']; - isPrivate: Scalars['Boolean']['output']; - owner: Owner; - revision: ModelRevision; - revisions: ModelRevisionConnection; - slug: Scalars['String']['output']; - updatedAtTimestamp: Scalars['Float']['output']; -}; - - -export type ModelRevisionArgs = { - id: Scalars['ID']['input']; -}; - - -export type ModelRevisionsArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - -export type ModelConnection = { - __typename?: 'ModelConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type ModelContent = SquiggleSnippet; - -export type ModelEdge = { - __typename?: 'ModelEdge'; - cursor: Scalars['String']['output']; - node: Model; -}; - -export type Variable = Node & { - __typename?: 'Variable'; - docstring: Scalars['String']['output']; - id: Scalars['ID']['output']; - modelRevision: ModelRevision; - title?: Maybe; - variableName: Scalars['String']['output']; - variableType: Scalars['String']['output']; -}; - -export type ModelRevision = Node & { - __typename?: 'ModelRevision'; - author?: Maybe; - comment: Scalars['String']['output']; - content: ModelContent; - createdAtTimestamp: Scalars['Float']['output']; - variables: Array; - forRelativeValues?: Maybe; - id: Scalars['ID']['output']; - model: Model; - relativeValuesExports: Array; -}; - - -export type ModelRevisionForRelativeValuesArgs = { - input?: InputMaybe; -}; - -export type ModelRevisionConnection = { - __typename?: 'ModelRevisionConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type ModelRevisionEdge = { - __typename?: 'ModelRevisionEdge'; - cursor: Scalars['String']['output']; - node: ModelRevision; -}; - -export type ModelRevisionForRelativeValuesInput = { - for?: InputMaybe; - variableName: Scalars['String']['input']; -}; - -export type ModelRevisionForRelativeValuesSlugOwnerInput = { - owner: Scalars['String']['input']; - slug: Scalars['String']['input']; -}; - -export type ModelsByVersion = { - __typename?: 'ModelsByVersion'; - count: Scalars['Int']['output']; - models: Array; - privateCount: Scalars['Int']['output']; - version: Scalars['String']['output']; -}; - -export type MoveModelResult = { - __typename?: 'MoveModelResult'; - model: Model; -}; - -export type Mutation = { - __typename?: 'Mutation'; - acceptReusableGroupInviteToken: MutationAcceptReusableGroupInviteTokenResult; - /** Admin-only query for upgrading model versions */ - adminUpdateModelVersion: MutationAdminUpdateModelVersionResult; - buildRelativeValuesCache: MutationBuildRelativeValuesCacheResult; - cancelGroupInvite: MutationCancelGroupInviteResult; - clearRelativeValuesCache: MutationClearRelativeValuesCacheResult; - createGroup: MutationCreateGroupResult; - createRelativeValuesDefinition: MutationCreateRelativeValuesDefinitionResult; - /** - * Create or replace a reusable invite token for a group, available as `reusableInviteToken` field on group object. - * - * You must be an admin of the group to call this mutation. Previous invite token, if it existed, will stop working. - */ - createReusableGroupInviteToken: MutationCreateReusableGroupInviteTokenResult; - createSquiggleSnippetModel: MutationCreateSquiggleSnippetModelResult; - deleteMembership: MutationDeleteMembershipResult; - deleteModel: MutationDeleteModelResult; - deleteRelativeValuesDefinition: MutationDeleteRelativeValuesDefinitionResult; - /** Disable a reusable invite token for a group. */ - deleteReusableGroupInviteToken: MutationDeleteReusableGroupInviteTokenResult; - inviteUserToGroup: MutationInviteUserToGroupResult; - moveModel: MutationMoveModelResult; - reactToGroupInvite: MutationReactToGroupInviteResult; - setUsername: MutationSetUsernameResult; - updateGroupInviteRole: MutationUpdateGroupInviteRoleResult; - updateMembershipRole: MutationUpdateMembershipRoleResult; - updateModelPrivacy: MutationUpdateModelPrivacyResult; - updateModelSlug: MutationUpdateModelSlugResult; - updateRelativeValuesDefinition: MutationUpdateRelativeValuesDefinitionResult; - updateSquiggleSnippetModel: MutationUpdateSquiggleSnippetModelResult; -}; - - -export type MutationAcceptReusableGroupInviteTokenArgs = { - input: MutationAcceptReusableGroupInviteTokenInput; -}; - - -export type MutationAdminUpdateModelVersionArgs = { - input: MutationAdminUpdateModelVersionInput; -}; - - -export type MutationBuildRelativeValuesCacheArgs = { - input: MutationBuildRelativeValuesCacheInput; -}; - - -export type MutationCancelGroupInviteArgs = { - input: MutationCancelGroupInviteInput; -}; - - -export type MutationClearRelativeValuesCacheArgs = { - input: MutationClearRelativeValuesCacheInput; -}; - - -export type MutationCreateGroupArgs = { - input: MutationCreateGroupInput; -}; - - -export type MutationCreateRelativeValuesDefinitionArgs = { - input: MutationCreateRelativeValuesDefinitionInput; -}; - - -export type MutationCreateReusableGroupInviteTokenArgs = { - input: MutationCreateReusableGroupInviteTokenInput; -}; - - -export type MutationCreateSquiggleSnippetModelArgs = { - input: MutationCreateSquiggleSnippetModelInput; -}; - - -export type MutationDeleteMembershipArgs = { - input: MutationDeleteMembershipInput; -}; - - -export type MutationDeleteModelArgs = { - input: MutationDeleteModelInput; -}; - - -export type MutationDeleteRelativeValuesDefinitionArgs = { - input: MutationDeleteRelativeValuesDefinitionInput; -}; - - -export type MutationDeleteReusableGroupInviteTokenArgs = { - input: MutationDeleteReusableGroupInviteTokenInput; -}; - - -export type MutationInviteUserToGroupArgs = { - input: MutationInviteUserToGroupInput; -}; - - -export type MutationMoveModelArgs = { - input: MutationMoveModelInput; -}; - - -export type MutationReactToGroupInviteArgs = { - input: MutationReactToGroupInviteInput; -}; - - -export type MutationSetUsernameArgs = { - username: Scalars['String']['input']; -}; - - -export type MutationUpdateGroupInviteRoleArgs = { - input: MutationUpdateGroupInviteRoleInput; -}; - - -export type MutationUpdateMembershipRoleArgs = { - input: MutationUpdateMembershipRoleInput; -}; - - -export type MutationUpdateModelPrivacyArgs = { - input: MutationUpdateModelPrivacyInput; -}; - - -export type MutationUpdateModelSlugArgs = { - input: MutationUpdateModelSlugInput; -}; - - -export type MutationUpdateRelativeValuesDefinitionArgs = { - input: MutationUpdateRelativeValuesDefinitionInput; -}; - - -export type MutationUpdateSquiggleSnippetModelArgs = { - input: MutationUpdateSquiggleSnippetModelInput; -}; - -export type MutationAcceptReusableGroupInviteTokenInput = { - groupSlug: Scalars['String']['input']; - inviteToken: Scalars['String']['input']; -}; - -export type MutationAcceptReusableGroupInviteTokenResult = AcceptReusableGroupInviteTokenResult | BaseError; - -export type MutationAdminUpdateModelVersionInput = { - modelId: Scalars['String']['input']; - version: Scalars['String']['input']; -}; - -export type MutationAdminUpdateModelVersionResult = AdminUpdateModelVersionResult | BaseError; - -export type MutationBuildRelativeValuesCacheInput = { - exportId: Scalars['String']['input']; -}; - -export type MutationBuildRelativeValuesCacheResult = BaseError | BuildRelativeValuesCacheResult; - -export type MutationCancelGroupInviteInput = { - inviteId: Scalars['String']['input']; -}; - -export type MutationCancelGroupInviteResult = BaseError | CancelGroupInviteResult; - -export type MutationClearRelativeValuesCacheInput = { - exportId: Scalars['String']['input']; -}; - -export type MutationClearRelativeValuesCacheResult = BaseError | ClearRelativeValuesCacheResult; - -export type MutationCreateGroupInput = { - slug: Scalars['String']['input']; -}; - -export type MutationCreateGroupResult = BaseError | CreateGroupResult; - -export type MutationCreateRelativeValuesDefinitionInput = { - clusters: Array; - /** Optional, if not set, definition will be created on current user's account */ - groupSlug?: InputMaybe; - items: Array; - recommendedUnit?: InputMaybe; - slug: Scalars['String']['input']; - title: Scalars['String']['input']; -}; - -export type MutationCreateRelativeValuesDefinitionResult = BaseError | CreateRelativeValuesDefinitionResult | ValidationError; - -export type MutationCreateReusableGroupInviteTokenInput = { - slug: Scalars['String']['input']; -}; - -export type MutationCreateReusableGroupInviteTokenResult = BaseError | CreateReusableGroupInviteTokenResult; - -export type MutationCreateSquiggleSnippetModelInput = { - /** Squiggle source code */ - code: Scalars['String']['input']; - /** Optional, if not set, model will be created on current user's account */ - groupSlug?: InputMaybe; - /** Defaults to false */ - isPrivate?: InputMaybe; - slug: Scalars['String']['input']; - version: Scalars['String']['input']; -}; - -export type MutationCreateSquiggleSnippetModelResult = BaseError | CreateSquiggleSnippetModelResult | ValidationError; - -export type MutationDeleteMembershipInput = { - group: Scalars['String']['input']; - user: Scalars['String']['input']; -}; - -export type MutationDeleteMembershipResult = BaseError | DeleteMembershipResult; - -export type MutationDeleteModelInput = { - owner: Scalars['String']['input']; - slug: Scalars['String']['input']; -}; - -export type MutationDeleteModelResult = BaseError | DeleteModelResult | NotFoundError | ValidationError; - -export type MutationDeleteRelativeValuesDefinitionInput = { - owner: Scalars['String']['input']; - slug: Scalars['String']['input']; -}; - -export type MutationDeleteRelativeValuesDefinitionResult = BaseError | DeleteRelativeValuesDefinitionResult; - -export type MutationDeleteReusableGroupInviteTokenInput = { - slug: Scalars['String']['input']; -}; - -export type MutationDeleteReusableGroupInviteTokenResult = BaseError | DeleteReusableGroupInviteTokenResult; - -export type MutationInviteUserToGroupInput = { - group: Scalars['String']['input']; - role: MembershipRole; - username: Scalars['String']['input']; -}; - -export type MutationInviteUserToGroupResult = BaseError | InviteUserToGroupResult | ValidationError; - -export type MutationMoveModelInput = { - newOwner: Scalars['String']['input']; - oldOwner: Scalars['String']['input']; - slug: Scalars['String']['input']; -}; - -export type MutationMoveModelResult = BaseError | MoveModelResult | NotFoundError | ValidationError; - -export type MutationReactToGroupInviteInput = { - action: GroupInviteReaction; - inviteId: Scalars['String']['input']; -}; - -export type MutationReactToGroupInviteResult = BaseError | ReactToGroupInviteResult; - -export type MutationSetUsernameResult = BaseError | Me | ValidationError; - -export type MutationUpdateGroupInviteRoleInput = { - inviteId: Scalars['String']['input']; - role: MembershipRole; -}; - -export type MutationUpdateGroupInviteRoleResult = BaseError | UpdateGroupInviteRoleResult; - -export type MutationUpdateMembershipRoleInput = { - group: Scalars['String']['input']; - role: MembershipRole; - user: Scalars['String']['input']; -}; - -export type MutationUpdateMembershipRoleResult = BaseError | UpdateMembershipRoleResult; - -export type MutationUpdateModelPrivacyInput = { - isPrivate: Scalars['Boolean']['input']; - owner: Scalars['String']['input']; - slug: Scalars['String']['input']; -}; - -export type MutationUpdateModelPrivacyResult = BaseError | UpdateModelPrivacyResult; - -export type MutationUpdateModelSlugInput = { - newSlug: Scalars['String']['input']; - oldSlug: Scalars['String']['input']; - owner: Scalars['String']['input']; -}; - -export type MutationUpdateModelSlugResult = BaseError | UpdateModelSlugResult; - -export type MutationUpdateRelativeValuesDefinitionInput = { - clusters: Array; - items: Array; - owner: Scalars['String']['input']; - recommendedUnit?: InputMaybe; - slug: Scalars['String']['input']; - title: Scalars['String']['input']; -}; - -export type MutationUpdateRelativeValuesDefinitionResult = BaseError | UpdateRelativeValuesDefinitionResult; - -export type MutationUpdateSquiggleSnippetModelInput = { - comment?: InputMaybe; - content: SquiggleSnippetContentInput; - variables?: InputMaybe>; - owner: Scalars['String']['input']; - relativeValuesExports?: InputMaybe>; - slug: Scalars['String']['input']; -}; - -export type MutationUpdateSquiggleSnippetModelResult = BaseError | UpdateSquiggleSnippetResult; - -export type Node = { - id: Scalars['ID']['output']; -}; - -export type NotFoundError = Error & { - __typename?: 'NotFoundError'; - message: Scalars['String']['output']; -}; - -export type Owner = { - id: Scalars['ID']['output']; - slug: Scalars['String']['output']; -}; - -export type PageInfo = { - __typename?: 'PageInfo'; - endCursor?: Maybe; - hasNextPage: Scalars['Boolean']['output']; - hasPreviousPage: Scalars['Boolean']['output']; - startCursor?: Maybe; -}; - -export type Query = { - __typename?: 'Query'; - globalStatistics: GlobalStatistics; - group: QueryGroupResult; - groups: GroupConnection; - me: Me; - model: QueryModelResult; - models: ModelConnection; - /** Admin-only query for listing models in /admin UI */ - modelsByVersion: Array; - node?: Maybe; - nodes: Array>; - relativeValuesDefinition: QueryRelativeValuesDefinitionResult; - relativeValuesDefinitions: RelativeValuesDefinitionConnection; - runSquiggle: SquiggleOutput; - userByUsername: QueryUserByUsernameResult; - users: QueryUsersConnection; -}; - - -export type QueryGroupArgs = { - slug: Scalars['String']['input']; -}; - - -export type QueryGroupsArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - input?: InputMaybe; - last?: InputMaybe; -}; - - -export type QueryModelArgs = { - input: QueryModelInput; -}; - - -export type QueryModelsArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -export type QueryNodeArgs = { - id: Scalars['ID']['input']; -}; - - -export type QueryNodesArgs = { - ids: Array; -}; - - -export type QueryRelativeValuesDefinitionArgs = { - input: QueryRelativeValuesDefinitionInput; -}; - - -export type QueryRelativeValuesDefinitionsArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - input?: InputMaybe; - last?: InputMaybe; -}; - - -export type QueryRunSquiggleArgs = { - code: Scalars['String']['input']; -}; - - -export type QueryUserByUsernameArgs = { - username: Scalars['String']['input']; -}; - - -export type QueryUsersArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - input?: InputMaybe; - last?: InputMaybe; -}; - -export type QueryGroupResult = BaseError | Group | NotFoundError; - -export type QueryModelInput = { - owner: Scalars['String']['input']; - slug: Scalars['String']['input']; -}; - -export type QueryModelResult = BaseError | Model | NotFoundError; - -export type QueryRelativeValuesDefinitionInput = { - owner: Scalars['String']['input']; - slug: Scalars['String']['input']; -}; - -export type QueryRelativeValuesDefinitionResult = BaseError | NotFoundError | RelativeValuesDefinition; - -export type QueryUserByUsernameResult = BaseError | NotFoundError | User; - -export type QueryUsersConnection = { - __typename?: 'QueryUsersConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type QueryUsersConnectionEdge = { - __typename?: 'QueryUsersConnectionEdge'; - cursor: Scalars['String']['output']; - node: User; -}; - -export type ReactToGroupInviteResult = { - __typename?: 'ReactToGroupInviteResult'; - invite: GroupInvite; - membership?: Maybe; -}; - -export type RelativeValuesCluster = { - __typename?: 'RelativeValuesCluster'; - color: Scalars['String']['output']; - id: Scalars['String']['output']; - recommendedUnit?: Maybe; -}; - -export type RelativeValuesClusterInput = { - color: Scalars['String']['input']; - id: Scalars['String']['input']; - recommendedUnit?: InputMaybe; -}; - -export type RelativeValuesDefinition = Node & { - __typename?: 'RelativeValuesDefinition'; - createdAtTimestamp: Scalars['Float']['output']; - currentRevision: RelativeValuesDefinitionRevision; - id: Scalars['ID']['output']; - isEditable: Scalars['Boolean']['output']; - Variables: Array; - owner: Owner; - slug: Scalars['String']['output']; - updatedAtTimestamp: Scalars['Float']['output']; -}; - -export type RelativeValuesDefinitionConnection = { - __typename?: 'RelativeValuesDefinitionConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type RelativeValuesDefinitionEdge = { - __typename?: 'RelativeValuesDefinitionEdge'; - cursor: Scalars['String']['output']; - node: RelativeValuesDefinition; -}; - -export type RelativeValuesDefinitionRevision = Node & { - __typename?: 'RelativeValuesDefinitionRevision'; - clusters: Array; - id: Scalars['ID']['output']; - items: Array; - recommendedUnit?: Maybe; - title: Scalars['String']['output']; -}; - -export type RelativeValuesDefinitionsQueryInput = { - owner?: InputMaybe; - slugContains?: InputMaybe; -}; - -export type RelativeValuesExport = Node & { - __typename?: 'RelativeValuesExport'; - cache: Array; - definition: RelativeValuesDefinition; - id: Scalars['ID']['output']; - modelRevision: ModelRevision; - variableName: Scalars['String']['output']; -}; - -export type RelativeValuesExportInput = { - definition: DefinitionRefInput; - variableName: Scalars['String']['input']; -}; - -export type RelativeValuesItem = { - __typename?: 'RelativeValuesItem'; - clusterId?: Maybe; - description: Scalars['String']['output']; - id: Scalars['String']['output']; - name: Scalars['String']['output']; -}; - -export type RelativeValuesItemInput = { - clusterId?: InputMaybe; - description?: InputMaybe; - id: Scalars['String']['input']; - name: Scalars['String']['input']; -}; - -export type RelativeValuesPairCache = Node & { - __typename?: 'RelativeValuesPairCache'; - errorString?: Maybe; - firstItem: Scalars['String']['output']; - id: Scalars['ID']['output']; - resultJSON: Scalars['String']['output']; - secondItem: Scalars['String']['output']; -}; - -export type SquiggleErrorOutput = SquiggleOutput & { - __typename?: 'SquiggleErrorOutput'; - errorString: Scalars['String']['output']; - isCached: Scalars['Boolean']['output']; -}; - -export type SquiggleVariableInput = { - docstring?: InputMaybe; - title?: InputMaybe; - variableName: Scalars['String']['input']; - variableType: Scalars['String']['input']; -}; - -export type SquiggleOkOutput = SquiggleOutput & { - __typename?: 'SquiggleOkOutput'; - bindingsJSON: Scalars['String']['output']; - isCached: Scalars['Boolean']['output']; - resultJSON: Scalars['String']['output']; -}; - -export type SquiggleOutput = { - isCached: Scalars['Boolean']['output']; -}; - -export type SquiggleSnippet = Node & { - __typename?: 'SquiggleSnippet'; - code: Scalars['String']['output']; - id: Scalars['ID']['output']; - version: Scalars['String']['output']; -}; - -export type SquiggleSnippetContentInput = { - code: Scalars['String']['input']; - version: Scalars['String']['input']; -}; - -export type UpdateGroupInviteRoleResult = { - __typename?: 'UpdateGroupInviteRoleResult'; - invite: GroupInvite; -}; - -export type UpdateMembershipRoleResult = { - __typename?: 'UpdateMembershipRoleResult'; - membership: UserGroupMembership; -}; - -export type UpdateModelPrivacyResult = { - __typename?: 'UpdateModelPrivacyResult'; - model: Model; -}; - -export type UpdateModelSlugResult = { - __typename?: 'UpdateModelSlugResult'; - model: Model; -}; - -export type UpdateRelativeValuesDefinitionResult = { - __typename?: 'UpdateRelativeValuesDefinitionResult'; - definition: RelativeValuesDefinition; -}; - -export type UpdateSquiggleSnippetResult = { - __typename?: 'UpdateSquiggleSnippetResult'; - model: Model; -}; - -export type User = Node & Owner & { - __typename?: 'User'; - groups: GroupConnection; - id: Scalars['ID']['output']; - isRoot: Scalars['Boolean']['output']; - models: ModelConnection; - relativeValuesDefinitions: RelativeValuesDefinitionConnection; - slug: Scalars['String']['output']; - username: Scalars['String']['output']; -}; - - -export type UserGroupsArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -export type UserModelsArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - - -export type UserRelativeValuesDefinitionsArgs = { - after?: InputMaybe; - before?: InputMaybe; - first?: InputMaybe; - last?: InputMaybe; -}; - -export type UserGroupInvite = GroupInvite & Node & { - __typename?: 'UserGroupInvite'; - group: Group; - id: Scalars['ID']['output']; - role: MembershipRole; - user: User; -}; - -export type UserGroupMembership = Node & { - __typename?: 'UserGroupMembership'; - group: Group; - id: Scalars['ID']['output']; - role: MembershipRole; - user: User; -}; - -export type UserGroupMembershipConnection = { - __typename?: 'UserGroupMembershipConnection'; - edges: Array; - pageInfo: PageInfo; -}; - -export type UserGroupMembershipEdge = { - __typename?: 'UserGroupMembershipEdge'; - cursor: Scalars['String']['output']; - node: UserGroupMembership; -}; - -export type UsersQueryInput = { - usernameContains?: InputMaybe; -}; - -export type ValidationError = Error & { - __typename?: 'ValidationError'; - issues: Array; - message: Scalars['String']['output']; -}; - -export type ValidationErrorIssue = { - __typename?: 'ValidationErrorIssue'; - message: Scalars['String']['output']; - path: Array; -}; - -export type Test_MyMembershipQueryVariables = Exact<{ - slug: Scalars['String']['input']; -}>; - - -export type Test_MyMembershipQuery = { __typename?: 'Query', result: { __typename: 'BaseError' } | { __typename: 'Group', id: string, myMembership?: { __typename?: 'UserGroupMembership', id: string, role: MembershipRole } | null } | { __typename: 'NotFoundError' } }; - -export type Test_MembershipsQueryVariables = Exact<{ - slug: Scalars['String']['input']; -}>; - - -export type Test_MembershipsQuery = { __typename?: 'Query', result: { __typename: 'BaseError' } | { __typename: 'Group', id: string, memberships: { __typename?: 'UserGroupMembershipConnection', edges: Array<{ __typename?: 'UserGroupMembershipEdge', node: { __typename?: 'UserGroupMembership', id: string, role: MembershipRole, user: { __typename?: 'User', slug: string } } }> } } | { __typename: 'NotFoundError' } }; - -export type Test_CreateGroupMutationVariables = Exact<{ - input: MutationCreateGroupInput; -}>; - - -export type Test_CreateGroupMutation = { __typename?: 'Mutation', result: { __typename: 'BaseError', message: string } | { __typename: 'CreateGroupResult', group: { __typename?: 'Group', id: string } } }; - -export type Test_CreateModelMutationVariables = Exact<{ - input: MutationCreateSquiggleSnippetModelInput; -}>; - - -export type Test_CreateModelMutation = { __typename?: 'Mutation', result: { __typename: 'BaseError', message: string } | { __typename: 'CreateSquiggleSnippetModelResult', model: { __typename?: 'Model', id: string } } | { __typename: 'ValidationError', message: string } }; - -export type Test_InviteMutationVariables = Exact<{ - input: MutationInviteUserToGroupInput; -}>; - - -export type Test_InviteMutation = { __typename?: 'Mutation', result: { __typename: 'BaseError', message: string } | { __typename: 'InviteUserToGroupResult', invite: { __typename?: 'EmailGroupInvite', id: string } | { __typename?: 'UserGroupInvite', id: string } } | { __typename: 'ValidationError' } }; - -export type Test_AcceptInviteMutationVariables = Exact<{ - input: MutationReactToGroupInviteInput; -}>; - - -export type Test_AcceptInviteMutation = { __typename?: 'Mutation', result: { __typename: 'BaseError', message: string } | { __typename: 'ReactToGroupInviteResult' } }; - -export type CreateGroupTestMutationVariables = Exact<{ [key: string]: never; }>; - - -export type CreateGroupTestMutation = { __typename?: 'Mutation', result: { __typename: 'BaseError', message: string } | { __typename: 'CreateGroupResult', group: { __typename?: 'Group', id: string, slug: string, memberships: { __typename?: 'UserGroupMembershipConnection', edges: Array<{ __typename?: 'UserGroupMembershipEdge', node: { __typename?: 'UserGroupMembership', role: MembershipRole, user: { __typename?: 'User', username: string } } }> } } } }; - -export type CreateSquiggleSnippetModelTestMutationVariables = Exact<{ - input: MutationCreateSquiggleSnippetModelInput; -}>; - - -export type CreateSquiggleSnippetModelTestMutation = { __typename?: 'Mutation', result: { __typename: 'BaseError', message: string } | { __typename: 'CreateSquiggleSnippetModelResult', model: { __typename?: 'Model', id: string, slug: string, isPrivate: boolean, owner: { __typename: 'Group', slug: string } | { __typename: 'User', slug: string } } } | { __typename: 'ValidationError', message: string, issues: Array<{ __typename?: 'ValidationErrorIssue', message: string }> } }; - -export type DeleteMembershipTestMutationVariables = Exact<{ - group: Scalars['String']['input']; - user: Scalars['String']['input']; -}>; - - -export type DeleteMembershipTestMutation = { __typename?: 'Mutation', result: { __typename: 'BaseError', message: string } | { __typename: 'DeleteMembershipResult', ok: boolean } }; - -export type DeleteModelTestMutationVariables = Exact<{ - input: MutationDeleteModelInput; -}>; - - -export type DeleteModelTestMutation = { __typename?: 'Mutation', result: { __typename: 'BaseError', message: string } | { __typename: 'DeleteModelResult', ok: boolean } | { __typename: 'NotFoundError', message: string } | { __typename: 'ValidationError', message: string } }; - -export type InviteTestMutationVariables = Exact<{ - input: MutationInviteUserToGroupInput; -}>; - - -export type InviteTestMutation = { __typename?: 'Mutation', result: { __typename: 'BaseError', message: string } | { __typename: 'InviteUserToGroupResult', invite: { __typename?: 'EmailGroupInvite', id: string, role: MembershipRole } | { __typename?: 'UserGroupInvite', id: string, role: MembershipRole } } | { __typename: 'ValidationError' } }; - -export type SetUsernameTestMutationVariables = Exact<{ - username: Scalars['String']['input']; -}>; - - -export type SetUsernameTestMutation = { __typename?: 'Mutation', result: { __typename: 'BaseError', message: string } | { __typename: 'Me', email: string, username?: string | null } | { __typename: 'ValidationError', message: string } }; - -export type TestGroupsQueryVariables = Exact<{ - input: GroupsQueryInput; -}>; - - -export type TestGroupsQuery = { __typename?: 'Query', result: { __typename: 'GroupConnection', edges: Array<{ __typename?: 'GroupEdge', node: { __typename?: 'Group', id: string, slug: string } }> } }; - -export type TestMeQueryVariables = Exact<{ [key: string]: never; }>; - - -export type TestMeQuery = { __typename?: 'Query', me: { __typename: 'Me', email: string, username?: string | null } }; - -export type TestModelsQueryVariables = Exact<{ [key: string]: never; }>; - - -export type TestModelsQuery = { __typename?: 'Query', models: { __typename?: 'ModelConnection', edges: Array<{ __typename?: 'ModelEdge', node: { __typename?: 'Model', slug: string, isEditable: boolean, isPrivate: boolean, owner: { __typename: 'Group', slug: string } | { __typename: 'User', slug: string } } }> } }; - -export type TestModels_CreateModelMutationVariables = Exact<{ - input: MutationCreateSquiggleSnippetModelInput; -}>; - - -export type TestModels_CreateModelMutation = { __typename?: 'Mutation', result: { __typename: 'BaseError' } | { __typename: 'CreateSquiggleSnippetModelResult' } | { __typename: 'ValidationError' } }; - -export type TestUserByUsernameQueryVariables = Exact<{ - username: Scalars['String']['input']; -}>; - - -export type TestUserByUsernameQuery = { __typename?: 'Query', result: { __typename: 'BaseError', message: string } | { __typename: 'NotFoundError', message: string } | { __typename: 'User', slug: string, username: string } }; - -export type TestUsersQueryVariables = Exact<{ - input?: InputMaybe; -}>; - - -export type TestUsersQuery = { __typename?: 'Query', result: { __typename?: 'QueryUsersConnection', edges: Array<{ __typename?: 'QueryUsersConnectionEdge', node: { __typename?: 'User', username: string } }> } }; - - -export const Test_MyMembershipDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Test_MyMembership"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"slug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"group"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"slug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Group"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"myMembership"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const Test_MembershipsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Test_Memberships"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"slug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"group"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"slug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Group"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"memberships"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const Test_CreateGroupDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"Test_CreateGroup"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MutationCreateGroupInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"createGroup"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CreateGroupResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const Test_CreateModelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"Test_CreateModel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MutationCreateSquiggleSnippetModelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"createSquiggleSnippetModel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CreateSquiggleSnippetModelResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"model"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const Test_InviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"Test_Invite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MutationInviteUserToGroupInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"inviteUserToGroup"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"BaseError"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"InviteUserToGroupResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"invite"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const Test_AcceptInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"Test_AcceptInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MutationReactToGroupInviteInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"reactToGroupInvite"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"BaseError"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ReactToGroupInviteResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}}]}}]}}]}}]} as unknown as DocumentNode; -export const CreateGroupTestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateGroupTest"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"createGroup"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"slug"},"value":{"kind":"StringValue","value":"testgroup","block":false}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CreateGroupResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"memberships"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"BaseError"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]} as unknown as DocumentNode; -export const CreateSquiggleSnippetModelTestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateSquiggleSnippetModelTest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MutationCreateSquiggleSnippetModelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"createSquiggleSnippetModel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ValidationError"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"issues"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CreateSquiggleSnippetModelResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"model"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"isPrivate"}},{"kind":"Field","name":{"kind":"Name","value":"owner"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const DeleteMembershipTestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteMembershipTest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"group"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"user"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"deleteMembership"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"group"},"value":{"kind":"Variable","name":{"kind":"Name","value":"group"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"user"},"value":{"kind":"Variable","name":{"kind":"Name","value":"user"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"BaseError"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteMembershipResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]}}]} as unknown as DocumentNode; -export const DeleteModelTestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteModelTest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MutationDeleteModelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"deleteModel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"NotFoundError"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteModelResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]}}]} as unknown as DocumentNode; -export const InviteTestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InviteTest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MutationInviteUserToGroupInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"inviteUserToGroup"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"BaseError"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"InviteUserToGroupResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"invite"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const SetUsernameTestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetUsernameTest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"username"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"setUsername"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"username"},"value":{"kind":"Variable","name":{"kind":"Name","value":"username"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ValidationError"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Me"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}}]}}]} as unknown as DocumentNode; -export const TestGroupsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"TestGroups"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"GroupsQueryInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"groups"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"10"}},{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const TestMeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"TestMe"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"me"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}}]} as unknown as DocumentNode; -export const TestModelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"TestModels"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"models"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"isEditable"}},{"kind":"Field","name":{"kind":"Name","value":"isPrivate"}},{"kind":"Field","name":{"kind":"Name","value":"owner"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const TestModels_CreateModelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"TestModels_createModel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MutationCreateSquiggleSnippetModelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"createSquiggleSnippetModel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}}]}}]}}]} as unknown as DocumentNode; -export const TestUserByUsernameDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"TestUserByUsername"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"username"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"userByUsername"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"username"},"value":{"kind":"Variable","name":{"kind":"Name","value":"username"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Error"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]} as unknown as DocumentNode; -export const TestUsersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"TestUsers"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UsersQueryInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"users"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/packages/hub/test/gql-gen/index.ts b/packages/hub/test/gql-gen/index.ts deleted file mode 100644 index f51599168f..0000000000 --- a/packages/hub/test/gql-gen/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./fragment-masking"; -export * from "./gql"; \ No newline at end of file diff --git a/packages/hub/test/graphql/commonQueries.ts b/packages/hub/test/graphql/commonQueries.ts deleted file mode 100644 index 0e4b5886a0..0000000000 --- a/packages/hub/test/graphql/commonQueries.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { graphql } from "../gql-gen"; -import { GroupInviteReaction, MembershipRole } from "../gql-gen/graphql"; -import { - executeCommonOperation, - setCurrentUser, - unsetCurrentUser, -} from "./helpers"; - -export const commonTestQueries = { - myMembership: async (groupSlug: string) => { - const query = graphql(/* GraphQL */ ` - query Test_MyMembership($slug: String!) { - result: group(slug: $slug) { - __typename - ... on Group { - id - myMembership { - id - role - } - } - } - } - `); - const group = await executeCommonOperation(query, { - variables: { slug: groupSlug }, - expectedTypename: "Group", - }); - return group?.myMembership; - }, - - memberships: async (groupSlug: string) => { - const query = graphql(/* GraphQL */ ` - query Test_Memberships($slug: String!) { - result: group(slug: $slug) { - __typename - ... on Group { - id - memberships { - edges { - node { - id - role - user { - slug - } - } - } - } - } - } - } - `); - const group = await executeCommonOperation(query, { - variables: { slug: groupSlug }, - expectedTypename: "Group", - }); - return group?.memberships.edges.map((edge) => edge.node); - }, -}; - -async function createGroup(slug: string) { - const mutation = graphql(/* GraphQL */ ` - mutation Test_CreateGroup($input: MutationCreateGroupInput!) { - result: createGroup(input: $input) { - __typename - ... on Error { - message - } - ... on CreateGroupResult { - group { - id - } - } - } - } - `); - - return await executeCommonOperation(mutation, { - variables: { input: { slug } }, - expectedTypename: "CreateGroupResult", - }); -} - -async function createModel({ - slug, - groupSlug, -}: { - slug: string; - groupSlug?: string; -}) { - const mutation = graphql(/* GraphQL */ ` - mutation Test_CreateModel( - $input: MutationCreateSquiggleSnippetModelInput! - ) { - result: createSquiggleSnippetModel(input: $input) { - __typename - ... on Error { - message - } - ... on CreateSquiggleSnippetModelResult { - model { - id - } - } - } - } - `); - - return await executeCommonOperation(mutation, { - variables: { input: { groupSlug, slug, code: "2+2", version: "dev" } }, - expectedTypename: "CreateSquiggleSnippetModelResult", - }); -} - -// note that the user is unset after this function -async function addMember({ - group, - admin, - user, - role, -}: { - // all of group, admin, user must already exist - group: string; - admin: string; - user: string; - role: MembershipRole; -}) { - await setCurrentUser(admin); - - const { invite } = await executeCommonOperation( - graphql(/* GraphQL */ ` - mutation Test_Invite($input: MutationInviteUserToGroupInput!) { - result: inviteUserToGroup(input: $input) { - __typename - ... on BaseError { - message - } - ... on InviteUserToGroupResult { - invite { - id - } - } - } - } - `), - { - variables: { - input: { group, username: user, role }, - }, - expectedTypename: "InviteUserToGroupResult", - } - ); - - await setCurrentUser(user); - await executeCommonOperation( - graphql(/* GraphQL */ ` - mutation Test_AcceptInvite($input: MutationReactToGroupInviteInput!) { - result: reactToGroupInvite(input: $input) { - __typename - ... on BaseError { - message - } - ... on ReactToGroupInviteResult { - __typename - } - } - } - `), - { - variables: { - input: { inviteId: invite.id, action: GroupInviteReaction.Accept }, - }, - expectedTypename: "ReactToGroupInviteResult", - } - ); - await unsetCurrentUser(); -} - -// These are optimized for convenience; their signatures don't match the underlying mutation's signature. -// Tests use these mutations when testing the mutation is not the goal of a test; instead, mutations are used to create underlying data for testing other queries and mutations. -export const commonTestMutations = { - createGroup, - createModel, - addMember, -}; diff --git a/packages/hub/test/graphql/helpers.ts b/packages/hub/test/graphql/helpers.ts deleted file mode 100644 index 56f9ce9872..0000000000 --- a/packages/hub/test/graphql/helpers.ts +++ /dev/null @@ -1,179 +0,0 @@ -import type { TypedDocumentNode } from "@graphql-typed-document-node/core"; -import { ExecutionResult, print } from "graphql"; -import { createYoga } from "graphql-yoga"; -import { Session } from "next-auth"; - -import { schema } from "@/graphql/schema"; -import { prisma } from "@/prisma"; - -let currentUser: Session["user"] | null; - -// Useful when you need a custom email or to create a user without username. -// In other cases, `setCurrentUser` function below is more convenient. -export async function setCurrentUserObject(user: { - email: string; - username?: string; -}) { - await prisma.user.upsert({ - where: { - email: user.email, - }, - create: { - email: user.email, - ...(user.username - ? { - asOwner: { - create: { - slug: user.username, - }, - }, - } - : {}), - }, - update: {}, - }); - currentUser = user; -} - -export async function setCurrentUser(username: string) { - await setCurrentUserObject({ - email: `${username}@example.com`, - username, - }); -} - -export async function unsetCurrentUser() { - currentUser = null; -} - -beforeEach(unsetCurrentUser); - -const yoga = createYoga({ - schema, - context: async () => { - const session: Session | null = currentUser - ? { - user: currentUser, - expires: "mock", - } - : null; - return { session }; - }, -}); - -// Note: buildHTTPExecutor from @graphql-tools/executor-http, as described in -// https://the-guild.dev/graphql/yoga-server/docs/features/testing, might be more flexible, -// but its executor returns an MaybeAsyncIterable that's hard to unpack. -export async function executeOperation( - operation: TypedDocumentNode, - variables?: TVariables -): Promise["data"]>> { - const response = await Promise.resolve( - yoga.fetch("http://yoga/graphql", { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify({ - query: print(operation), - variables: variables ?? undefined, - }), - }) - ); - const yogaResponse = await response.json(); - - if (!yogaResponse.data) { - throw new Error("No data"); - } - return yogaResponse.data; -} - -type CommonResult = { - // see also: `useAsyncMutation` hook - readonly result: { - readonly __typename: string; - }; -}; - -export async function executeCommonOperation< - TResult extends CommonResult, - const TExpectedTypename extends string, - TVariables, ->( - operation: TypedDocumentNode, - options: { - variables?: TVariables; - expectedTypename: TExpectedTypename; - } -): Promise< - Extract -> { - const { result } = await executeOperation( - operation, - options.variables - ); - - if (result.__typename === options.expectedTypename) { - // we want to return a result before "BaseError" check, because it's valid for the test to expect an error - return result as Extract< - TResult["result"], - { readonly __typename: TExpectedTypename } - >; - } - - if (result.__typename === "BaseError") { - throw new Error( - (result as { message?: string })?.message ?? - `Typename mismatch: ${result.__typename}` - ); - } - - throw new Error(`Unexpected result: ${result.__typename}`); -} - -// returns two wrappers on top of executeCommonOperation -export function createRunners< - TResult extends CommonResult, - const TExpectedTypename extends string, - TVariables, ->( - mutation: TypedDocumentNode, - expectedTypename: TExpectedTypename -) { - const runOk = async (variables: TVariables) => { - return await executeCommonOperation(mutation, { - variables, - expectedTypename, - }); - }; - - const runError = async ( - variables: TVariables, - typename: T - ) => { - return await executeCommonOperation(mutation, { - variables, - expectedTypename: typename, - }); - }; - - return { runOk, runError }; -} - -// common convenient speicalization of createRunners - functions accept input instead of variables -export function createInputRunners< - TResult extends CommonResult, - const TExpectedTypename extends string, - TInput, ->( - mutation: TypedDocumentNode, - expectedTypename: TExpectedTypename -) { - const { runOk, runError } = createRunners(mutation, expectedTypename); - return { - runOk: (input: TInput) => runOk({ input }), - runError: (input: TInput, typename: T) => - runError({ input }, typename), - }; -} diff --git a/packages/hub/test/graphql/mutations/createGroup.test.ts b/packages/hub/test/graphql/mutations/createGroup.test.ts deleted file mode 100644 index 673f77bdf4..0000000000 --- a/packages/hub/test/graphql/mutations/createGroup.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { graphql } from "../../gql-gen"; -import { createRunners, setCurrentUser } from "../helpers"; - -const CreateGroupTest = graphql(/* GraphQL */ ` - mutation CreateGroupTest { - result: createGroup(input: { slug: "testgroup" }) { - __typename - ... on CreateGroupResult { - group { - id - slug - memberships { - edges { - node { - user { - username - } - role - } - } - } - } - } - ... on BaseError { - message - } - } - } -`); - -const { runOk, runError } = createRunners(CreateGroupTest, "CreateGroupResult"); - -test("no auth", async () => { - const result = await runError({}, "BaseError"); - expect(result.message).toMatch("Not authorized"); -}); - -test("basic", async () => { - await setCurrentUser("mockuser"); - const result = await runOk({}); - - expect(result).toMatchObject({ - group: { - slug: "testgroup", - }, - }); - expect(result.group.memberships).toEqual({ - edges: [ - { - node: { - user: { - username: "mockuser", - }, - role: "Admin", - }, - }, - ], - }); -}); diff --git a/packages/hub/test/graphql/mutations/createSquiggleSnippetModel.test.ts b/packages/hub/test/graphql/mutations/createSquiggleSnippetModel.test.ts deleted file mode 100644 index c318d30ded..0000000000 --- a/packages/hub/test/graphql/mutations/createSquiggleSnippetModel.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { graphql } from "../../gql-gen"; -import { commonTestMutations } from "../commonQueries"; -import { createInputRunners, setCurrentUser } from "../helpers"; - -const Mutation = graphql(/* GraphQL */ ` - mutation CreateSquiggleSnippetModelTest( - $input: MutationCreateSquiggleSnippetModelInput! - ) { - result: createSquiggleSnippetModel(input: $input) { - __typename - ... on Error { - message - } - ... on ValidationError { - issues { - message - } - } - ... on CreateSquiggleSnippetModelResult { - model { - id - slug - isPrivate - owner { - __typename - slug - } - } - } - } - } -`); - -const { runOk, runError } = createInputRunners( - Mutation, - "CreateSquiggleSnippetModelResult" -); - -test("no auth", async () => { - const result = await runError( - { code: "2+2", slug: "testmodel", version: "dev" }, - "BaseError" - ); - expect(result.message).toMatch("Not authorized"); -}); - -test("bad slug", async () => { - await setCurrentUser("mockuser"); - const result = await runError( - { code: "2+2", slug: "foo bar", version: "dev" }, - "ValidationError" - ); - expect(result.message).toMatch("[input.slug] Must be alphanumerical"); -}); - -test("basic", async () => { - await setCurrentUser("mockuser"); - const result = await runOk({ - code: "2+2", - slug: "testmodel", - version: "dev", - }); - - expect(result.model.slug).toBe("testmodel"); - expect(result.model.owner.__typename).toBe("User"); - expect(result.model.owner.slug).toBe("mockuser"); - expect(result.model.isPrivate).toBe(false); -}); - -test("private", async () => { - await setCurrentUser("mockuser"); - const result = await runOk({ - code: "2+2", - slug: "testmodel", - version: "dev", - isPrivate: true, - }); - - expect(result.model.slug).toBe("testmodel"); - expect(result.model.owner.__typename).toBe("User"); - expect(result.model.owner.slug).toBe("mockuser"); - expect(result.model.isPrivate).toBe(true); -}); - -test("for group", async () => { - await setCurrentUser("mockuser"); - await commonTestMutations.createGroup("testgroup"); - - const result = await runOk({ - code: "2+2", - slug: "testmodel", - groupSlug: "testgroup", - version: "dev", - }); - - expect(result.model.owner.__typename).toBe("Group"); - expect(result.model.owner.slug).toBe("testgroup"); -}); - -test("for group with bad slug", async () => { - await setCurrentUser("mockuser"); - - const result = await runError( - { - code: "2+2", - slug: "testmodel", - groupSlug: "no such group", - version: "dev", - }, - "ValidationError" - ); - - expect(result.message).toMatch("[input.groupSlug] Must be alphanumerical"); -}); - -test("duplicate", async () => { - await setCurrentUser("mockuser"); - - await runOk({ code: "2+2", slug: "testmodel", version: "dev" }); - - const result = await runError( - { code: "2+2", slug: "testmodel", version: "dev" }, - "BaseError" - ); - - expect(result.message).toMatch( - "Model testmodel already exists on this account" - ); -}); diff --git a/packages/hub/test/graphql/mutations/deleteMembership.test.ts b/packages/hub/test/graphql/mutations/deleteMembership.test.ts deleted file mode 100644 index a56d8c8a28..0000000000 --- a/packages/hub/test/graphql/mutations/deleteMembership.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { graphql } from "../../gql-gen"; -import { MembershipRole } from "../../gql-gen/graphql"; -import { commonTestMutations, commonTestQueries } from "../commonQueries"; -import { createRunners, setCurrentUser } from "../helpers"; - -const { runOk, runError } = createRunners( - graphql(/* GraphQL */ ` - mutation DeleteMembershipTest($group: String!, $user: String!) { - result: deleteMembership(input: { group: $group, user: $user }) { - __typename - ... on BaseError { - message - } - ... on DeleteMembershipResult { - ok - } - } - } - `), - "DeleteMembershipResult" -); - -async function commonConfiguration() { - const group = "testgroup"; - const admin = "mockadmin"; - await setCurrentUser(admin); - await setCurrentUser("mockadmin2"); - await setCurrentUser("mockuser"); - await setCurrentUser("mockuser2"); - - await setCurrentUser(admin); - await commonTestMutations.createGroup(group); - - await commonTestMutations.addMember({ - group, - admin, - user: "mockuser", - role: MembershipRole.Member, - }); - await commonTestMutations.addMember({ - group, - admin, - user: "mockuser2", - role: MembershipRole.Member, - }); - await commonTestMutations.addMember({ - group, - admin, - user: "mockadmin2", - role: MembershipRole.Admin, - }); -} - -test("member deletes self", async () => { - await commonConfiguration(); - - // sanity check - expect( - (await commonTestQueries.memberships("testgroup")) - .map((m) => m.user.slug) - .sort() - ).toEqual(["mockadmin", "mockadmin2", "mockuser", "mockuser2"].sort()); - - await setCurrentUser("mockuser"); - - const result = await runOk({ - group: "testgroup", - user: "mockuser", - }); - expect(result.ok).toBe(true); - - expect( - (await commonTestQueries.memberships("testgroup")) - .map((m) => m.user.slug) - .sort() - ).toEqual(["mockadmin", "mockadmin2", "mockuser2"].sort()); -}); - -test("admin deletes member", async () => { - await commonConfiguration(); - - await setCurrentUser("mockadmin"); - - const result = await runOk({ - group: "testgroup", - user: "mockuser", - }); - expect(result.ok).toBe(true); - - expect( - (await commonTestQueries.memberships("testgroup")) - .map((m) => m.user.slug) - .sort() - ).toEqual(["mockadmin", "mockadmin2", "mockuser2"].sort()); -}); - -test("member can't delete admin", async () => { - await commonConfiguration(); - - await setCurrentUser("mockuser"); - - const result = await runError( - { - group: "testgroup", - user: "mockadmin", - }, - "BaseError" - ); - expect(result.message).toBe("Only admins can delete other members"); - - expect( - (await commonTestQueries.memberships("testgroup")) - .map((m) => m.user.slug) - .sort() - ).toEqual(["mockadmin", "mockadmin2", "mockuser", "mockuser2"].sort()); -}); - -test("no such member", async () => { - await commonConfiguration(); - - await setCurrentUser("another-user"); - await setCurrentUser("mockadmin"); - - const result = await runError( - { - group: "testgroup", - user: "another-user", - }, - "BaseError" - ); - expect(result.message).toBe("another-user is not a member of testgroup"); -}); - -test("not signed in", async () => { - await commonConfiguration(); - const result = await runError( - { group: "testgroup", user: "mockadmin" }, - "BaseError" - ); - expect(result.message).toMatch("Not authorized"); -}); - -test("not a member", async () => { - await commonConfiguration(); - await setCurrentUser("another-user"); - const result = await runError( - { group: "testgroup", user: "mockadmin" }, - "BaseError" - ); - expect(result.message).toBe("You're not a member of this group"); -}); - -test("delete self as last user", async () => { - await setCurrentUser("mockuser"); - await commonTestMutations.createGroup("testgroup"); - const result = await runError( - { group: "testgroup", user: "mockuser" }, - "BaseError" - ); - expect(result.message).toBe( - "Can't delete, mockuser is the last admin of testgroup" - ); -}); diff --git a/packages/hub/test/graphql/mutations/deleteModel.test.ts b/packages/hub/test/graphql/mutations/deleteModel.test.ts deleted file mode 100644 index 859038e6f6..0000000000 --- a/packages/hub/test/graphql/mutations/deleteModel.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { graphql } from "../../gql-gen"; -import { commonTestMutations } from "../commonQueries"; -import { createInputRunners, setCurrentUser } from "../helpers"; - -const Mutation = graphql(/* GraphQL */ ` - mutation DeleteModelTest($input: MutationDeleteModelInput!) { - result: deleteModel(input: $input) { - __typename - ... on Error { - message - } - ... on NotFoundError { - message - } - ... on DeleteModelResult { - ok - } - } - } -`); - -const { runOk, runError } = createInputRunners(Mutation, "DeleteModelResult"); - -test("no auth", async () => { - const result = await runError( - { owner: "testuser", slug: "testmodel" }, - "BaseError" - ); - expect(result.message).toMatch("Not authorized"); -}); - -test("no such model", async () => { - await setCurrentUser("mockuser"); - const result = await runError( - { owner: "testuser", slug: "testmodel" }, - "NotFoundError" - ); - expect(result.message).toMatch("Can't find model"); -}); - -test("ok", async () => { - await setCurrentUser("mockuser"); - await commonTestMutations.createModel({ slug: "testmodel" }); - - const result = await runOk({ owner: "mockuser", slug: "testmodel" }); - expect(result.ok).toBe(true); -}); - -test("double delete", async () => { - const owner = "mockuser"; - const slug = "testmodel"; - await setCurrentUser(owner); - await commonTestMutations.createModel({ slug }); - - await runOk({ owner, slug }); - const result = await runError({ owner, slug }, "NotFoundError"); - - expect(result.message).toMatch("Can't find model"); -}); - -test("wrong user", async () => { - const owner = "mockuser"; - const otherUser = "user2"; - await setCurrentUser(owner); - await commonTestMutations.createModel({ slug: "testmodel" }); - await setCurrentUser(otherUser); - - const result = await runError( - { owner, slug: "testmodel" }, - "NotFoundError" // TODO - error should be more specific - ); - expect(result.message).toMatch("Can't find model"); -}); - -test("deleting group model", async () => { - const groupSlug = "group1"; - - await setCurrentUser("mockuser"); - await commonTestMutations.createGroup(groupSlug); - await commonTestMutations.createModel({ groupSlug, slug: "testmodel" }); - - const result = await runOk({ owner: groupSlug, slug: "testmodel" }); - expect(result.ok).toBe(true); -}); diff --git a/packages/hub/test/graphql/mutations/inviteUserToGroup.test.ts b/packages/hub/test/graphql/mutations/inviteUserToGroup.test.ts deleted file mode 100644 index 31b6cb2954..0000000000 --- a/packages/hub/test/graphql/mutations/inviteUserToGroup.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { graphql } from "../../gql-gen"; -import { MembershipRole } from "../../gql-gen/graphql"; -import { commonTestMutations } from "../commonQueries"; -import { - createInputRunners, - setCurrentUser, - unsetCurrentUser, -} from "../helpers"; - -const ownerUser = "mockowner"; -const memberUser = "mockmember"; - -async function prepareGroup() { - await setCurrentUser(ownerUser); - await commonTestMutations.createGroup("testgroup"); -} - -const { runOk, runError } = createInputRunners( - graphql(/* GraphQL */ ` - mutation InviteTest($input: MutationInviteUserToGroupInput!) { - result: inviteUserToGroup(input: $input) { - __typename - ... on BaseError { - message - } - ... on InviteUserToGroupResult { - invite { - id - role - } - } - } - } - `), - "InviteUserToGroupResult" -); - -test("basic invite", async () => { - await prepareGroup(); - await setCurrentUser(memberUser); // create a user - await setCurrentUser(ownerUser); - const result = await runOk({ - group: "testgroup", - username: "mockmember", - role: MembershipRole.Member, - }); - expect(result.invite.role).toBe("Member"); -}); - -test("no auth", async () => { - await prepareGroup(); - await unsetCurrentUser(); - - const result = await runError( - { - group: "testgroup", - username: "mockmember", - role: MembershipRole.Member, - }, - "BaseError" - ); - expect(result.message).toMatch("Not authorized"); -}); - -test("invite to another's group", async () => { - await prepareGroup(); - await setCurrentUser(memberUser); - - const result = await runError( - { - group: "testgroup", - username: "mockmember", - role: MembershipRole.Member, - }, - "BaseError" - ); - expect(result.message).toMatch("You're not a member of testgroup group"); -}); - -test("invite to nonexistent group", async () => { - await prepareGroup(); - await setCurrentUser(memberUser); // create a user - await setCurrentUser(ownerUser); - - const result = await runError( - { - group: "nosuchgroup", - username: "mockmember", - role: MembershipRole.Member, - }, - "BaseError" - ); - expect(result.message).toMatch("Group nosuchgroup not found"); -}); - -test("invite nonexistent user", async () => { - await prepareGroup(); - await setCurrentUser(ownerUser); - - const result = await runError( - { - group: "testgroup", - username: "nosuchuser", - role: MembershipRole.Member, - }, - "BaseError" - ); - expect(result.message).toMatch("Invited user nosuchuser not found"); -}); - -test("existing invite", async () => { - await prepareGroup(); - await setCurrentUser(memberUser); // create a user - await setCurrentUser(ownerUser); - - await runOk({ - group: "testgroup", - username: memberUser, - role: MembershipRole.Member, - }); - const result = await runError( - { - group: "testgroup", - username: memberUser, - role: MembershipRole.Member, - }, - "BaseError" - ); - expect(result.message).toMatch( - "There's already a pending invite for mockmember to join testgroup" - ); -}); - -test("already a member", async () => { - await prepareGroup(); - await setCurrentUser(memberUser); - await setCurrentUser(ownerUser); - await commonTestMutations.addMember({ - group: "testgroup", - admin: ownerUser, - user: memberUser, - role: MembershipRole.Member, - }); - - await setCurrentUser(ownerUser); - const result = await runError( - { - group: "testgroup", - username: memberUser, - role: MembershipRole.Member, - }, - "BaseError" - ); - expect(result.message).toMatch("mockmember is already a member of testgroup"); -}); diff --git a/packages/hub/test/graphql/mutations/setUsername.test.ts b/packages/hub/test/graphql/mutations/setUsername.test.ts deleted file mode 100644 index c7cc601294..0000000000 --- a/packages/hub/test/graphql/mutations/setUsername.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { graphql } from "../../gql-gen"; -import { createRunners, setCurrentUserObject } from "../helpers"; - -const SetUsernameTest = graphql(/* GraphQL */ ` - mutation SetUsernameTest($username: String!) { - result: setUsername(username: $username) { - __typename - ... on Error { - message - } - ... on ValidationError { - message - } - ... on Me { - email - username - } - } - } -`); - -const { runOk, runError } = createRunners(SetUsernameTest, "Me"); - -test("no auth", async () => { - const result = await runError({ username: "mockuser" }, "BaseError"); - expect(result.message).toMatch("Not authorized"); -}); - -test("already set", async () => { - await setCurrentUserObject({ - email: "mock@example.com", - username: "mockuser", - }); - - const result = await runError({ username: "mockuser2" }, "BaseError"); - expect(result.message).toMatch("Username is already set"); -}); - -test("bad username", async () => { - await setCurrentUserObject({ email: "mock@example.com" }); - const result = await runError({ username: "foo bar" }, "ValidationError"); - expect(result.message).toMatch("[username] Must be alphanumerical"); -}); - -test("not available", async () => { - await setCurrentUserObject({ email: "mock@example.com" }); - await runOk({ username: "mockuser" }); - - await setCurrentUserObject({ email: "mock2@example.com" }); - const result = await runError({ username: "mockuser" }, "BaseError"); - - expect(result.message).toMatch("Username mockuser is not available"); -}); - -test("basic", async () => { - await setCurrentUserObject({ email: "mock@example.com" }); - const result = await runOk({ username: "mockuser" }); - - expect(result).toMatchObject({ - email: "mock@example.com", - username: "mockuser", - }); -}); diff --git a/packages/hub/test/graphql/queries/groups.test.ts b/packages/hub/test/graphql/queries/groups.test.ts deleted file mode 100644 index 62f1d953cc..0000000000 --- a/packages/hub/test/graphql/queries/groups.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { graphql } from "../../gql-gen"; -import { commonTestMutations } from "../commonQueries"; -import { createRunners, setCurrentUser, unsetCurrentUser } from "../helpers"; - -const { runOk, runError } = createRunners( - graphql(/* GraphQL */ ` - query TestGroups($input: GroupsQueryInput!) { - result: groups(first: 10, input: $input) { - __typename - edges { - node { - id - slug - } - } - } - } - `), - "GroupConnection" -); - -test("list groups", async () => { - await setCurrentUser("mockuser"); - await commonTestMutations.createGroup("group1"); - await commonTestMutations.createGroup("group2"); - await commonTestMutations.createGroup("group3"); - const result = await runOk({ input: {} }); - - expect(result.edges.length).toEqual(3); -}); - -test("slugContains", async () => { - await setCurrentUser("mockuser"); - await commonTestMutations.createGroup("foo-bar"); - await commonTestMutations.createGroup("foo-baz"); - await commonTestMutations.createGroup("bar-baz"); - const result = await runOk({ input: { slugContains: "bar" } }); - - expect(result.edges.length).toEqual(2); - expect(result.edges.map((e) => e.node.slug).sort()).toEqual( - ["foo-bar", "bar-baz"].sort() - ); -}); - -test("myOnly", async () => { - await setCurrentUser("user1"); - await commonTestMutations.createGroup("group1"); - await commonTestMutations.createGroup("group4"); - - await setCurrentUser("user2"); - await commonTestMutations.createGroup("group2"); - await commonTestMutations.createGroup("group3"); - - const result2 = await runOk({ input: { myOnly: true } }); - - expect(result2.edges.length).toEqual(2); - expect(result2.edges.map((e) => e.node.slug).sort()).toEqual( - ["group2", "group3"].sort() - ); - - await setCurrentUser("user1"); - const result1 = await runOk({ input: { myOnly: true } }); - expect(result1.edges.length).toEqual(2); - expect(result1.edges.map((e) => e.node.slug).sort()).toEqual( - ["group1", "group4"].sort() - ); -}); - -test("myOnly not signed in", async () => { - await setCurrentUser("user1"); - await commonTestMutations.createGroup("group1"); - await commonTestMutations.createGroup("group4"); - await unsetCurrentUser(); - const result = await runOk({ input: { myOnly: true } }); - expect(result.edges.length).toEqual(0); -}); diff --git a/packages/hub/test/graphql/queries/me.test.ts b/packages/hub/test/graphql/queries/me.test.ts deleted file mode 100644 index 2f3ac758b2..0000000000 --- a/packages/hub/test/graphql/queries/me.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { graphql } from "../../gql-gen"; -import { executeOperation, setCurrentUserObject } from "../helpers"; - -test("basic", async () => { - await setCurrentUserObject({ - email: "mock@example.com", - username: "mockuser", - }); - const { me } = await executeOperation( - graphql(/* GraphQL */ ` - query TestMe { - me { - __typename - email - username - } - } - `) - ); - - expect(me.email).toBe("mock@example.com"); - expect(me.username).toBe("mockuser"); -}); diff --git a/packages/hub/test/graphql/queries/models.test.ts b/packages/hub/test/graphql/queries/models.test.ts deleted file mode 100644 index 980db63810..0000000000 --- a/packages/hub/test/graphql/queries/models.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { graphql } from "../../gql-gen"; -import { - executeCommonOperation, - executeOperation, - setCurrentUser, - unsetCurrentUser, -} from "../helpers"; - -const Query = graphql(/* GraphQL */ ` - query TestModels { - models { - edges { - node { - slug - isEditable - isPrivate - owner { - __typename - slug - } - } - } - } - } -`); - -const CreateModel = graphql(/* GraphQL */ ` - mutation TestModels_createModel( - $input: MutationCreateSquiggleSnippetModelInput! - ) { - result: createSquiggleSnippetModel(input: $input) { - __typename - } - } -`); - -test("empty", async () => { - const { models } = await executeOperation(Query); - expect(models.edges.length).toBe(0); -}); - -const user = "mockuser"; - -async function createModel(slug: string, options: { private?: boolean } = {}) { - await executeCommonOperation(CreateModel, { - variables: { - input: { - code: "2+2", - slug, - version: "dev", - ...(options.private ? { isPrivate: true } : {}), - }, - }, - expectedTypename: "CreateSquiggleSnippetModelResult", - }); -} - -test("list models", async () => { - await setCurrentUser(user); - await createModel("model1"); - await createModel("model2"); - await createModel("model3"); - const { models } = await executeOperation(Query); - - expect(models.edges.length).toBe(3); -}); - -test("default limit", async () => { - await setCurrentUser(user); - for (let i = 0; i < 30; i++) { - await createModel(`model${i}`); - } - const { models } = await executeOperation(Query); - - expect(models.edges.length).toBe(20); -}); - -test("list private models", async () => { - await setCurrentUser(user); - await createModel("model1"); - await createModel("model2"); - await createModel("model3", { private: true }); - const { models } = await executeOperation(Query); - - expect(models.edges.map((edge) => edge.node.slug)).toEqual([ - "model3", - "model2", - "model1", - ]); -}); - -test("hide private models from anon users", async () => { - await setCurrentUser(user); - await createModel("model1"); - await createModel("model2", { private: true }); - await createModel("model3"); - unsetCurrentUser(); - const { models } = await executeOperation(Query); - - expect(models.edges.length).toBe(2); - expect(models.edges.map((edge) => edge.node.slug)).toEqual([ - "model3", - "model1", - ]); -}); - -test("hide private models from other users", async () => { - await setCurrentUser(user); - await createModel("model1"); - await createModel("model2", { private: true }); - await createModel("model3"); - await setCurrentUser("otheruser"); - const { models } = await executeOperation(Query); - - expect(models.edges.length).toBe(2); - expect(models.edges.map((edge) => edge.node.slug)).toEqual([ - "model3", - "model1", - ]); -}); diff --git a/packages/hub/test/graphql/queries/userByUsername.test.ts b/packages/hub/test/graphql/queries/userByUsername.test.ts deleted file mode 100644 index 90635cc268..0000000000 --- a/packages/hub/test/graphql/queries/userByUsername.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { graphql } from "../../gql-gen"; -import { executeCommonOperation, setCurrentUser } from "../helpers"; - -const query = graphql(/* GraphQL */ ` - query TestUserByUsername($username: String!) { - result: userByUsername(username: $username) { - __typename - ... on User { - slug - username - } - ... on Error { - message - } - } - } -`); - -test("find self", async () => { - await setCurrentUser("mockuser"); - const user = await executeCommonOperation(query, { - variables: { username: "mockuser" }, - expectedTypename: "User", - }); - - expect(user.slug).toBe("mockuser"); - expect(user.username).toBe("mockuser"); -}); - -test("find other user", async () => { - await setCurrentUser("mockuser"); - await setCurrentUser("mockuser2"); - await setCurrentUser("mockuser3"); - const user = await executeCommonOperation(query, { - variables: { username: "mockuser2" }, - expectedTypename: "User", - }); - - expect(user.slug).toBe("mockuser2"); - expect(user.username).toBe("mockuser2"); -}); - -test("not found", async () => { - await setCurrentUser("mockuser"); - const error = await executeCommonOperation(query, { - variables: { username: "mockuser2" }, - expectedTypename: "NotFoundError", - }); - - expect(error.message).toBe("User mockuser2 not found"); -}); diff --git a/packages/hub/test/graphql/queries/users.test.ts b/packages/hub/test/graphql/queries/users.test.ts deleted file mode 100644 index 80c109bdcf..0000000000 --- a/packages/hub/test/graphql/queries/users.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { graphql } from "../../gql-gen"; -import { - executeOperation, - setCurrentUser, - setCurrentUserObject, -} from "../helpers"; - -const query = graphql(/* GraphQL */ ` - query TestUsers($input: UsersQueryInput) { - result: users(input: $input) { - edges { - node { - username - } - } - } - } -`); - -test("find users", async () => { - await setCurrentUser("mockuser"); - await setCurrentUser("mockuser2"); - const { result: users } = await executeOperation(query); - - expect(users.edges.length).toBe(2); -}); - -test("skip users without usernames", async () => { - await setCurrentUser("mockuser"); - await setCurrentUser("mockuser2"); - await setCurrentUserObject({ email: "mockuser3@example.com" }); - const { result: users } = await executeOperation(query); - - expect(users.edges.length).toBe(2); -}); - -test("filter by username", async () => { - await setCurrentUser("mockuser"); - await setCurrentUser("mockuser2"); - await setCurrentUserObject({ email: "mockuser3@example.com" }); - await setCurrentUser("unrelated"); - const { result: users } = await executeOperation(query, { - input: { usernameContains: "ock" }, - }); - - expect(users.edges.length).toBe(2); - expect(users.edges.map((e) => e.node.username).sort()).toEqual([ - "mockuser", - "mockuser2", - ]); -}); diff --git a/packages/hub/tsconfig.json b/packages/hub/tsconfig.json index d613339ed0..143fcdd821 100644 --- a/packages/hub/tsconfig.json +++ b/packages/hub/tsconfig.json @@ -3,7 +3,6 @@ "compilerOptions": { "paths": { "@/*": ["./src/*"], - "@gen/*": ["./src/__generated__/*"], }, // type check settings "plugins": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8bdf6c2a60..b625f8eb12 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -412,30 +412,6 @@ importers: '@auth/prisma-adapter': specifier: ^2.7.4 version: 2.7.4(@prisma/client@5.22.0(prisma@5.22.0))(nodemailer@6.9.13) - '@pothos/core': - specifier: ^3.41.1 - version: 3.41.1(graphql@16.8.1) - '@pothos/plugin-errors': - specifier: ^3.11.1 - version: 3.11.1(@pothos/core@3.41.1(graphql@16.8.1))(graphql@16.8.1) - '@pothos/plugin-prisma': - specifier: ^3.65.2 - version: 3.65.2(@pothos/core@3.41.1(graphql@16.8.1))(@prisma/client@5.22.0(prisma@5.22.0))(graphql@16.8.1)(typescript@5.6.3) - '@pothos/plugin-relay': - specifier: ^3.46.0 - version: 3.46.0(@pothos/core@3.41.1(graphql@16.8.1))(graphql@16.8.1) - '@pothos/plugin-scope-auth': - specifier: ^3.22.0 - version: 3.22.0(@pothos/core@3.41.1(graphql@16.8.1))(graphql@16.8.1) - '@pothos/plugin-simple-objects': - specifier: ^3.7.0 - version: 3.7.0(@pothos/core@3.41.1(graphql@16.8.1))(graphql@16.8.1) - '@pothos/plugin-validation': - specifier: ^3.10.1 - version: 3.10.1(@pothos/core@3.41.1(graphql@16.8.1))(graphql@16.8.1)(zod@3.23.8) - '@pothos/plugin-with-input': - specifier: ^3.10.1 - version: 3.10.1(@pothos/core@3.41.1(graphql@16.8.1))(graphql@16.8.1) '@prisma/client': specifier: 5.22.0 version: 5.22.0(prisma@5.22.0) @@ -469,12 +445,6 @@ importers: date-fns: specifier: ^3.6.0 version: 3.6.0 - graphql: - specifier: ^16.8.1 - version: 16.8.1 - graphql-yoga: - specifier: ^5.1.1 - version: 5.1.1(graphql@16.8.1) immutable: specifier: ^4.3.6 version: 4.3.6 @@ -514,15 +484,9 @@ importers: react-markdown: specifier: ^9.0.1 version: 9.0.1(@types/react@18.3.3)(react@18.3.1) - react-relay: - specifier: ^16.2.0 - version: 16.2.0(react@18.3.1) react-select: specifier: ^5.8.0 version: 5.8.0(patch_hash=pok3nxq32ihaf3qpdecuz4j5ea)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - relay-runtime: - specifier: ^16.2.0 - version: 16.2.0 remark-breaks: specifier: ^4.0.0 version: 4.0.0 @@ -536,15 +500,6 @@ importers: specifier: ^3.23.8 version: 3.23.8 devDependencies: - '@graphql-codegen/cli': - specifier: ^5.0.2 - version: 5.0.2(@parcel/watcher@2.4.1)(@types/node@20.12.7)(enquirer@2.3.6)(graphql@16.8.1)(typescript@5.6.3) - '@graphql-codegen/client-preset': - specifier: ^4.2.5 - version: 4.2.5(graphql@16.8.1) - '@graphql-typed-document-node/core': - specifier: ^3.2.0 - version: 3.2.0(graphql@16.8.1) '@parcel/watcher': specifier: ^2.4.1 version: 2.4.1 @@ -572,15 +527,6 @@ importers: '@types/react': specifier: ^18.3.3 version: 18.3.3 - '@types/react-relay': - specifier: ^16.0.6 - version: 16.0.6 - '@types/relay-runtime': - specifier: ^14.1.23 - version: 14.1.23 - babel-plugin-relay: - specifier: ^16.2.0 - version: 16.2.0 dotenv-cli: specifier: ^7.4.2 version: 7.4.2 @@ -611,9 +557,6 @@ importers: prisma: specifier: ^5.22.0 version: 5.22.0 - relay-compiler: - specifier: ^16.2.0 - version: 16.2.0 tailwindcss: specifier: ^3.4.3 version: 3.4.3(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.6.3)) @@ -1345,16 +1288,6 @@ packages: '@anthropic-ai/sdk@0.29.2': resolution: {integrity: sha512-5dwiOPO/AZvhY4bJIG9vjFKU9Kza3hA6VEsbIQg6L9vny2RQIpCFhV50nB9IrG2edZaHZb4HuQ9Wmsn5zgWyZg==} - '@ardatan/relay-compiler@12.0.0': - resolution: {integrity: sha512-9anThAaj1dQr6IGmzBMcfzOQKTa5artjuPmw8NYK/fiGEMjADbSguBY2FMDykt+QhilR3wc9VA/3yVju7JHg7Q==} - hasBin: true - peerDependencies: - graphql: '*' - - '@ardatan/sync-fetch@0.0.1': - resolution: {integrity: sha512-xhlTqH0m31mnsG0tIP4ETgfSB6gXDaYYsUWTrlUV93fFQPI9dd8hE0Ot6MHLCtqgB32hwJAC3YZMWlXZw7AleA==} - engines: {node: '>=14'} - '@auth/core@0.37.2': resolution: {integrity: sha512-kUvzyvkcd6h1vpeMAojK2y7+PAV5H+0Cc9+ZlKYDFhDY31AlvsB+GW5vNO4qE3Y07KeQgvNO9U0QUx/fN62kBw==} peerDependencies: @@ -1456,10 +1389,6 @@ packages: resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.25.4': - resolution: {integrity: sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==} - engines: {node: '>=6.9.0'} - '@babel/compat-data@7.26.2': resolution: {integrity: sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==} engines: {node: '>=6.9.0'} @@ -1468,14 +1397,6 @@ packages: resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} engines: {node: '>=6.9.0'} - '@babel/generator@7.23.6': - resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.24.1': - resolution: {integrity: sha512-DfCRfZsBcrPEHUfuBMgbJ1Ut01Y/itOs+hY2nFLgqsqXd52/iSiVq5TITtUasIUgm+IIKdY2/1I7auiQOEeC9A==} - engines: {node: '>=6.9.0'} - '@babel/generator@7.25.6': resolution: {integrity: sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==} engines: {node: '>=6.9.0'} @@ -1500,10 +1421,6 @@ packages: resolution: {integrity: sha512-C47lC7LIDCnz0h4vai/tpNOI95tCd5ZT3iBt/DBH5lXKHZsyNQv18yf1wIIg2ntiQNgmAvA+DgZ82iW8Qdym8g==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.25.2': - resolution: {integrity: sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==} - engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.25.9': resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} engines: {node: '>=6.9.0'} @@ -1796,20 +1713,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-proposal-class-properties@7.18.6': - resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-object-rest-spread@7.20.7': - resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} engines: {node: '>=6.9.0'} @@ -1837,12 +1740,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-assertions@7.24.7': - resolution: {integrity: sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-assertions@7.26.0': resolution: {integrity: sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==} engines: {node: '>=6.9.0'} @@ -1931,12 +1828,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/plugin-transform-arrow-functions@7.24.7': - resolution: {integrity: sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-arrow-functions@7.25.9': resolution: {integrity: sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==} engines: {node: '>=6.9.0'} @@ -1955,24 +1846,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-block-scoped-functions@7.24.7': - resolution: {integrity: sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-block-scoped-functions@7.25.9': resolution: {integrity: sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-block-scoping@7.24.7': - resolution: {integrity: sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-block-scoping@7.25.9': resolution: {integrity: sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==} engines: {node: '>=6.9.0'} @@ -1997,36 +1876,18 @@ packages: peerDependencies: '@babel/core': ^7.12.0 - '@babel/plugin-transform-classes@7.24.7': - resolution: {integrity: sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-classes@7.25.9': resolution: {integrity: sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-computed-properties@7.24.7': - resolution: {integrity: sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-computed-properties@7.25.9': resolution: {integrity: sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-destructuring@7.24.7': - resolution: {integrity: sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-destructuring@7.25.9': resolution: {integrity: sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==} engines: {node: '>=6.9.0'} @@ -2075,24 +1936,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-for-of@7.24.7': - resolution: {integrity: sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-for-of@7.25.9': resolution: {integrity: sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-function-name@7.24.7': - resolution: {integrity: sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-function-name@7.25.9': resolution: {integrity: sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==} engines: {node: '>=6.9.0'} @@ -2105,12 +1954,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-literals@7.24.7': - resolution: {integrity: sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-literals@7.25.9': resolution: {integrity: sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==} engines: {node: '>=6.9.0'} @@ -2123,12 +1966,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-member-expression-literals@7.24.7': - resolution: {integrity: sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-member-expression-literals@7.25.9': resolution: {integrity: sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==} engines: {node: '>=6.9.0'} @@ -2207,12 +2044,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-object-super@7.24.7': - resolution: {integrity: sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-object-super@7.25.9': resolution: {integrity: sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==} engines: {node: '>=6.9.0'} @@ -2237,12 +2068,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-parameters@7.24.7': - resolution: {integrity: sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-parameters@7.25.9': resolution: {integrity: sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==} engines: {node: '>=6.9.0'} @@ -2267,12 +2092,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-property-literals@7.24.7': - resolution: {integrity: sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-property-literals@7.25.9': resolution: {integrity: sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==} engines: {node: '>=6.9.0'} @@ -2321,24 +2140,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-shorthand-properties@7.24.7': - resolution: {integrity: sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-shorthand-properties@7.25.9': resolution: {integrity: sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-spread@7.24.7': - resolution: {integrity: sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-spread@7.25.9': resolution: {integrity: sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==} engines: {node: '>=6.9.0'} @@ -2351,12 +2158,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-template-literals@7.24.7': - resolution: {integrity: sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-template-literals@7.25.9': resolution: {integrity: sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==} engines: {node: '>=6.9.0'} @@ -2465,10 +2266,6 @@ packages: resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} engines: {node: '>=6.9.0'} - '@babel/template@7.23.9': - resolution: {integrity: sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==} - engines: {node: '>=6.9.0'} - '@babel/template@7.24.0': resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} engines: {node: '>=6.9.0'} @@ -2481,10 +2278,6 @@ packages: resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.24.1': - resolution: {integrity: sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==} - engines: {node: '>=6.9.0'} - '@babel/traverse@7.24.5': resolution: {integrity: sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==} engines: {node: '>=6.9.0'} @@ -2761,14 +2554,6 @@ packages: '@emotion/weak-memoize@0.3.1': resolution: {integrity: sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==} - '@envelop/core@5.0.0': - resolution: {integrity: sha512-aJdnH/ptv+cvwfvciCBe7TSvccBwo9g0S5f6u35TBVzRVqIGkK03lFlIL+x1cnfZgN9EfR2b1PH2galrT1CdCQ==} - engines: {node: '>=18.0.0'} - - '@envelop/types@5.0.0': - resolution: {integrity: sha512-IPjmgSc4KpQRlO4qbEDnBEixvtb06WDmjKfi/7fkZaryh5HuOmTtixe1EupQI5XfXO8joc3d27uUZ0QdC++euA==} - engines: {node: '>=18.0.0'} - '@esbuild-plugins/node-resolve@0.2.2': resolution: {integrity: sha512-+t5FdX3ATQlb53UFDBRb4nqjYBz492bIrnVWvpQHpzZlu9BQL5HasMZhqc409ygUwOWCXZhrWr6NyZ6T6Y+cxw==} peerDependencies: @@ -3406,227 +3191,6 @@ packages: '@content-collections/mdx': 0.x.x fumadocs-core: ^13.2.1 || ^14.0.0 - '@graphql-codegen/add@5.0.2': - resolution: {integrity: sha512-ouBkSvMFUhda5VoKumo/ZvsZM9P5ZTyDsI8LW18VxSNWOjrTeLXBWHG8Gfaai0HwhflPtCYVABbriEcOmrRShQ==} - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-codegen/cli@5.0.2': - resolution: {integrity: sha512-MBIaFqDiLKuO4ojN6xxG9/xL9wmfD3ZjZ7RsPjwQnSHBCUXnEkdKvX+JVpx87Pq29Ycn8wTJUguXnTZ7Di0Mlw==} - hasBin: true - peerDependencies: - '@parcel/watcher': ^2.1.0 - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - peerDependenciesMeta: - '@parcel/watcher': - optional: true - - '@graphql-codegen/client-preset@4.2.5': - resolution: {integrity: sha512-hAdB6HN8EDmkoBtr0bPUN/7NH6svzqbcTDMWBCRXPESXkl7y80po+IXrXUjsSrvhKG8xkNXgJNz/2mjwHzywcA==} - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-codegen/core@4.0.2': - resolution: {integrity: sha512-IZbpkhwVqgizcjNiaVzNAzm/xbWT6YnGgeOLwVjm4KbJn3V2jchVtuzHH09G5/WkkLSk2wgbXNdwjM41JxO6Eg==} - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-codegen/gql-tag-operations@4.0.6': - resolution: {integrity: sha512-y6iXEDpDNjwNxJw3WZqX1/Znj0QHW7+y8O+t2V8qvbTT+3kb2lr9ntc8By7vCr6ctw9tXI4XKaJgpTstJDOwFA==} - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-codegen/plugin-helpers@5.0.3': - resolution: {integrity: sha512-yZ1rpULIWKBZqCDlvGIJRSyj1B2utkEdGmXZTBT/GVayP4hyRYlkd36AJV/LfEsVD8dnsKL5rLz2VTYmRNlJ5Q==} - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-codegen/schema-ast@4.0.2': - resolution: {integrity: sha512-5mVAOQQK3Oz7EtMl/l3vOQdc2aYClUzVDHHkMvZlunc+KlGgl81j8TLa+X7ANIllqU4fUEsQU3lJmk4hXP6K7Q==} - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-codegen/typed-document-node@5.0.6': - resolution: {integrity: sha512-US0J95hOE2/W/h42w4oiY+DFKG7IetEN1mQMgXXeat1w6FAR5PlIz4JrRrEkiVfVetZ1g7K78SOwBD8/IJnDiA==} - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-codegen/typescript-operations@4.2.0': - resolution: {integrity: sha512-lmuwYb03XC7LNRS8oo9M4/vlOrq/wOKmTLBHlltK2YJ1BO/4K/Q9Jdv/jDmJpNydHVR1fmeF4wAfsIp1f9JibA==} - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-codegen/typescript@4.0.6': - resolution: {integrity: sha512-IBG4N+Blv7KAL27bseruIoLTjORFCT3r+QYyMC3g11uY3/9TPpaUyjSdF70yBe5GIQ6dAgDU+ENUC1v7EPi0rw==} - peerDependencies: - graphql: ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-codegen/visitor-plugin-common@5.1.0': - resolution: {integrity: sha512-eamQxtA9bjJqI2lU5eYoA1GbdMIRT2X8m8vhWYsVQVWD3qM7sx/IqJU0kx0J3Vd4/CSd36BzL6RKwksibytDIg==} - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@graphql-tools/apollo-engine-loader@8.0.0': - resolution: {integrity: sha512-axQTbN5+Yxs1rJ6cWQBOfw3AEeC+fvIuZSfJLPLLvFJLj4pUm9fhxey/g6oQZAAQJqKPfw+tLDUQvnfvRK8Kmg==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/batch-execute@9.0.2': - resolution: {integrity: sha512-Y2uwdZI6ZnatopD/SYfZ1eGuQFI7OU2KGZ2/B/7G9ISmgMl5K+ZZWz/PfIEXeiHirIDhyk54s4uka5rj2xwKqQ==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/code-file-loader@8.0.2': - resolution: {integrity: sha512-AKNpkElUL2cWocYpC4DzNEpo6qJw8Lp+L3bKQ/mIfmbsQxgLz5uve6zHBMhDaFPdlwfIox41N3iUSvi77t9e8A==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/delegate@10.0.3': - resolution: {integrity: sha512-Jor9oazZ07zuWkykD3OOhT/2XD74Zm6Ar0ENZMk75MDD51wB2UWUIMljtHxbJhV5A6UBC2v8x6iY0xdCGiIlyw==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/documents@1.0.0': - resolution: {integrity: sha512-rHGjX1vg/nZ2DKqRGfDPNC55CWZBMldEVcH+91BThRa6JeT80NqXknffLLEZLRUxyikCfkwMsk6xR3UNMqG0Rg==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/executor-graphql-ws@1.1.0': - resolution: {integrity: sha512-yM67SzwE8rYRpm4z4AuGtABlOp9mXXVy6sxXnTJRoYIdZrmDbKVfIY+CpZUJCqS0FX3xf2+GoHlsj7Qswaxgcg==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/executor-http@1.0.2': - resolution: {integrity: sha512-JKTB4E3kdQM2/1NEcyrVPyQ8057ZVthCV5dFJiKktqY9IdmF00M8gupFcW3jlbM/Udn78ickeUBsUzA3EouqpA==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/executor-legacy-ws@1.0.3': - resolution: {integrity: sha512-rr3IDeO9Dh+8u8KIro++5kzJJYPHkcrIAWzqXtN663nhInC85iW7Ko91yOYwf7ovBci/7s+4Rqe4ZRyca1LGjQ==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/executor@1.1.0': - resolution: {integrity: sha512-+1wmnaUHETSYxiK/ELsT60x584Rw3QKBB7F/7fJ83HKPnLifmE2Dm/K9Eyt6L0Ppekf1jNUbWBpmBGb8P5hAeg==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/git-loader@8.0.2': - resolution: {integrity: sha512-AuCB0nlPvsHh8u42zRZdlD/ZMaWP9A44yAkQUVCZir1E/LG63fsZ9svTWJ+CbusW3Hd0ZP9qpxEhlHxnd4Tlsg==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/github-loader@8.0.0': - resolution: {integrity: sha512-VuroArWKcG4yaOWzV0r19ElVIV6iH6UKDQn1MXemND0xu5TzrFme0kf3U9o0YwNo0kUYEk9CyFM0BYg4he17FA==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/graphql-file-loader@8.0.0': - resolution: {integrity: sha512-wRXj9Z1IFL3+zJG1HWEY0S4TXal7+s1vVhbZva96MSp0kbb/3JBF7j0cnJ44Eq0ClccMgGCDFqPFXty4JlpaPg==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/graphql-tag-pluck@8.0.2': - resolution: {integrity: sha512-U6fE4yEHxuk/nqmPixHpw1WhqdS6aYuaV60m1bEmUmGJNbpAhaMBy01JncpvpF15yZR5LZ0UjkHg+A3Lhoc8YQ==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/import@7.0.0': - resolution: {integrity: sha512-NVZiTO8o1GZs6OXzNfjB+5CtQtqsZZpQOq+Uu0w57kdUkT4RlQKlwhT8T81arEsbV55KpzkpFsOZP7J1wdmhBw==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/json-file-loader@8.0.0': - resolution: {integrity: sha512-ki6EF/mobBWJjAAC84xNrFMhNfnUFD6Y0rQMGXekrUgY0NdeYXHU0ZUgHzC9O5+55FslqUmAUHABePDHTyZsLg==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/load@8.0.0': - resolution: {integrity: sha512-Cy874bQJH0FP2Az7ELPM49iDzOljQmK1PPH6IuxsWzLSTxwTqd8dXA09dcVZrI7/LsN26heTY2R8q2aiiv0GxQ==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/merge@9.0.0': - resolution: {integrity: sha512-J7/xqjkGTTwOJmaJQJ2C+VDBDOWJL3lKrHJN4yMaRLAJH3PosB7GiPRaSDZdErs0+F77sH2MKs2haMMkywzx7Q==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/optimize@2.0.0': - resolution: {integrity: sha512-nhdT+CRGDZ+bk68ic+Jw1OZ99YCDIKYA5AlVAnBHJvMawSx9YQqQAIj4refNc1/LRieGiuWvhbG3jvPVYho0Dg==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/prisma-loader@8.0.1': - resolution: {integrity: sha512-bl6e5sAYe35Z6fEbgKXNrqRhXlCJYeWKBkarohgYA338/SD9eEhXtg3Cedj7fut3WyRLoQFpHzfiwxKs7XrgXg==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/relay-operation-optimizer@7.0.0': - resolution: {integrity: sha512-UNlJi5y3JylhVWU4MBpL0Hun4Q7IoJwv9xYtmAz+CgRa066szzY7dcuPfxrA7cIGgG/Q6TVsKsYaiF4OHPs1Fw==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/schema@10.0.0': - resolution: {integrity: sha512-kf3qOXMFcMs2f/S8Y3A8fm/2w+GaHAkfr3Gnhh2LOug/JgpY/ywgFVxO3jOeSpSEdoYcDKLcXVjMigNbY4AdQg==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/url-loader@8.0.0': - resolution: {integrity: sha512-rPc9oDzMnycvz+X+wrN3PLrhMBQkG4+sd8EzaFN6dypcssiefgWKToXtRKI8HHK68n2xEq1PyrOpkjHFJB+GwA==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/utils@10.0.6': - resolution: {integrity: sha512-hZMjl/BbX10iagovakgf3IiqArx8TPsotq5pwBld37uIX1JiZoSbgbCIFol7u55bh32o6cfDEiiJgfAD5fbeyQ==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/wrap@10.0.1': - resolution: {integrity: sha512-Cw6hVrKGM2OKBXeuAGltgy4tzuqQE0Nt7t/uAqnuokSXZhMHXJUb124Bnvxc2gPZn5chfJSDafDe4Cp8ZAVJgg==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-typed-document-node/core@3.2.0': - resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-yoga/logger@2.0.0': - resolution: {integrity: sha512-Mg8psdkAp+YTG1OGmvU+xa6xpsAmSir0hhr3yFYPyLNwzUj95DdIwsMpKadDj9xDpYgJcH3Hp/4JMal9DhQimA==} - engines: {node: '>=18.0.0'} - - '@graphql-yoga/subscription@5.0.0': - resolution: {integrity: sha512-Ri7sK8hmxd/kwaEa0YT8uqQUb2wOLsmBMxI90QDyf96lzOMJRgBuNYoEkU1pSgsgmW2glceZ96sRYfaXqwVxUw==} - engines: {node: '>=18.0.0'} - - '@graphql-yoga/typed-event-target@3.0.0': - resolution: {integrity: sha512-w+liuBySifrstuHbFrHoHAEyVnDFVib+073q8AeAJ/qqJfvFvAwUPLLtNohR/WDVRgSasfXtl3dcNuVJWN+rjg==} - engines: {node: '>=18.0.0'} - '@headlessui/react@2.2.0': resolution: {integrity: sha512-RzCEg+LXsuI7mHiSomsu/gBJSjpupm6A1qIZ5sWjd7JhARNlMiSA4kKfJpCKwU9tE+zMRterhhrP74PvfJrpXQ==} engines: {node: '>=10'} @@ -4208,17 +3772,6 @@ packages: resolution: {integrity: sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==} engines: {node: '>= 10.0.0'} - '@peculiar/asn1-schema@2.3.6': - resolution: {integrity: sha512-izNRxPoaeJeg/AyH8hER6s+H7p4itk+03QCa4sbxI3lNdseQYCuxzgsuNK8bTXChtLTjpJz6NmXKA73qLa3rCA==} - - '@peculiar/json-schema@1.1.12': - resolution: {integrity: sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==} - engines: {node: '>=8.0.0'} - - '@peculiar/webcrypto@1.4.3': - resolution: {integrity: sha512-VtaY4spKTdN5LjJ04im/d/joXuvLbQdgy5Z4DXF4MFZhQ+MTrejbNMkfZBp1Bs3O5+bFqnJgyGdPuZQflvIa5A==} - engines: {node: '>=10.12.0'} - '@peggyjs/from-mem@1.2.1': resolution: {integrity: sha512-qh5zG8WKT36142/FqOYtpF0scRR3ZJ3H5XST1bJ/KV2FvyB5MvUB/tB9ZjihRe1iKjJD4PBOZczzwEx7hJtgMw==} engines: {node: '>=18'} @@ -4231,57 +3784,6 @@ packages: resolution: {integrity: sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@pothos/core@3.41.1': - resolution: {integrity: sha512-K+TGTK2Q7rmLU9WaC1cSDiGZaU9M+gHNbCYBom2W1vHuEYDUAiihVHz9tXYsrYjFMJSK+wLJ7Xp2374bQa9x/w==} - peerDependencies: - graphql: '>=15.1.0' - - '@pothos/plugin-errors@3.11.1': - resolution: {integrity: sha512-ty+L1yfGartuuIMszGZyDQtzwpt/7ijVbnmGfOZdcQZOlk1P0MUr1iJ56Z5QhaHwZuwyVBF17gwP7ZroGs9htg==} - peerDependencies: - '@pothos/core': '*' - graphql: '>=15.1.0' - - '@pothos/plugin-prisma@3.65.2': - resolution: {integrity: sha512-Ahv2UfHhKINNwjWw7kO3esg7CFePg4xEUrvvSPqI6/Qt+XmvA0btXpN/lYknRoz93M/BuJLYoH15DslHCazhWA==} - hasBin: true - peerDependencies: - '@pothos/core': '*' - '@prisma/client': '*' - graphql: 16.8.1 - typescript: '>=4.7.2' - - '@pothos/plugin-relay@3.46.0': - resolution: {integrity: sha512-EOmyqAsMy/vWgitPWcQRDUvLBcZF5M/RrFxoFeI/VWnRpUJPr8XmHh28jb70G8Wzfg56btPpTY440doSKb60og==} - peerDependencies: - '@pothos/core': '*' - graphql: '>=15.1.0' - - '@pothos/plugin-scope-auth@3.22.0': - resolution: {integrity: sha512-iT+JT0uIvoVGuQeQ2Q/y+wwP27SQH+1Hs8Uz5rrtGOblsKGW0cRcJYuXMi5+XJFXOY3CPNMnv6aroetxZkEcEQ==} - peerDependencies: - '@pothos/core': '*' - graphql: '>=15.1.0' - - '@pothos/plugin-simple-objects@3.7.0': - resolution: {integrity: sha512-CgZJLaHLt1Q30j+XCiWV6qVJcae1ksiUFdi8kz4sEsOhCNfdVwz64s8rx7SSqsdmPbjb68dJIDKpq2Qg9VZb8g==} - peerDependencies: - '@pothos/core': '*' - graphql: '>=15.1.0' - - '@pothos/plugin-validation@3.10.1': - resolution: {integrity: sha512-FdYHzVSbrHZ4cDPOW/RB8Zsyx7qNuybAbC5DO3zVSsPPgY/FWLNWtQ6CaE+tkg5Je1RxpmO5od7Ig1Wt+akqnA==} - peerDependencies: - '@pothos/core': '*' - graphql: '>=15.1.0' - zod: '*' - - '@pothos/plugin-with-input@3.10.1': - resolution: {integrity: sha512-1Vz1EWMgNO/F37LHxkMr6V5w7lqiDiIvkonSZvOAbLUzOpwbrAgf36j2NrXFWahTN8Q6lPt4x9pNHnBeH18pAg==} - peerDependencies: - '@pothos/core': '*' - graphql: '>=15.1.0' - '@prisma/client@5.22.0': resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==} engines: {node: '>=16.13'} @@ -4291,9 +3793,6 @@ packages: prisma: optional: true - '@prisma/debug@5.15.0': - resolution: {integrity: sha512-QpEAOjieLPc/4sMny/WrWqtpIAmBYsgqwWlWwIctqZO0AbhQ9QcT6x2Ut3ojbDo/pFRCCA1Z1+xm2MUy7fAkZA==} - '@prisma/debug@5.22.0': resolution: {integrity: sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==} @@ -4306,9 +3805,6 @@ packages: '@prisma/fetch-engine@5.22.0': resolution: {integrity: sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==} - '@prisma/generator-helper@5.15.0': - resolution: {integrity: sha512-7pB3v57GU4Q/iBauGbvQQGenMJSu2ArQboge4Ca6bw0gA7nConfIHP48MdNIYCrBbNPcIVFmrNomyhqCb3IuWQ==} - '@prisma/get-platform@5.22.0': resolution: {integrity: sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==} @@ -5053,9 +4549,6 @@ packages: react: '>=17' react-dom: '>=17' - '@repeaterjs/repeater@3.0.4': - resolution: {integrity: sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA==} - '@rollup/pluginutils@5.1.0': resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} engines: {node: '>=14.0.0'} @@ -5815,15 +5308,9 @@ packages: '@types/js-cookie@2.2.7': resolution: {integrity: sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==} - '@types/js-yaml@4.0.5': - resolution: {integrity: sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==} - '@types/jsdom@20.0.1': resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} - '@types/json-stable-stringify@1.0.34': - resolution: {integrity: sha512-s2cfwagOQAS8o06TcwKfr9Wx11dNGbH2E9vJz1cqV+a/LOyhWNLUNd6JSRYNzvB4d29UuJX2M0Dj9vE1T8fRXw==} - '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} @@ -5902,18 +5389,12 @@ packages: '@types/react-dom@18.3.0': resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} - '@types/react-relay@16.0.6': - resolution: {integrity: sha512-VTntVQJhlwQYNUlbNgGf8RYy7EtQPRZqsD/w2Si0ygZspJXuNlVdRkklWMFN99EMRhHDpqlNHD8i3wIs7QRz9g==} - '@types/react-transition-group@4.4.6': resolution: {integrity: sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==} '@types/react@18.3.3': resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==} - '@types/relay-runtime@14.1.23': - resolution: {integrity: sha512-tP2l6YLI2HJ11UzEB7j4IWeADyiPIKTehdeyHsyOzNBu7WvKsyf4kAZDmsB2NPaXp9Lud+KEJbRi/VW+jEDYCA==} - '@types/resolve@1.20.6': resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} @@ -5950,9 +5431,6 @@ packages: '@types/vscode@1.86.0': resolution: {integrity: sha512-DnIXf2ftWv+9LWOB5OJeIeaLigLHF7fdXF6atfc7X5g2w/wVZBgk0amP7b+ub5xAuW1q7qP5YcFvOcit/DtyCQ==} - '@types/ws@8.5.5': - resolution: {integrity: sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==} - '@types/yargs-parser@21.0.0': resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} @@ -6142,31 +5620,6 @@ packages: engines: {node: '>= 16'} hasBin: true - '@whatwg-node/events@0.0.3': - resolution: {integrity: sha512-IqnKIDWfXBJkvy/k6tzskWTc2NK3LcqHlb+KHGCrjOCH4jfQckRX0NAiIcC/vIqQkzLYw2r2CTSwAxcrtcD6lA==} - - '@whatwg-node/events@0.1.1': - resolution: {integrity: sha512-AyQEn5hIPV7Ze+xFoXVU3QTHXVbWPrzaOkxtENMPMuNL6VVHrp4hHfDt9nrQpjO7BgvuM95dMtkycX5M/DZR3w==} - engines: {node: '>=16.0.0'} - - '@whatwg-node/fetch@0.8.8': - resolution: {integrity: sha512-CdcjGC2vdKhc13KKxgsc6/616BQ7ooDIgPeTuAiE8qfCnS0mGzcfCOoZXypQSz73nxI+GWc7ZReIAVhxoE1KCg==} - - '@whatwg-node/fetch@0.9.7': - resolution: {integrity: sha512-heClS5ctTmoEvVbFd+6ztX0SyQduI3/Q+77vtNApDU/+Mwajy6ugxaoDGgSzJUoQ37McSV09kcGCt1Jcc6z9lQ==} - engines: {node: '>=16.0.0'} - - '@whatwg-node/node-fetch@0.3.6': - resolution: {integrity: sha512-w9wKgDO4C95qnXZRwZTfCmLWqyRnooGjcIwG0wADWjw9/HN0p7dtvtgSvItZtUyNteEvgTrd8QojNEqV6DAGTA==} - - '@whatwg-node/node-fetch@0.4.6': - resolution: {integrity: sha512-9oLD57yW0WWfu4ZtEmybBrx1JfkO4TzDpD2zXlp2Ue3UJplifQRap+MbE0PxRlVWtR4KWsIRamhn7J44DkwoyA==} - engines: {node: '>=16.0.0'} - - '@whatwg-node/server@0.9.1': - resolution: {integrity: sha512-Zq0FWafIlJzYyyAL7UwzsEhqSMzvKci/XqIFKpC4V031PxKKPBmZSP1mXCew5eZFW8Z5Sj6bZNi2CIsHd062ug==} - engines: {node: '>=16.0.0'} - '@wogns3623/eslint-plugin-better-exhaustive-deps@1.1.0': resolution: {integrity: sha512-AkQzn7bSngpml8nqkJmXMTHfoO1sviqTfc6jaNaQKX1Rj6jHqRmm4hzn1ahN3oH4b7sTjMnzXfGjN/16XcFKxw==} peerDependencies: @@ -6376,16 +5829,9 @@ packages: resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} engines: {node: '>=0.10.0'} - asap@2.0.6: - resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} - asn1.js@5.4.1: resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} - asn1js@3.0.5: - resolution: {integrity: sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==} - engines: {node: '>=12.0.0'} - assert@2.1.0: resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} @@ -6399,10 +5845,6 @@ packages: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} - astral-regex@2.0.0: - resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} - engines: {node: '>=8'} - astring@1.8.6: resolution: {integrity: sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==} hasBin: true @@ -6416,10 +5858,6 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - auto-bind@4.0.0: - resolution: {integrity: sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==} - engines: {node: '>=8'} - auto-bind@5.0.1: resolution: {integrity: sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -6471,9 +5909,6 @@ packages: resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - babel-plugin-macros@2.8.0: - resolution: {integrity: sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==} - babel-plugin-macros@3.1.0: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} @@ -6493,22 +5928,11 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - babel-plugin-relay@16.2.0: - resolution: {integrity: sha512-+3n7kSFH5MelySnO5MLXl2S+Bq4nAGcdWylXeqNXZODbzgYtqak194Z4u5KElLAHGhsyKIMTW7qazBPvMbxhFQ==} - - babel-plugin-syntax-trailing-function-commas@7.0.0-beta.0: - resolution: {integrity: sha512-Xj9XuRuz3nTSbaTXWv3itLOcxyF4oPD8douBBmj7U9BBC6nEBYfyOJYQMf/8PJAFotC62UY5dFfIGEPr7WswzQ==} - babel-preset-current-node-syntax@1.0.1: resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} peerDependencies: '@babel/core': ^7.0.0 - babel-preset-fbjs@3.4.0: - resolution: {integrity: sha512-9ywCsCvo1ojrw0b+XYk7aFvTH6D9064t0RIL1rtMf3nsa02Xw41MS7sZw216Im35xj/UY0PDBQsa1brUDDF1Ow==} - peerDependencies: - '@babel/core': ^7.0.0 - babel-preset-jest@29.6.3: resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -6611,11 +6035,6 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - browserslist@4.23.3: - resolution: {integrity: sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - browserslist@4.24.2: resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -6669,25 +6088,10 @@ packages: resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} engines: {node: '>= 0.4'} - caller-callsite@2.0.0: - resolution: {integrity: sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==} - engines: {node: '>=4'} - - caller-path@2.0.0: - resolution: {integrity: sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==} - engines: {node: '>=4'} - - callsites@2.0.0: - resolution: {integrity: sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==} - engines: {node: '>=4'} - callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - camel-case@4.1.2: - resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} - camelcase-css@2.0.1: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} @@ -6720,9 +6124,6 @@ packages: caniuse-lite@1.0.30001680: resolution: {integrity: sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==} - capital-case@1.0.4: - resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} - ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -6746,12 +6147,6 @@ packages: resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - change-case-all@1.0.15: - resolution: {integrity: sha512-3+GIFhk3sNuvFAJKU46o26OdzudQlPNBCu1ZQi3cMeMHhty1bhDxu2WrEilVNYaGvqUtR1VSigFcJOiS13dRhQ==} - - change-case@4.1.2: - resolution: {integrity: sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==} - char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -6842,18 +6237,10 @@ packages: resolution: {integrity: sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==} engines: {node: 10.* || >= 12.*} - cli-truncate@2.1.0: - resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} - engines: {node: '>=8'} - cli-truncate@4.0.0: resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} engines: {node: '>=18'} - cli-width@3.0.0: - resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} - engines: {node: '>= 10'} - client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} @@ -6940,9 +6327,6 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} - colorette@2.0.20: - resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -6978,10 +6362,6 @@ packages: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} - common-tags@1.8.2: - resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} - engines: {node: '>=4.0.0'} - commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} @@ -7007,9 +6387,6 @@ packages: resolution: {integrity: sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==} engines: {node: ^14.18.0 || >=16.10.0} - constant-case@3.0.4: - resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==} - content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -7051,27 +6428,10 @@ packages: cose-base@1.0.3: resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} - cosmiconfig@5.2.1: - resolution: {integrity: sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==} - engines: {node: '>=4'} - - cosmiconfig@6.0.0: - resolution: {integrity: sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==} - engines: {node: '>=8'} - cosmiconfig@7.1.0: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} - cosmiconfig@8.3.6: - resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} - engines: {node: '>=14'} - peerDependencies: - typescript: '>=4.9.5' - peerDependenciesMeta: - typescript: - optional: true - create-ecdh@4.0.4: resolution: {integrity: sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==} @@ -7092,9 +6452,6 @@ packages: crelt@1.0.5: resolution: {integrity: sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==} - cross-fetch@3.1.8: - resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==} - cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} @@ -7333,9 +6690,6 @@ packages: dataloader@1.4.0: resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} - dataloader@2.2.2: - resolution: {integrity: sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==} - date-fns@3.6.0: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} @@ -7345,9 +6699,6 @@ packages: dayjs@1.11.10: resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} - debounce@1.2.1: - resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} - debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -7599,9 +6950,6 @@ packages: domutils@3.1.0: resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} - dot-case@3.0.4: - resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} - dotenv-cli@7.4.2: resolution: {integrity: sha512-SbUj8l61zIbzyhIbg0FwPJq6+wjbzdn9oEtozQpZ6kW2ihCcapKVZj49oCT3oPM+mgQm+itgvUQcG5szxVrZTA==} hasBin: true @@ -7614,10 +6962,6 @@ packages: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} - dset@3.1.2: - resolution: {integrity: sha512-g/M9sqy3oHe477Ar4voQxWtaPIFw1jTdKZuomOjhCcBx9nHUNn0pu6NopuFFrTh/TRZIKEj+76vLWFu9BNKk+Q==} - engines: {node: '>=4'} - duplexify@3.7.1: resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==} @@ -7638,9 +6982,6 @@ packages: electron-to-chromium@1.4.752: resolution: {integrity: sha512-P3QJreYI/AUTcfBVrC4zy9KvnZWekViThgQMX/VpJ+IsOBbcX5JFpORM4qWapwWQ+agb2nYAOyn/4PMXOk0m2Q==} - electron-to-chromium@1.5.13: - resolution: {integrity: sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==} - electron-to-chromium@1.5.51: resolution: {integrity: sha512-kKeWV57KSS8jH4alKt/jKnvHPmJgBxXzGUSbMd4eQF+iOsVPl7bz2KUmu6eo80eMP8wVioTfTyTzdMgM15WXNg==} @@ -8005,17 +7346,10 @@ packages: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} - extract-files@11.0.0: - resolution: {integrity: sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ==} - engines: {node: ^12.20 || >= 14.13} - fast-check@3.19.0: resolution: {integrity: sha512-CO2JX/8/PT9bDGO1iXa5h5ey1skaKI1dvecERyhH4pp3PGjwd3KIjMAXEg79Ps9nclsdt4oPbfqiAnLU0EwrAQ==} engines: {node: '>=8.0.0'} - fast-decode-uri-component@1.0.1: - resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -8032,9 +7366,6 @@ packages: fast-loops@1.1.3: resolution: {integrity: sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g==} - fast-querystring@1.1.1: - resolution: {integrity: sha512-qR2r+e3HvhEFmpdHMv//U8FnFlnYjaC6QKDuaXALDkw2kvHO8WDjxH+f/rHGR4Me4pnk8p9JAkRNTjYHAKRn2Q==} - fast-shallow-equal@1.0.0: resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==} @@ -8057,12 +7388,6 @@ packages: fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - fbjs-css-vars@1.0.2: - resolution: {integrity: sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==} - - fbjs@3.0.5: - resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} - fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} @@ -8077,10 +7402,6 @@ packages: fetch-retry@5.0.6: resolution: {integrity: sha512-3yurQZ2hD9VISAhJJP9bpYFNQrHHBXE2JxxjY5aLEcDi46RmAzJE2OC9FAde0yis5ElW0jTTzs0zfg/Cca4XqQ==} - figures@3.2.0: - resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} - engines: {node: '>=8'} - figures@5.0.0: resolution: {integrity: sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==} engines: {node: '>=14'} @@ -8408,47 +7729,6 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - graphql-config@5.0.2: - resolution: {integrity: sha512-7TPxOrlbiG0JplSZYCyxn2XQtqVhXomEjXUmWJVSS5ET1nPhOJSsIb/WTwqWhcYX6G0RlHXSj9PLtGTKmxLNGg==} - engines: {node: '>= 16.0.0'} - peerDependencies: - cosmiconfig-toml-loader: ^1.0.0 - graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - peerDependenciesMeta: - cosmiconfig-toml-loader: - optional: true - - graphql-request@6.1.0: - resolution: {integrity: sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==} - peerDependencies: - graphql: 14 - 16 - - graphql-tag@2.12.6: - resolution: {integrity: sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==} - engines: {node: '>=10'} - peerDependencies: - graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - - graphql-ws@5.14.0: - resolution: {integrity: sha512-itrUTQZP/TgswR4GSSYuwWUzrE/w5GhbwM2GX3ic2U7aw33jgEsayfIlvaj7/GcIvZgNMzsPTrE5hqPuFUiE5g==} - engines: {node: '>=10'} - peerDependencies: - graphql: '>=0.11 <=16' - - graphql-yoga@5.1.1: - resolution: {integrity: sha512-oak5nVKTHpqJgpA1aT3cJPOlCidrW7l6nbc5L6w07VdFul16ielGI2ZnQDAXO+qQih09/4WspD5x0SsSZH+hkg==} - engines: {node: '>=18.0.0'} - peerDependencies: - graphql: ^15.2.0 || ^16.0.0 - - graphql@15.3.0: - resolution: {integrity: sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w==} - engines: {node: '>= 10.x'} - - graphql@16.8.1: - resolution: {integrity: sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==} - engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - gray-matter@4.0.3: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} engines: {node: '>=6.0'} @@ -8570,9 +7850,6 @@ packages: hastscript@8.0.0: resolution: {integrity: sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==} - header-case@2.0.4: - resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==} - heap@0.2.7: resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==} @@ -8629,10 +7906,6 @@ packages: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} engines: {node: '>= 6'} - http-proxy-agent@7.0.0: - resolution: {integrity: sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==} - engines: {node: '>= 14'} - http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -8641,10 +7914,6 @@ packages: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} - https-proxy-agent@7.0.2: - resolution: {integrity: sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==} - engines: {node: '>= 14'} - https-proxy-agent@7.0.4: resolution: {integrity: sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==} engines: {node: '>= 14'} @@ -8703,25 +7972,13 @@ packages: engines: {node: '>=16.x'} hasBin: true - immutable@3.7.6: - resolution: {integrity: sha512-AizQPcaofEtO11RZhPPHBOJRdo/20MKQF9mBLnVkBoyHi1/zXK8fzVdnEpSV9gxqtnh6Qomfp3F0xT5qP/vThw==} - engines: {node: '>=0.8.0'} - immutable@4.3.6: resolution: {integrity: sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==} - import-fresh@2.0.0: - resolution: {integrity: sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==} - engines: {node: '>=4'} - import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} - import-from@4.0.0: - resolution: {integrity: sha512-P9J71vT5nLlDeV8FHs5nNxaLbrpfAV5cF5srvbZfpwpcJoM/xZR3hiv+q+SAnuSmuGbXMWud063iIMx/V/EWZQ==} - engines: {node: '>=12.2'} - import-local@3.1.0: resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} engines: {node: '>=8'} @@ -8778,10 +8035,6 @@ packages: inline-style-prefixer@7.0.0: resolution: {integrity: sha512-I7GEdScunP1dQ6IM2mQWh6v0mOYdYmH3Bp31UecKdrcUgcURTcctSe1IECdUznSHKSmsHtjrT3CwCPI1pyxfUQ==} - inquirer@8.2.6: - resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} - engines: {node: '>=12.0.0'} - internal-slot@1.0.5: resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} engines: {node: '>= 0.4'} @@ -8804,10 +8057,6 @@ packages: resolution: {integrity: sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - is-absolute@1.0.0: - resolution: {integrity: sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==} - engines: {node: '>=0.10.0'} - is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} @@ -8863,10 +8112,6 @@ packages: is-deflate@1.0.0: resolution: {integrity: sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==} - is-directory@0.3.1: - resolution: {integrity: sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==} - engines: {node: '>=0.10.0'} - is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} @@ -8933,9 +8178,6 @@ packages: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} - is-lower-case@2.0.2: - resolution: {integrity: sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ==} - is-map@2.0.2: resolution: {integrity: sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==} @@ -8995,10 +8237,6 @@ packages: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} - is-relative@1.0.0: - resolution: {integrity: sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==} - engines: {node: '>=0.10.0'} - is-set@2.0.2: resolution: {integrity: sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==} @@ -9033,10 +8271,6 @@ packages: resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} engines: {node: '>= 0.4'} - is-unc-path@1.0.0: - resolution: {integrity: sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==} - engines: {node: '>=0.10.0'} - is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -9045,9 +8279,6 @@ packages: resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} engines: {node: '>=12'} - is-upper-case@2.0.2: - resolution: {integrity: sha512-44pxmxAvnnAOwBg4tHPnkfvgjPwbc5QIsSstNU+YcJ1ovxVzCWpSGosPJOZh/a1tdl81fbgnLc9LLv+x2ywbPQ==} - is-weakmap@2.0.1: resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==} @@ -9095,11 +8326,6 @@ packages: resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} engines: {node: '>=0.10.0'} - isomorphic-ws@5.0.0: - resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} - peerDependencies: - ws: '*' - istanbul-lib-coverage@3.2.0: resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} engines: {node: '>=8'} @@ -9277,17 +8503,10 @@ packages: node-notifier: optional: true - jiti@1.20.0: - resolution: {integrity: sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==} - hasBin: true - jiti@1.21.0: resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} hasBin: true - jose@4.15.5: - resolution: {integrity: sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==} - jose@5.9.6: resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==} @@ -9349,9 +8568,6 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - json-parse-better-errors@1.0.2: - resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} - json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} @@ -9365,13 +8581,6 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - json-stable-stringify@1.0.2: - resolution: {integrity: sha512-eunSSaEnxV12z+Z73y/j5N37/In40GK4GmsSy+tEHJMxknvqnA7/djeYtAgW0GsWHUfg+847WJjKaEylk2y09g==} - - json-to-pretty-yaml@1.2.2: - resolution: {integrity: sha512-rvm6hunfCcqegwYaG5T4yKJWxc9FXFgBVrcTZ4XfSVRwa5HA/Xs+vB/Eo9treYYHCeNM0nrSUr82V/M31Urc7A==} - engines: {node: '>= 0.2.0'} - json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -9390,9 +8599,6 @@ packages: jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} - jsonify@0.0.1: - resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==} - jsonwebtoken@9.0.2: resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} engines: {node: '>=12', npm: '>=6'} @@ -9506,15 +8712,6 @@ packages: linkify-it@3.0.3: resolution: {integrity: sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==} - listr2@4.0.5: - resolution: {integrity: sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==} - engines: {node: '>=12'} - peerDependencies: - enquirer: '>= 2.3.0 < 3' - peerDependenciesMeta: - enquirer: - optional: true - lite-emit@2.3.0: resolution: {integrity: sha512-QMPrnwPho7lfkzZUN3a0RJ/oiwpt464eXf6aVh1HGOYh+s7Utu78q3FcFbW59c8TNWWQaz9flKN1cEb8dmxD+g==} @@ -9573,9 +8770,6 @@ packages: lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} - lodash.sortby@4.7.0: - resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} - lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} @@ -9586,10 +8780,6 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} - log-update@4.0.0: - resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} - engines: {node: '>=10'} - longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -9600,16 +8790,6 @@ packages: loupe@2.3.7: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} - lower-case-first@2.0.2: - resolution: {integrity: sha512-EVm/rR94FJTZi3zefZ82fLWab+GX14LJN4HrWBcuo6Evmsl9hEfnqxgcHCKb9q+mNf6EVdsjx/qucYFIIB84pg==} - - lower-case@2.0.2: - resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} - - lru-cache@10.0.0: - resolution: {integrity: sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw==} - engines: {node: 14 || >=16.14} - lru-cache@10.2.2: resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} engines: {node: 14 || >=16.14} @@ -9656,10 +8836,6 @@ packages: makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} - map-cache@0.2.2: - resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} - engines: {node: '>=0.10.0'} - map-obj@1.0.1: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} engines: {node: '>=0.10.0'} @@ -9799,15 +8975,6 @@ packages: mermaid@10.9.0: resolution: {integrity: sha512-swZju0hFox/B/qoLKK0rOxxgh8Cf7rJSfAUc1u8fezVihYMvrJAS45GzAxTVf4Q+xn9uMgitBcmWk7nWGXOs/g==} - meros@1.3.0: - resolution: {integrity: sha512-2BNGOimxEz5hmjUG2FwoxCt5HN7BXdaWyFqEwxPTrJzVdABtrL4TiHTcsWSFAxPQ/tOnEaQEJh3qWq71QRMY+w==} - engines: {node: '>=13'} - peerDependencies: - '@types/node': '>=13' - peerDependenciesMeta: - '@types/node': - optional: true - methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} @@ -10036,10 +9203,6 @@ packages: minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - minimatch@4.2.3: - resolution: {integrity: sha512-lIUdtK5hdofgCTu3aT0sOaHsYR37viUuIc0rwnnDXImbwFRcumyLMeZaM0t0I/fgxS6s6JMfu0rLD1Wz9pv1ng==} - engines: {node: '>=10'} - minimatch@5.1.6: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} @@ -10222,9 +9385,6 @@ packages: sass: optional: true - no-case@3.0.4: - resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} - node-abi@3.47.0: resolution: {integrity: sha512-2s6B2CWZM//kPgwnuI0KrYwNjfdByE25zvAaEpq9IH4zcNsarH8Ihu/UuX6XMPEogDAxkuUFeZn60pXNHAqn3A==} engines: {node: '>=10'} @@ -10292,10 +9452,6 @@ packages: resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==} engines: {node: ^16.14.0 || >=18.0.0} - normalize-path@2.1.1: - resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==} - engines: {node: '>=0.10.0'} - normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -10331,9 +9487,6 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - nullthrows@1.1.1: - resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} - nwsapi@2.2.10: resolution: {integrity: sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==} @@ -10513,9 +9666,6 @@ packages: pako@2.1.0: resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} - param-case@3.0.4: - resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} - parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -10526,14 +9676,6 @@ packages: parse-entities@4.0.1: resolution: {integrity: sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==} - parse-filepath@1.0.2: - resolution: {integrity: sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==} - engines: {node: '>=0.8'} - - parse-json@4.0.0: - resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} - engines: {node: '>=4'} - parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -10554,9 +9696,6 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} - pascal-case@3.1.2: - resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} - patch-console@2.0.0: resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -10564,9 +9703,6 @@ packages: path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - path-case@3.0.4: - resolution: {integrity: sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==} - path-exists@3.0.0: resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} @@ -10590,14 +9726,6 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-root-regex@0.1.2: - resolution: {integrity: sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==} - engines: {node: '>=0.10.0'} - - path-root@0.1.1: - resolution: {integrity: sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==} - engines: {node: '>=0.10.0'} - path-scurry@1.10.2: resolution: {integrity: sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==} engines: {node: '>=16 || 14 >=14.17'} @@ -10901,9 +10029,6 @@ packages: resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} engines: {node: '>=10'} - promise@7.3.1: - resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} - prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -10958,13 +10083,6 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} - pvtsutils@1.3.5: - resolution: {integrity: sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==} - - pvutils@1.1.3: - resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==} - engines: {node: '>=6.0.0'} - qs@6.11.0: resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} engines: {node: '>=0.6'} @@ -11101,11 +10219,6 @@ packages: peerDependencies: react: ^18.3.1 - react-relay@16.2.0: - resolution: {integrity: sha512-f/HtC4whyYmK6/WUeOVakXRoBkV+JEgoSeBHXfIC2U6AuH14NrKXnFicX65LksfzgD1OUfYF6IqGQ4MvO52lTQ==} - peerDependencies: - react: ^16.9.0 || ^17 || ^18 - react-remove-scroll-bar@2.3.6: resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==} engines: {node: '>=10'} @@ -11335,16 +10448,6 @@ packages: rehype-slug@6.0.0: resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==} - relay-compiler@16.2.0: - resolution: {integrity: sha512-KuyzUBKL9PZRNtIZWNlWEOl7OliUxaGJ2d+3mkiWEiGCEuGnNTxqEg4kJyL341aIGZC4gSqEpfvRTcMqnSM4qQ==} - hasBin: true - - relay-runtime@12.0.0: - resolution: {integrity: sha512-QU6JKr1tMsry22DXNy9Whsq5rmvwr3LSZiiWV/9+DFpuTWvp+WFhobWMc8TC4OjKFfNhEZy7mOiqUAn5atQtug==} - - relay-runtime@16.2.0: - resolution: {integrity: sha512-SrIyYItH1EZUj37NI8nZALasuq7mNyFrrSNgMefhgxNZxTVnr1KCp43LaxUfZqhsWbw4Y00JSGDRQXlcv4STHQ==} - remark-breaks@4.0.0: resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==} @@ -11381,15 +10484,6 @@ packages: remark@15.0.1: resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} - remedial@1.0.8: - resolution: {integrity: sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg==} - - remove-trailing-separator@1.1.0: - resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==} - - remove-trailing-spaces@1.0.8: - resolution: {integrity: sha512-O3vsMYfWighyFbTd8hk8VaSj9UAGENxAtX+//ugIst2RMk5e03h6RoIS+0ylsFxY1gvmPuAY/PO4It+gPEeySA==} - require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -11407,10 +10501,6 @@ packages: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} - resolve-from@3.0.0: - resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==} - engines: {node: '>=4'} - resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -11450,9 +10540,6 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rfdc@1.3.0: - resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} - rimraf@2.6.3: resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -11491,19 +10578,12 @@ packages: resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} engines: {node: '>=18'} - run-async@2.4.1: - resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} - engines: {node: '>=0.12.0'} - run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} rw@1.3.3: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} - rxjs@7.8.1: - resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} - sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} @@ -11544,9 +10624,6 @@ packages: scroll-into-view-if-needed@3.1.0: resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} - scuid@1.1.0: - resolution: {integrity: sha512-MuCAyrGZcTLfQoH2XoBlQ8C6bzwN88XT/0slOGz0pn8+gIP85BOAfYa44ZXQUTOwRwPU0QvgU+V+OSajl/59Xg==} - section-matter@1.0.0: resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} engines: {node: '>=4'} @@ -11582,9 +10659,6 @@ packages: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} engines: {node: '>= 0.8.0'} - sentence-case@3.0.4: - resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} - serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -11614,9 +10688,6 @@ packages: resolution: {integrity: sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==} engines: {node: '>=6.9'} - setimmediate@1.0.5: - resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} - setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -11648,9 +10719,6 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shell-quote@1.8.1: - resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} - shiki@0.14.7: resolution: {integrity: sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==} @@ -11687,9 +10755,6 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - signedsource@1.0.0: - resolution: {integrity: sha512-6+eerH9fEnNmi/hyM1DXcRK3pWdoMQtlkQ+ns0ntzunjKqp5i3sKCc80ym8Fib3iaYhdJUOPdhlJWj1tvge2Ww==} - simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} @@ -11718,14 +10783,6 @@ packages: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} engines: {node: '>=14.16'} - slice-ansi@3.0.0: - resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} - engines: {node: '>=8'} - - slice-ansi@4.0.0: - resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} - engines: {node: '>=10'} - slice-ansi@5.0.0: resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} engines: {node: '>=12'} @@ -11739,9 +10796,6 @@ packages: engines: {node: '>=6'} hasBin: true - snake-case@3.0.4: - resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} - source-map-generator@0.8.0: resolution: {integrity: sha512-psgxdGMwl5MZM9S3FWee4EgsEaIjahYV5AzGnwUvPhWeITz/j6rKpysQHlQ4USdxvINlb8lKfWGIXwfkrgtqkA==} engines: {node: '>= 10'} @@ -11794,9 +10848,6 @@ packages: spdx-license-ids@3.0.12: resolution: {integrity: sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==} - sponge-case@1.0.1: - resolution: {integrity: sha512-dblb9Et4DAtiZ5YSUZHLl4XhH4uK80GhAZrVXdN4O2P4gQ40Wa5UIOPUHlA/nFd2PLblBZWUioLMMAVrgpoYcA==} - sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -11844,9 +10895,6 @@ packages: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} - string-env-interpolation@1.0.1: - resolution: {integrity: sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==} - string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} engines: {node: '>=10'} @@ -11988,9 +11036,6 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - swap-case@2.0.2: - resolution: {integrity: sha512-kc6S2YS/2yXbtkSMunBtKdah4VFETZ8Oh6ONSmSd9bRxhqTrtARUCBUiWXH3xVPpvR7tz2CSnkuXVE42EcGnMw==} - symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -12084,9 +11129,6 @@ packages: through2@2.0.5: resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} - through@2.3.8: - resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - tiny-glob@0.2.9: resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} @@ -12101,9 +11143,6 @@ packages: resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} engines: {node: '>=14.0.0'} - title-case@3.0.3: - resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==} - titleize@3.0.0: resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==} engines: {node: '>=12'} @@ -12196,9 +11235,6 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - ts-log@2.2.5: - resolution: {integrity: sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA==} - ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true @@ -12369,9 +11405,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - ua-parser-js@1.0.37: - resolution: {integrity: sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==} - uc.micro@1.0.6: resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} @@ -12386,10 +11419,6 @@ packages: unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} - unc-path-regex@0.1.2: - resolution: {integrity: sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==} - engines: {node: '>=0.10.0'} - undefsafe@2.0.5: resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} @@ -12496,10 +11525,6 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} - unixify@1.0.0: - resolution: {integrity: sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg==} - engines: {node: '>=0.10.0'} - unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -12518,24 +11543,12 @@ packages: peerDependencies: browserslist: '>= 4.21.0' - update-browserslist-db@1.1.0: - resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - update-browserslist-db@1.1.1: resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' - upper-case-first@2.0.2: - resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==} - - upper-case@2.0.2: - resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==} - uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -12548,12 +11561,6 @@ packages: urlgrey@1.0.0: resolution: {integrity: sha512-hJfIzMPJmI9IlLkby8QrsCykQ+SXDeO2W5Q9QTW3QpqZVTx4a/K7p8/5q+/isD8vsbVaFgql/gvAoQCRQ2Cb5w==} - urlpattern-polyfill@8.0.2: - resolution: {integrity: sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==} - - urlpattern-polyfill@9.0.0: - resolution: {integrity: sha512-WHN8KDQblxd32odxeIgo83rdVDE2bvdkb86it7bMhYZwWKJz0+O0RK/eZiHYnM+zgt/U7hAHOlCQGfjjvSkw2g==} - use-callback-ref@1.3.2: resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==} engines: {node: '>=10'} @@ -12639,10 +11646,6 @@ packages: resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - value-or-promise@1.0.12: - resolution: {integrity: sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==} - engines: {node: '>=12'} - vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -12782,10 +11785,6 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} - web-streams-polyfill@3.2.1: - resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} - engines: {node: '>= 8'} - web-streams-polyfill@4.0.0-beta.3: resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} engines: {node: '>= 14'} @@ -12793,9 +11792,6 @@ packages: web-worker@1.2.0: resolution: {integrity: sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==} - webcrypto-core@1.7.7: - resolution: {integrity: sha512-7FjigXNsBfopEj+5DV2nhNpfic2vumtjjgPmeDKk45z+MJwXKKfhPB7118Pfzrmh4jqOMST6Ch37iPAHoImg5g==} - webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -12909,30 +11905,6 @@ packages: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - ws@8.14.1: - resolution: {integrity: sha512-4OOseMUq8AzRBI/7SLMUwO+FEDnguetSk7KMb1sHwvF2w2Wv5Hoj0nlifx8vtGsftE/jWHojPy8sMMzYLJ2G/A==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - ws@8.16.0: - resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.17.0: resolution: {integrity: sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==} engines: {node: '>=10.0.0'} @@ -13012,9 +11984,6 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yaml-ast-parser@0.0.43: - resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} - yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} @@ -13211,36 +12180,6 @@ snapshots: transitivePeerDependencies: - encoding - '@ardatan/relay-compiler@12.0.0(graphql@16.8.1)': - dependencies: - '@babel/core': 7.26.0 - '@babel/generator': 7.24.1 - '@babel/parser': 7.26.2 - '@babel/runtime': 7.26.0 - '@babel/traverse': 7.24.1 - '@babel/types': 7.24.0 - babel-preset-fbjs: 3.4.0(@babel/core@7.26.0) - chalk: 4.1.2 - fb-watchman: 2.0.2 - fbjs: 3.0.5 - glob: 7.2.3 - graphql: 16.8.1 - immutable: 3.7.6 - invariant: 2.2.4 - nullthrows: 1.1.1 - relay-runtime: 12.0.0 - signedsource: 1.0.0 - yargs: 15.4.1 - transitivePeerDependencies: - - encoding - - supports-color - - '@ardatan/sync-fetch@0.0.1': - dependencies: - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding - '@auth/core@0.37.2(nodemailer@6.9.13)': dependencies: '@panva/hkdf': 1.2.1 @@ -13380,8 +12319,6 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.0.1 - '@babel/compat-data@7.25.4': {} - '@babel/compat-data@7.26.2': {} '@babel/core@7.26.0': @@ -13404,20 +12341,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.23.6': - dependencies: - '@babel/types': 7.24.0 - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - jsesc: 2.5.2 - - '@babel/generator@7.24.1': - dependencies: - '@babel/types': 7.25.6 - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - jsesc: 2.5.2 - '@babel/generator@7.25.6': dependencies: '@babel/types': 7.25.6 @@ -13452,14 +12375,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-compilation-targets@7.25.2': - dependencies: - '@babel/compat-data': 7.25.4 - '@babel/helper-validator-option': 7.24.8 - browserslist: 4.23.3 - lru-cache: 5.1.1 - semver: 6.3.1 - '@babel/helper-compilation-targets@7.25.9': dependencies: '@babel/compat-data': 7.26.2 @@ -13814,23 +12729,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.26.0) - '@babel/helper-plugin-utils': 7.24.7 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.26.0)': - dependencies: - '@babel/compat-data': 7.25.4 - '@babel/core': 7.26.0 - '@babel/helper-compilation-targets': 7.25.2 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.26.0) - '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -13855,11 +12753,6 @@ snapshots: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-import-assertions@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-import-assertions@7.26.0(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -13941,11 +12834,6 @@ snapshots: '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.26.0) '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-arrow-functions@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-arrow-functions@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -13969,21 +12857,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-block-scoped-functions@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-block-scoped-functions@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-block-scoping@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-block-scoping@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -14013,20 +12891,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-classes@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-annotate-as-pure': 7.24.7 - '@babel/helper-compilation-targets': 7.25.2 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-function-name': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-replace-supers': 7.24.7(@babel/core@7.26.0) - '@babel/helper-split-export-declaration': 7.24.7 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-classes@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -14039,23 +12903,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-computed-properties@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/template': 7.25.0 - - '@babel/plugin-transform-computed-properties@7.25.9(@babel/core@7.26.0)': + '@babel/plugin-transform-computed-properties@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 '@babel/template': 7.25.9 - '@babel/plugin-transform-destructuring@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-destructuring@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -14102,14 +12955,6 @@ snapshots: '@babel/helper-plugin-utils': 7.24.7 '@babel/plugin-syntax-flow': 7.23.3(@babel/core@7.26.0) - '@babel/plugin-transform-for-of@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-for-of@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -14118,13 +12963,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-function-name@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-compilation-targets': 7.25.2 - '@babel/helper-function-name': 7.24.7 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-function-name@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -14139,11 +12977,6 @@ snapshots: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-literals@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-literals@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -14154,11 +12987,6 @@ snapshots: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-member-expression-literals@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-member-expression-literals@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -14249,14 +13077,6 @@ snapshots: '@babel/helper-plugin-utils': 7.25.9 '@babel/plugin-transform-parameters': 7.25.9(@babel/core@7.26.0) - '@babel/plugin-transform-object-super@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-replace-supers': 7.24.7(@babel/core@7.26.0) - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-object-super@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -14287,11 +13107,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-parameters@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-parameters@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -14322,11 +13137,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-property-literals@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-property-literals@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -14378,24 +13188,11 @@ snapshots: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-shorthand-properties@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-shorthand-properties@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-spread@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-spread@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -14409,11 +13206,6 @@ snapshots: '@babel/core': 7.26.0 '@babel/helper-plugin-utils': 7.25.9 - '@babel/plugin-transform-template-literals@7.24.7(@babel/core@7.26.0)': - dependencies: - '@babel/core': 7.26.0 - '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-template-literals@7.25.9(@babel/core@7.26.0)': dependencies: '@babel/core': 7.26.0 @@ -14614,12 +13406,6 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 - '@babel/template@7.23.9': - dependencies: - '@babel/code-frame': 7.24.2 - '@babel/parser': 7.26.2 - '@babel/types': 7.24.0 - '@babel/template@7.24.0': dependencies: '@babel/code-frame': 7.24.2 @@ -14638,21 +13424,6 @@ snapshots: '@babel/parser': 7.26.2 '@babel/types': 7.26.0 - '@babel/traverse@7.24.1': - dependencies: - '@babel/code-frame': 7.24.7 - '@babel/generator': 7.25.6 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-split-export-declaration': 7.24.5 - '@babel/parser': 7.26.2 - '@babel/types': 7.25.6 - debug: 4.3.6 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - '@babel/traverse@7.24.5': dependencies: '@babel/code-frame': 7.24.7 @@ -15143,15 +13914,6 @@ snapshots: '@emotion/weak-memoize@0.3.1': {} - '@envelop/core@5.0.0': - dependencies: - '@envelop/types': 5.0.0 - tslib: 2.6.2 - - '@envelop/types@5.0.0': - dependencies: - tslib: 2.8.1 - '@esbuild-plugins/node-resolve@0.2.2(esbuild@0.21.5)': dependencies: '@types/resolve': 1.20.6 @@ -15531,451 +14293,6 @@ snapshots: '@content-collections/mdx': 0.2.0(@content-collections/core@0.7.3(typescript@5.6.3))(acorn@8.11.3)(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025) fumadocs-core: 14.0.2(@types/react@18.3.3) - '@graphql-codegen/add@5.0.2(graphql@16.8.1)': - dependencies: - '@graphql-codegen/plugin-helpers': 5.0.3(graphql@16.8.1) - graphql: 16.8.1 - tslib: 2.6.2 - - '@graphql-codegen/cli@5.0.2(@parcel/watcher@2.4.1)(@types/node@20.12.7)(enquirer@2.3.6)(graphql@16.8.1)(typescript@5.6.3)': - dependencies: - '@babel/generator': 7.23.6 - '@babel/template': 7.23.9 - '@babel/types': 7.23.9 - '@graphql-codegen/client-preset': 4.2.5(graphql@16.8.1) - '@graphql-codegen/core': 4.0.2(graphql@16.8.1) - '@graphql-codegen/plugin-helpers': 5.0.3(graphql@16.8.1) - '@graphql-tools/apollo-engine-loader': 8.0.0(graphql@16.8.1) - '@graphql-tools/code-file-loader': 8.0.2(graphql@16.8.1) - '@graphql-tools/git-loader': 8.0.2(graphql@16.8.1) - '@graphql-tools/github-loader': 8.0.0(@types/node@20.12.7)(graphql@16.8.1) - '@graphql-tools/graphql-file-loader': 8.0.0(graphql@16.8.1) - '@graphql-tools/json-file-loader': 8.0.0(graphql@16.8.1) - '@graphql-tools/load': 8.0.0(graphql@16.8.1) - '@graphql-tools/prisma-loader': 8.0.1(@types/node@20.12.7)(graphql@16.8.1) - '@graphql-tools/url-loader': 8.0.0(@types/node@20.12.7)(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - '@whatwg-node/fetch': 0.8.8 - chalk: 4.1.2 - cosmiconfig: 8.3.6(typescript@5.6.3) - debounce: 1.2.1 - detect-indent: 6.1.0 - graphql: 16.8.1 - graphql-config: 5.0.2(@types/node@20.12.7)(graphql@16.8.1)(typescript@5.6.3) - inquirer: 8.2.6 - is-glob: 4.0.3 - jiti: 1.20.0 - json-to-pretty-yaml: 1.2.2 - listr2: 4.0.5(enquirer@2.3.6) - log-symbols: 4.1.0 - micromatch: 4.0.5 - shell-quote: 1.8.1 - string-env-interpolation: 1.0.1 - ts-log: 2.2.5 - tslib: 2.6.2 - yaml: 2.3.4 - yargs: 17.7.2 - optionalDependencies: - '@parcel/watcher': 2.4.1 - transitivePeerDependencies: - - '@types/node' - - bufferutil - - cosmiconfig-toml-loader - - encoding - - enquirer - - supports-color - - typescript - - utf-8-validate - - '@graphql-codegen/client-preset@4.2.5(graphql@16.8.1)': - dependencies: - '@babel/helper-plugin-utils': 7.24.5 - '@babel/template': 7.24.0 - '@graphql-codegen/add': 5.0.2(graphql@16.8.1) - '@graphql-codegen/gql-tag-operations': 4.0.6(graphql@16.8.1) - '@graphql-codegen/plugin-helpers': 5.0.3(graphql@16.8.1) - '@graphql-codegen/typed-document-node': 5.0.6(graphql@16.8.1) - '@graphql-codegen/typescript': 4.0.6(graphql@16.8.1) - '@graphql-codegen/typescript-operations': 4.2.0(graphql@16.8.1) - '@graphql-codegen/visitor-plugin-common': 5.1.0(graphql@16.8.1) - '@graphql-tools/documents': 1.0.0(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - '@graphql-typed-document-node/core': 3.2.0(graphql@16.8.1) - graphql: 16.8.1 - tslib: 2.6.2 - transitivePeerDependencies: - - encoding - - supports-color - - '@graphql-codegen/core@4.0.2(graphql@16.8.1)': - dependencies: - '@graphql-codegen/plugin-helpers': 5.0.3(graphql@16.8.1) - '@graphql-tools/schema': 10.0.0(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - graphql: 16.8.1 - tslib: 2.6.2 - - '@graphql-codegen/gql-tag-operations@4.0.6(graphql@16.8.1)': - dependencies: - '@graphql-codegen/plugin-helpers': 5.0.3(graphql@16.8.1) - '@graphql-codegen/visitor-plugin-common': 5.1.0(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - auto-bind: 4.0.0 - graphql: 16.8.1 - tslib: 2.6.2 - transitivePeerDependencies: - - encoding - - supports-color - - '@graphql-codegen/plugin-helpers@5.0.3(graphql@16.8.1)': - dependencies: - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - change-case-all: 1.0.15 - common-tags: 1.8.2 - graphql: 16.8.1 - import-from: 4.0.0 - lodash: 4.17.21 - tslib: 2.6.2 - - '@graphql-codegen/schema-ast@4.0.2(graphql@16.8.1)': - dependencies: - '@graphql-codegen/plugin-helpers': 5.0.3(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - graphql: 16.8.1 - tslib: 2.6.2 - - '@graphql-codegen/typed-document-node@5.0.6(graphql@16.8.1)': - dependencies: - '@graphql-codegen/plugin-helpers': 5.0.3(graphql@16.8.1) - '@graphql-codegen/visitor-plugin-common': 5.1.0(graphql@16.8.1) - auto-bind: 4.0.0 - change-case-all: 1.0.15 - graphql: 16.8.1 - tslib: 2.6.2 - transitivePeerDependencies: - - encoding - - supports-color - - '@graphql-codegen/typescript-operations@4.2.0(graphql@16.8.1)': - dependencies: - '@graphql-codegen/plugin-helpers': 5.0.3(graphql@16.8.1) - '@graphql-codegen/typescript': 4.0.6(graphql@16.8.1) - '@graphql-codegen/visitor-plugin-common': 5.1.0(graphql@16.8.1) - auto-bind: 4.0.0 - graphql: 16.8.1 - tslib: 2.6.2 - transitivePeerDependencies: - - encoding - - supports-color - - '@graphql-codegen/typescript@4.0.6(graphql@16.8.1)': - dependencies: - '@graphql-codegen/plugin-helpers': 5.0.3(graphql@16.8.1) - '@graphql-codegen/schema-ast': 4.0.2(graphql@16.8.1) - '@graphql-codegen/visitor-plugin-common': 5.1.0(graphql@16.8.1) - auto-bind: 4.0.0 - graphql: 16.8.1 - tslib: 2.6.2 - transitivePeerDependencies: - - encoding - - supports-color - - '@graphql-codegen/visitor-plugin-common@5.1.0(graphql@16.8.1)': - dependencies: - '@graphql-codegen/plugin-helpers': 5.0.3(graphql@16.8.1) - '@graphql-tools/optimize': 2.0.0(graphql@16.8.1) - '@graphql-tools/relay-operation-optimizer': 7.0.0(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - auto-bind: 4.0.0 - change-case-all: 1.0.15 - dependency-graph: 0.11.0 - graphql: 16.8.1 - graphql-tag: 2.12.6(graphql@16.8.1) - parse-filepath: 1.0.2 - tslib: 2.6.2 - transitivePeerDependencies: - - encoding - - supports-color - - '@graphql-tools/apollo-engine-loader@8.0.0(graphql@16.8.1)': - dependencies: - '@ardatan/sync-fetch': 0.0.1 - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - '@whatwg-node/fetch': 0.9.7 - graphql: 16.8.1 - tslib: 2.6.2 - transitivePeerDependencies: - - encoding - - '@graphql-tools/batch-execute@9.0.2(graphql@16.8.1)': - dependencies: - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - dataloader: 2.2.2 - graphql: 16.8.1 - tslib: 2.8.1 - value-or-promise: 1.0.12 - - '@graphql-tools/code-file-loader@8.0.2(graphql@16.8.1)': - dependencies: - '@graphql-tools/graphql-tag-pluck': 8.0.2(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - globby: 11.1.0 - graphql: 16.8.1 - tslib: 2.6.2 - unixify: 1.0.0 - transitivePeerDependencies: - - supports-color - - '@graphql-tools/delegate@10.0.3(graphql@16.8.1)': - dependencies: - '@graphql-tools/batch-execute': 9.0.2(graphql@16.8.1) - '@graphql-tools/executor': 1.1.0(graphql@16.8.1) - '@graphql-tools/schema': 10.0.0(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - dataloader: 2.2.2 - graphql: 16.8.1 - tslib: 2.8.1 - - '@graphql-tools/documents@1.0.0(graphql@16.8.1)': - dependencies: - graphql: 16.8.1 - lodash.sortby: 4.7.0 - tslib: 2.6.2 - - '@graphql-tools/executor-graphql-ws@1.1.0(graphql@16.8.1)': - dependencies: - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - '@types/ws': 8.5.5 - graphql: 16.8.1 - graphql-ws: 5.14.0(graphql@16.8.1) - isomorphic-ws: 5.0.0(ws@8.16.0) - tslib: 2.8.1 - ws: 8.16.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@graphql-tools/executor-http@1.0.2(@types/node@20.12.7)(graphql@16.8.1)': - dependencies: - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - '@repeaterjs/repeater': 3.0.4 - '@whatwg-node/fetch': 0.9.7 - extract-files: 11.0.0 - graphql: 16.8.1 - meros: 1.3.0(@types/node@20.12.7) - tslib: 2.8.1 - value-or-promise: 1.0.12 - transitivePeerDependencies: - - '@types/node' - - '@graphql-tools/executor-legacy-ws@1.0.3(graphql@16.8.1)': - dependencies: - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - '@types/ws': 8.5.5 - graphql: 16.8.1 - isomorphic-ws: 5.0.0(ws@8.14.1) - tslib: 2.8.1 - ws: 8.14.1 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@graphql-tools/executor@1.1.0(graphql@16.8.1)': - dependencies: - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - '@graphql-typed-document-node/core': 3.2.0(graphql@16.8.1) - '@repeaterjs/repeater': 3.0.4 - graphql: 16.8.1 - tslib: 2.6.2 - value-or-promise: 1.0.12 - - '@graphql-tools/git-loader@8.0.2(graphql@16.8.1)': - dependencies: - '@graphql-tools/graphql-tag-pluck': 8.0.2(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - graphql: 16.8.1 - is-glob: 4.0.3 - micromatch: 4.0.5 - tslib: 2.6.2 - unixify: 1.0.0 - transitivePeerDependencies: - - supports-color - - '@graphql-tools/github-loader@8.0.0(@types/node@20.12.7)(graphql@16.8.1)': - dependencies: - '@ardatan/sync-fetch': 0.0.1 - '@graphql-tools/executor-http': 1.0.2(@types/node@20.12.7)(graphql@16.8.1) - '@graphql-tools/graphql-tag-pluck': 8.0.2(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - '@whatwg-node/fetch': 0.9.7 - graphql: 16.8.1 - tslib: 2.6.2 - value-or-promise: 1.0.12 - transitivePeerDependencies: - - '@types/node' - - encoding - - supports-color - - '@graphql-tools/graphql-file-loader@8.0.0(graphql@16.8.1)': - dependencies: - '@graphql-tools/import': 7.0.0(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - globby: 11.1.0 - graphql: 16.8.1 - tslib: 2.6.2 - unixify: 1.0.0 - - '@graphql-tools/graphql-tag-pluck@8.0.2(graphql@16.8.1)': - dependencies: - '@babel/core': 7.26.0 - '@babel/parser': 7.26.2 - '@babel/plugin-syntax-import-assertions': 7.24.7(@babel/core@7.26.0) - '@babel/traverse': 7.24.1 - '@babel/types': 7.24.0 - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - graphql: 16.8.1 - tslib: 2.8.1 - transitivePeerDependencies: - - supports-color - - '@graphql-tools/import@7.0.0(graphql@16.8.1)': - dependencies: - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - graphql: 16.8.1 - resolve-from: 5.0.0 - tslib: 2.8.1 - - '@graphql-tools/json-file-loader@8.0.0(graphql@16.8.1)': - dependencies: - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - globby: 11.1.0 - graphql: 16.8.1 - tslib: 2.6.2 - unixify: 1.0.0 - - '@graphql-tools/load@8.0.0(graphql@16.8.1)': - dependencies: - '@graphql-tools/schema': 10.0.0(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - graphql: 16.8.1 - p-limit: 3.1.0 - tslib: 2.6.2 - - '@graphql-tools/merge@9.0.0(graphql@16.8.1)': - dependencies: - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - graphql: 16.8.1 - tslib: 2.8.1 - - '@graphql-tools/optimize@2.0.0(graphql@16.8.1)': - dependencies: - graphql: 16.8.1 - tslib: 2.8.1 - - '@graphql-tools/prisma-loader@8.0.1(@types/node@20.12.7)(graphql@16.8.1)': - dependencies: - '@graphql-tools/url-loader': 8.0.0(@types/node@20.12.7)(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - '@types/js-yaml': 4.0.5 - '@types/json-stable-stringify': 1.0.34 - '@whatwg-node/fetch': 0.9.7 - chalk: 4.1.2 - debug: 4.3.6 - dotenv: 16.4.5 - graphql: 16.8.1 - graphql-request: 6.1.0(graphql@16.8.1) - http-proxy-agent: 7.0.0 - https-proxy-agent: 7.0.2 - jose: 4.15.5 - js-yaml: 4.1.0 - json-stable-stringify: 1.0.2 - lodash: 4.17.21 - scuid: 1.1.0 - tslib: 2.6.2 - yaml-ast-parser: 0.0.43 - transitivePeerDependencies: - - '@types/node' - - bufferutil - - encoding - - supports-color - - utf-8-validate - - '@graphql-tools/relay-operation-optimizer@7.0.0(graphql@16.8.1)': - dependencies: - '@ardatan/relay-compiler': 12.0.0(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - graphql: 16.8.1 - tslib: 2.8.1 - transitivePeerDependencies: - - encoding - - supports-color - - '@graphql-tools/schema@10.0.0(graphql@16.8.1)': - dependencies: - '@graphql-tools/merge': 9.0.0(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - graphql: 16.8.1 - tslib: 2.6.2 - value-or-promise: 1.0.12 - - '@graphql-tools/url-loader@8.0.0(@types/node@20.12.7)(graphql@16.8.1)': - dependencies: - '@ardatan/sync-fetch': 0.0.1 - '@graphql-tools/delegate': 10.0.3(graphql@16.8.1) - '@graphql-tools/executor-graphql-ws': 1.1.0(graphql@16.8.1) - '@graphql-tools/executor-http': 1.0.2(@types/node@20.12.7)(graphql@16.8.1) - '@graphql-tools/executor-legacy-ws': 1.0.3(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - '@graphql-tools/wrap': 10.0.1(graphql@16.8.1) - '@types/ws': 8.5.5 - '@whatwg-node/fetch': 0.9.7 - graphql: 16.8.1 - isomorphic-ws: 5.0.0(ws@8.16.0) - tslib: 2.6.2 - value-or-promise: 1.0.12 - ws: 8.16.0 - transitivePeerDependencies: - - '@types/node' - - bufferutil - - encoding - - utf-8-validate - - '@graphql-tools/utils@10.0.6(graphql@16.8.1)': - dependencies: - '@graphql-typed-document-node/core': 3.2.0(graphql@16.8.1) - dset: 3.1.2 - graphql: 16.8.1 - tslib: 2.6.2 - - '@graphql-tools/wrap@10.0.1(graphql@16.8.1)': - dependencies: - '@graphql-tools/delegate': 10.0.3(graphql@16.8.1) - '@graphql-tools/schema': 10.0.0(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - graphql: 16.8.1 - tslib: 2.8.1 - value-or-promise: 1.0.12 - - '@graphql-typed-document-node/core@3.2.0(graphql@16.8.1)': - dependencies: - graphql: 16.8.1 - - '@graphql-yoga/logger@2.0.0': - dependencies: - tslib: 2.6.2 - - '@graphql-yoga/subscription@5.0.0': - dependencies: - '@graphql-yoga/typed-event-target': 3.0.0 - '@repeaterjs/repeater': 3.0.4 - '@whatwg-node/events': 0.1.1 - tslib: 2.6.2 - - '@graphql-yoga/typed-event-target@3.0.0': - dependencies: - '@repeaterjs/repeater': 3.0.4 - tslib: 2.8.1 - '@headlessui/react@2.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/react': 0.26.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -16673,24 +14990,6 @@ snapshots: '@parcel/watcher-win32-ia32': 2.4.1 '@parcel/watcher-win32-x64': 2.4.1 - '@peculiar/asn1-schema@2.3.6': - dependencies: - asn1js: 3.0.5 - pvtsutils: 1.3.5 - tslib: 2.8.1 - - '@peculiar/json-schema@1.1.12': - dependencies: - tslib: 2.8.1 - - '@peculiar/webcrypto@1.4.3': - dependencies: - '@peculiar/asn1-schema': 2.3.6 - '@peculiar/json-schema': 1.1.12 - pvtsutils: 1.3.5 - tslib: 2.8.1 - webcrypto-core: 1.7.7 - '@peggyjs/from-mem@1.2.1': dependencies: semver: 7.6.0 @@ -16707,55 +15006,10 @@ snapshots: tiny-glob: 0.2.9 tslib: 2.8.1 - '@pothos/core@3.41.1(graphql@16.8.1)': - dependencies: - graphql: 16.8.1 - - '@pothos/plugin-errors@3.11.1(@pothos/core@3.41.1(graphql@16.8.1))(graphql@16.8.1)': - dependencies: - '@pothos/core': 3.41.1(graphql@16.8.1) - graphql: 16.8.1 - - '@pothos/plugin-prisma@3.65.2(@pothos/core@3.41.1(graphql@16.8.1))(@prisma/client@5.22.0(prisma@5.22.0))(graphql@16.8.1)(typescript@5.6.3)': - dependencies: - '@pothos/core': 3.41.1(graphql@16.8.1) - '@prisma/client': 5.22.0(prisma@5.22.0) - '@prisma/generator-helper': 5.15.0 - graphql: 16.8.1 - typescript: 5.6.3 - - '@pothos/plugin-relay@3.46.0(@pothos/core@3.41.1(graphql@16.8.1))(graphql@16.8.1)': - dependencies: - '@pothos/core': 3.41.1(graphql@16.8.1) - graphql: 16.8.1 - - '@pothos/plugin-scope-auth@3.22.0(@pothos/core@3.41.1(graphql@16.8.1))(graphql@16.8.1)': - dependencies: - '@pothos/core': 3.41.1(graphql@16.8.1) - graphql: 16.8.1 - - '@pothos/plugin-simple-objects@3.7.0(@pothos/core@3.41.1(graphql@16.8.1))(graphql@16.8.1)': - dependencies: - '@pothos/core': 3.41.1(graphql@16.8.1) - graphql: 16.8.1 - - '@pothos/plugin-validation@3.10.1(@pothos/core@3.41.1(graphql@16.8.1))(graphql@16.8.1)(zod@3.23.8)': - dependencies: - '@pothos/core': 3.41.1(graphql@16.8.1) - graphql: 16.8.1 - zod: 3.23.8 - - '@pothos/plugin-with-input@3.10.1(@pothos/core@3.41.1(graphql@16.8.1))(graphql@16.8.1)': - dependencies: - '@pothos/core': 3.41.1(graphql@16.8.1) - graphql: 16.8.1 - '@prisma/client@5.22.0(prisma@5.22.0)': optionalDependencies: prisma: 5.22.0 - '@prisma/debug@5.15.0': {} - '@prisma/debug@5.22.0': {} '@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2': {} @@ -16773,10 +15027,6 @@ snapshots: '@prisma/engines-version': 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2 '@prisma/get-platform': 5.22.0 - '@prisma/generator-helper@5.15.0': - dependencies: - '@prisma/debug': 5.15.0 - '@prisma/get-platform@5.22.0': dependencies: '@prisma/debug': 5.22.0 @@ -17887,8 +16137,6 @@ snapshots: - '@types/react' - immer - '@repeaterjs/repeater@3.0.4': {} - '@rollup/pluginutils@5.1.0(rollup@4.17.2)': dependencies: '@types/estree': 1.0.5 @@ -19308,16 +17556,12 @@ snapshots: '@types/js-cookie@2.2.7': {} - '@types/js-yaml@4.0.5': {} - '@types/jsdom@20.0.1': dependencies: '@types/node': 22.9.0 '@types/tough-cookie': 4.0.2 parse5: 7.1.2 - '@types/json-stable-stringify@1.0.34': {} - '@types/json5@0.0.29': {} '@types/katex@0.16.3': {} @@ -19387,11 +17631,6 @@ snapshots: dependencies: '@types/react': 18.3.3 - '@types/react-relay@16.0.6': - dependencies: - '@types/react': 18.3.3 - '@types/relay-runtime': 14.1.23 - '@types/react-transition-group@4.4.6': dependencies: '@types/react': 18.3.3 @@ -19401,8 +17640,6 @@ snapshots: '@types/prop-types': 15.7.11 csstype: 3.1.3 - '@types/relay-runtime@14.1.23': {} - '@types/resolve@1.20.6': {} '@types/semver@7.5.8': {} @@ -19434,10 +17671,6 @@ snapshots: '@types/vscode@1.86.0': {} - '@types/ws@8.5.5': - dependencies: - '@types/node': 22.9.0 - '@types/yargs-parser@21.0.0': {} '@types/yargs@17.0.24': @@ -19752,44 +17985,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@whatwg-node/events@0.0.3': {} - - '@whatwg-node/events@0.1.1': {} - - '@whatwg-node/fetch@0.8.8': - dependencies: - '@peculiar/webcrypto': 1.4.3 - '@whatwg-node/node-fetch': 0.3.6 - busboy: 1.6.0 - urlpattern-polyfill: 8.0.2 - web-streams-polyfill: 3.2.1 - - '@whatwg-node/fetch@0.9.7': - dependencies: - '@whatwg-node/node-fetch': 0.4.6 - urlpattern-polyfill: 9.0.0 - - '@whatwg-node/node-fetch@0.3.6': - dependencies: - '@whatwg-node/events': 0.0.3 - busboy: 1.6.0 - fast-querystring: 1.1.1 - fast-url-parser: 1.1.3 - tslib: 2.8.1 - - '@whatwg-node/node-fetch@0.4.6': - dependencies: - '@whatwg-node/events': 0.1.1 - busboy: 1.6.0 - fast-querystring: 1.1.1 - fast-url-parser: 1.1.3 - tslib: 2.8.1 - - '@whatwg-node/server@0.9.1': - dependencies: - '@whatwg-node/fetch': 0.9.7 - tslib: 2.6.2 - '@wogns3623/eslint-plugin-better-exhaustive-deps@1.1.0(eslint@8.57.0)': dependencies: eslint: 8.57.0 @@ -20020,8 +18215,6 @@ snapshots: arrify@1.0.1: {} - asap@2.0.6: {} - asn1.js@5.4.1: dependencies: bn.js: 4.12.0 @@ -20029,12 +18222,6 @@ snapshots: minimalistic-assert: 1.0.1 safer-buffer: 2.1.2 - asn1js@3.0.5: - dependencies: - pvtsutils: 1.3.5 - pvutils: 1.1.3 - tslib: 2.8.1 - assert@2.1.0: dependencies: call-bind: 1.0.7 @@ -20051,8 +18238,6 @@ snapshots: dependencies: tslib: 2.8.1 - astral-regex@2.0.0: {} - astring@1.8.6: {} async@3.2.4: {} @@ -20063,8 +18248,6 @@ snapshots: asynckit@0.4.0: {} - auto-bind@4.0.0: {} - auto-bind@5.0.1: {} autoprefixer@10.4.19(postcss@8.4.38): @@ -20136,12 +18319,6 @@ snapshots: '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.20.5 - babel-plugin-macros@2.8.0: - dependencies: - '@babel/runtime': 7.23.9 - cosmiconfig: 6.0.0 - resolve: 1.22.8 - babel-plugin-macros@3.1.0: dependencies: '@babel/runtime': 7.26.0 @@ -20172,14 +18349,6 @@ snapshots: transitivePeerDependencies: - supports-color - babel-plugin-relay@16.2.0: - dependencies: - babel-plugin-macros: 2.8.0 - cosmiconfig: 5.2.1 - graphql: 15.3.0 - - babel-plugin-syntax-trailing-function-commas@7.0.0-beta.0: {} - babel-preset-current-node-syntax@1.0.1(@babel/core@7.26.0): dependencies: '@babel/core': 7.26.0 @@ -20196,39 +18365,6 @@ snapshots: '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.0) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.26.0) - babel-preset-fbjs@3.4.0(@babel/core@7.26.0): - dependencies: - '@babel/core': 7.26.0 - '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.26.0) - '@babel/plugin-proposal-object-rest-spread': 7.20.7(@babel/core@7.26.0) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.26.0) - '@babel/plugin-syntax-flow': 7.23.3(@babel/core@7.26.0) - '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.26.0) - '@babel/plugin-transform-arrow-functions': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-block-scoped-functions': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-block-scoping': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-classes': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-computed-properties': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-destructuring': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-flow-strip-types': 7.23.3(@babel/core@7.26.0) - '@babel/plugin-transform-for-of': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-function-name': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-literals': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-member-expression-literals': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-modules-commonjs': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-object-super': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-property-literals': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-react-display-name': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-react-jsx': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-shorthand-properties': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-spread': 7.24.7(@babel/core@7.26.0) - '@babel/plugin-transform-template-literals': 7.24.7(@babel/core@7.26.0) - babel-plugin-syntax-trailing-function-commas: 7.0.0-beta.0 - transitivePeerDependencies: - - supports-color - babel-preset-jest@29.6.3(@babel/core@7.26.0): dependencies: '@babel/core': 7.26.0 @@ -20369,13 +18505,6 @@ snapshots: node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.23.0) - browserslist@4.23.3: - dependencies: - caniuse-lite: 1.0.30001679 - electron-to-chromium: 1.5.13 - node-releases: 2.0.18 - update-browserslist-db: 1.1.0(browserslist@4.23.3) - browserslist@4.24.2: dependencies: caniuse-lite: 1.0.30001679 @@ -20432,23 +18561,8 @@ snapshots: get-intrinsic: 1.2.4 set-function-length: 1.2.2 - caller-callsite@2.0.0: - dependencies: - callsites: 2.0.0 - - caller-path@2.0.0: - dependencies: - caller-callsite: 2.0.0 - - callsites@2.0.0: {} - callsites@3.1.0: {} - camel-case@4.1.2: - dependencies: - pascal-case: 3.1.2 - tslib: 2.8.1 - camelcase-css@2.0.1: {} camelcase-keys@6.2.2: @@ -20471,12 +18585,6 @@ snapshots: caniuse-lite@1.0.30001680: {} - capital-case@1.0.4: - dependencies: - no-case: 3.0.4 - tslib: 2.8.1 - upper-case-first: 2.0.2 - ccount@2.0.1: {} chai@4.4.1: @@ -20507,34 +18615,6 @@ snapshots: chalk@5.3.0: {} - change-case-all@1.0.15: - dependencies: - change-case: 4.1.2 - is-lower-case: 2.0.2 - is-upper-case: 2.0.2 - lower-case: 2.0.2 - lower-case-first: 2.0.2 - sponge-case: 1.0.1 - swap-case: 2.0.2 - title-case: 3.0.3 - upper-case: 2.0.2 - upper-case-first: 2.0.2 - - change-case@4.1.2: - dependencies: - camel-case: 4.1.2 - capital-case: 1.0.4 - constant-case: 3.0.4 - dot-case: 3.0.4 - header-case: 2.0.4 - no-case: 3.0.4 - param-case: 3.0.4 - pascal-case: 3.1.2 - path-case: 3.0.4 - sentence-case: 3.0.4 - snake-case: 3.0.4 - tslib: 2.8.1 - char-regex@1.0.2: {} character-entities-html4@2.1.0: {} @@ -20637,18 +18717,11 @@ snapshots: optionalDependencies: '@colors/colors': 1.5.0 - cli-truncate@2.1.0: - dependencies: - slice-ansi: 3.0.0 - string-width: 4.2.3 - cli-truncate@4.0.0: dependencies: slice-ansi: 5.0.0 string-width: 7.2.0 - cli-width@3.0.0: {} - client-only@0.0.1: {} cliui@6.0.0: @@ -20750,8 +18823,6 @@ snapshots: color-string: 1.9.1 optional: true - colorette@2.0.20: {} - combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -20772,8 +18843,6 @@ snapshots: commander@9.5.0: {} - common-tags@1.8.2: {} - commondir@1.0.1: {} compressible@2.0.18: @@ -20805,12 +18874,6 @@ snapshots: consola@3.2.3: {} - constant-case@3.0.4: - dependencies: - no-case: 3.0.4 - tslib: 2.8.1 - upper-case: 2.0.2 - content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 @@ -20839,24 +18902,9 @@ snapshots: core-util-is@1.0.3: {} - cose-base@1.0.3: - dependencies: - layout-base: 1.0.2 - - cosmiconfig@5.2.1: - dependencies: - import-fresh: 2.0.0 - is-directory: 0.3.1 - js-yaml: 3.14.1 - parse-json: 4.0.0 - - cosmiconfig@6.0.0: - dependencies: - '@types/parse-json': 4.0.1 - import-fresh: 3.3.0 - parse-json: 5.2.0 - path-type: 4.0.0 - yaml: 1.10.2 + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 cosmiconfig@7.1.0: dependencies: @@ -20866,15 +18914,6 @@ snapshots: path-type: 4.0.0 yaml: 1.10.2 - cosmiconfig@8.3.6(typescript@5.6.3): - dependencies: - import-fresh: 3.3.0 - js-yaml: 4.1.0 - parse-json: 5.2.0 - path-type: 4.0.0 - optionalDependencies: - typescript: 5.6.3 - create-ecdh@4.0.4: dependencies: bn.js: 4.12.0 @@ -20931,12 +18970,6 @@ snapshots: crelt@1.0.5: {} - cross-fetch@3.1.8: - dependencies: - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding - cross-spawn@5.1.0: dependencies: lru-cache: 4.1.5 @@ -21218,16 +19251,12 @@ snapshots: dataloader@1.4.0: {} - dataloader@2.2.2: {} - date-fns@3.6.0: {} date-fns@4.1.0: {} dayjs@1.11.10: {} - debounce@1.2.1: {} - debug@2.6.9: dependencies: ms: 2.0.0 @@ -21471,11 +19500,6 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 - dot-case@3.0.4: - dependencies: - no-case: 3.0.4 - tslib: 2.8.1 - dotenv-cli@7.4.2: dependencies: cross-spawn: 7.0.3 @@ -21487,8 +19511,6 @@ snapshots: dotenv@16.4.5: {} - dset@3.1.2: {} - duplexify@3.7.1: dependencies: end-of-stream: 1.4.4 @@ -21510,8 +19532,6 @@ snapshots: electron-to-chromium@1.4.752: {} - electron-to-chromium@1.5.13: {} - electron-to-chromium@1.5.51: {} elkjs@0.9.1: {} @@ -21826,7 +19846,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.7 eslint-import-resolver-typescript: 3.5.3(eslint-plugin-import@2.28.1(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.28.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.5.3)(eslint@8.57.0) + eslint-plugin-import: 2.28.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.5.3(eslint-plugin-import@2.28.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.7.1(eslint@8.57.0) eslint-plugin-react: 7.33.2(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -21849,7 +19869,7 @@ snapshots: debug: 4.3.6 enhanced-resolve: 5.15.0 eslint: 8.57.0 - eslint-plugin-import: 2.28.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.5.3)(eslint@8.57.0) + eslint-plugin-import: 2.28.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.5.3(eslint-plugin-import@2.28.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) get-tsconfig: 4.7.5 globby: 13.1.3 is-core-module: 2.13.1 @@ -21869,7 +19889,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.28.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.5.3)(eslint@8.57.0): + eslint-plugin-import@2.28.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.5.3(eslint-plugin-import@2.28.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: array-includes: 3.1.7 array.prototype.findlastindex: 1.2.3 @@ -22202,14 +20222,10 @@ snapshots: iconv-lite: 0.4.24 tmp: 0.0.33 - extract-files@11.0.0: {} - fast-check@3.19.0: dependencies: pure-rand: 6.1.0 - fast-decode-uri-component@1.0.1: {} - fast-deep-equal@3.1.3: {} fast-glob@3.3.2: @@ -22226,10 +20242,6 @@ snapshots: fast-loops@1.1.3: {} - fast-querystring@1.1.1: - dependencies: - fast-decode-uri-component: 1.0.1 - fast-shallow-equal@1.0.0: {} fast-url-parser@1.1.3: @@ -22252,20 +20264,6 @@ snapshots: dependencies: bser: 2.1.1 - fbjs-css-vars@1.0.2: {} - - fbjs@3.0.5: - dependencies: - cross-fetch: 3.1.8 - fbjs-css-vars: 1.0.2 - loose-envify: 1.4.0 - object-assign: 4.1.1 - promise: 7.3.1 - setimmediate: 1.0.5 - ua-parser-js: 1.0.37 - transitivePeerDependencies: - - encoding - fd-slicer@1.1.0: dependencies: pend: 1.2.0 @@ -22276,10 +20274,6 @@ snapshots: fetch-retry@5.0.6: {} - figures@3.2.0: - dependencies: - escape-string-regexp: 1.0.5 - figures@5.0.0: dependencies: escape-string-regexp: 5.0.0 @@ -22695,63 +20689,6 @@ snapshots: graphemer@1.4.0: {} - graphql-config@5.0.2(@types/node@20.12.7)(graphql@16.8.1)(typescript@5.6.3): - dependencies: - '@graphql-tools/graphql-file-loader': 8.0.0(graphql@16.8.1) - '@graphql-tools/json-file-loader': 8.0.0(graphql@16.8.1) - '@graphql-tools/load': 8.0.0(graphql@16.8.1) - '@graphql-tools/merge': 9.0.0(graphql@16.8.1) - '@graphql-tools/url-loader': 8.0.0(@types/node@20.12.7)(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - cosmiconfig: 8.3.6(typescript@5.6.3) - graphql: 16.8.1 - jiti: 1.21.0 - minimatch: 4.2.3 - string-env-interpolation: 1.0.1 - tslib: 2.6.2 - transitivePeerDependencies: - - '@types/node' - - bufferutil - - encoding - - typescript - - utf-8-validate - - graphql-request@6.1.0(graphql@16.8.1): - dependencies: - '@graphql-typed-document-node/core': 3.2.0(graphql@16.8.1) - cross-fetch: 3.1.8 - graphql: 16.8.1 - transitivePeerDependencies: - - encoding - - graphql-tag@2.12.6(graphql@16.8.1): - dependencies: - graphql: 16.8.1 - tslib: 2.8.1 - - graphql-ws@5.14.0(graphql@16.8.1): - dependencies: - graphql: 16.8.1 - - graphql-yoga@5.1.1(graphql@16.8.1): - dependencies: - '@envelop/core': 5.0.0 - '@graphql-tools/executor': 1.1.0(graphql@16.8.1) - '@graphql-tools/schema': 10.0.0(graphql@16.8.1) - '@graphql-tools/utils': 10.0.6(graphql@16.8.1) - '@graphql-yoga/logger': 2.0.0 - '@graphql-yoga/subscription': 5.0.0 - '@whatwg-node/fetch': 0.9.7 - '@whatwg-node/server': 0.9.1 - dset: 3.1.2 - graphql: 16.8.1 - lru-cache: 10.0.0 - tslib: 2.6.2 - - graphql@15.3.0: {} - - graphql@16.8.1: {} - gray-matter@4.0.3: dependencies: js-yaml: 3.14.1 @@ -22951,11 +20888,6 @@ snapshots: property-information: 6.4.0 space-separated-tokens: 2.0.2 - header-case@2.0.4: - dependencies: - capital-case: 1.0.4 - tslib: 2.8.1 - heap@0.2.7: {} hmac-drbg@1.0.1: @@ -23025,13 +20957,6 @@ snapshots: transitivePeerDependencies: - supports-color - http-proxy-agent@7.0.0: - dependencies: - agent-base: 7.1.0 - debug: 4.3.6 - transitivePeerDependencies: - - supports-color - http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.0 @@ -23046,13 +20971,6 @@ snapshots: transitivePeerDependencies: - supports-color - https-proxy-agent@7.0.2: - dependencies: - agent-base: 7.1.0 - debug: 4.3.6 - transitivePeerDependencies: - - supports-color - https-proxy-agent@7.0.4: dependencies: agent-base: 7.1.0 @@ -23105,22 +21023,13 @@ snapshots: dependencies: queue: 6.0.2 - immutable@3.7.6: {} - immutable@4.3.6: {} - import-fresh@2.0.0: - dependencies: - caller-path: 2.0.0 - resolve-from: 3.0.0 - import-fresh@3.3.0: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 - import-from@4.0.0: {} - import-local@3.1.0: dependencies: pkg-dir: 4.2.0 @@ -23188,24 +21097,6 @@ snapshots: css-in-js-utils: 3.1.0 fast-loops: 1.1.3 - inquirer@8.2.6: - dependencies: - ansi-escapes: 4.3.2 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-width: 3.0.0 - external-editor: 3.1.0 - figures: 3.2.0 - lodash: 4.17.21 - mute-stream: 0.0.8 - ora: 5.4.1 - run-async: 2.4.1 - rxjs: 7.8.1 - string-width: 4.2.3 - strip-ansi: 6.0.1 - through: 2.3.8 - wrap-ansi: 6.2.0 - internal-slot@1.0.5: dependencies: get-intrinsic: 1.2.2 @@ -23224,11 +21115,6 @@ snapshots: is-absolute-url@4.0.1: {} - is-absolute@1.0.0: - dependencies: - is-relative: 1.0.0 - is-windows: 1.0.2 - is-alphabetical@2.0.1: {} is-alphanumerical@2.0.1: @@ -23285,8 +21171,6 @@ snapshots: is-deflate@1.0.0: {} - is-directory@0.3.1: {} - is-docker@2.2.1: {} is-docker@3.0.0: {} @@ -23329,10 +21213,6 @@ snapshots: is-interactive@1.0.0: {} - is-lower-case@2.0.2: - dependencies: - tslib: 2.8.1 - is-map@2.0.2: {} is-nan@1.3.2: @@ -23377,10 +21257,6 @@ snapshots: call-bind: 1.0.7 has-tostringtag: 1.0.0 - is-relative@1.0.0: - dependencies: - is-unc-path: 1.0.0 - is-set@2.0.2: {} is-shared-array-buffer@1.0.2: @@ -23411,18 +21287,10 @@ snapshots: dependencies: which-typed-array: 1.1.15 - is-unc-path@1.0.0: - dependencies: - unc-path-regex: 0.1.2 - is-unicode-supported@0.1.0: {} is-unicode-supported@1.3.0: {} - is-upper-case@2.0.2: - dependencies: - tslib: 2.8.1 - is-weakmap@2.0.1: {} is-weakref@1.0.2: @@ -23460,14 +21328,6 @@ snapshots: isobject@3.0.1: {} - isomorphic-ws@5.0.0(ws@8.14.1): - dependencies: - ws: 8.14.1 - - isomorphic-ws@5.0.0(ws@8.16.0): - dependencies: - ws: 8.16.0 - istanbul-lib-coverage@3.2.0: {} istanbul-lib-instrument@5.2.1: @@ -23952,12 +21812,8 @@ snapshots: - supports-color - ts-node - jiti@1.20.0: {} - jiti@1.21.0: {} - jose@4.15.5: {} - jose@5.9.6: {} js-cookie@2.2.1: {} @@ -24069,8 +21925,6 @@ snapshots: json-buffer@3.0.1: {} - json-parse-better-errors@1.0.2: {} - json-parse-even-better-errors@2.3.1: {} json-parse-even-better-errors@3.0.2: {} @@ -24079,15 +21933,6 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} - json-stable-stringify@1.0.2: - dependencies: - jsonify: 0.0.1 - - json-to-pretty-yaml@1.2.2: - dependencies: - remedial: 1.0.8 - remove-trailing-spaces: 1.0.8 - json5@1.0.2: dependencies: minimist: 1.2.8 @@ -24106,8 +21951,6 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 - jsonify@0.0.1: {} - jsonwebtoken@9.0.2: dependencies: jws: 3.2.2 @@ -24268,19 +22111,6 @@ snapshots: dependencies: uc.micro: 1.0.6 - listr2@4.0.5(enquirer@2.3.6): - dependencies: - cli-truncate: 2.1.0 - colorette: 2.0.20 - log-update: 4.0.0 - p-map: 4.0.0 - rfdc: 1.3.0 - rxjs: 7.8.1 - through: 2.3.8 - wrap-ansi: 7.0.0 - optionalDependencies: - enquirer: 2.3.6 - lite-emit@2.3.0: {} load-plugin@6.0.3: @@ -24334,8 +22164,6 @@ snapshots: lodash.once@4.1.1: {} - lodash.sortby@4.7.0: {} - lodash.startcase@4.4.0: {} lodash@4.17.21: {} @@ -24345,13 +22173,6 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 - log-update@4.0.0: - dependencies: - ansi-escapes: 4.3.2 - cli-cursor: 3.1.0 - slice-ansi: 4.0.0 - wrap-ansi: 6.2.0 - longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -24362,16 +22183,6 @@ snapshots: dependencies: get-func-name: 2.0.2 - lower-case-first@2.0.2: - dependencies: - tslib: 2.8.1 - - lower-case@2.0.2: - dependencies: - tslib: 2.8.1 - - lru-cache@10.0.0: {} - lru-cache@10.2.2: {} lru-cache@4.1.5: @@ -24418,8 +22229,6 @@ snapshots: dependencies: tmpl: 1.0.5 - map-cache@0.2.2: {} - map-obj@1.0.1: {} map-obj@4.3.0: {} @@ -24748,10 +22557,6 @@ snapshots: transitivePeerDependencies: - supports-color - meros@1.3.0(@types/node@20.12.7): - optionalDependencies: - '@types/node': 20.12.7 - methods@1.1.2: {} micromark-core-commonmark@1.1.0: @@ -25207,10 +23012,6 @@ snapshots: dependencies: brace-expansion: 1.1.11 - minimatch@4.2.3: - dependencies: - brace-expansion: 1.1.11 - minimatch@5.1.6: dependencies: brace-expansion: 2.0.1 @@ -25385,11 +23186,6 @@ snapshots: - '@babel/core' - babel-plugin-macros - no-case@3.0.4: - dependencies: - lower-case: 2.0.2 - tslib: 2.8.1 - node-abi@3.47.0: dependencies: semver: 7.6.3 @@ -25456,10 +23252,6 @@ snapshots: semver: 7.6.3 validate-npm-package-license: 3.0.4 - normalize-path@2.1.1: - dependencies: - remove-trailing-separator: 1.1.0 - normalize-path@3.0.0: {} normalize-range@0.1.2: {} @@ -25496,8 +23288,6 @@ snapshots: dependencies: boolbase: 1.0.0 - nullthrows@1.1.1: {} - nwsapi@2.2.10: {} nwsapi@2.2.13: {} @@ -25701,11 +23491,6 @@ snapshots: pako@2.1.0: {} - param-case@3.0.4: - dependencies: - dot-case: 3.0.4 - tslib: 2.8.1 - parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -25729,17 +23514,6 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 - parse-filepath@1.0.2: - dependencies: - is-absolute: 1.0.0 - map-cache: 0.2.2 - path-root: 0.1.1 - - parse-json@4.0.0: - dependencies: - error-ex: 1.3.2 - json-parse-better-errors: 1.0.2 - parse-json@5.2.0: dependencies: '@babel/code-frame': 7.24.7 @@ -25764,20 +23538,10 @@ snapshots: parseurl@1.3.3: {} - pascal-case@3.1.2: - dependencies: - no-case: 3.0.4 - tslib: 2.8.1 - patch-console@2.0.0: {} path-browserify@1.0.1: {} - path-case@3.0.4: - dependencies: - dot-case: 3.0.4 - tslib: 2.8.1 - path-exists@3.0.0: {} path-exists@4.0.0: {} @@ -25790,12 +23554,6 @@ snapshots: path-parse@1.0.7: {} - path-root-regex@0.1.2: {} - - path-root@0.1.1: - dependencies: - path-root-regex: 0.1.2 - path-scurry@1.10.2: dependencies: lru-cache: 10.2.2 @@ -26047,10 +23805,6 @@ snapshots: err-code: 2.0.3 retry: 0.12.0 - promise@7.3.1: - dependencies: - asap: 2.0.6 - prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -26112,12 +23866,6 @@ snapshots: pure-rand@6.1.0: {} - pvtsutils@1.3.5: - dependencies: - tslib: 2.8.1 - - pvutils@1.1.3: {} - qs@6.11.0: dependencies: side-channel: 1.0.6 @@ -26293,17 +24041,6 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 - react-relay@16.2.0(react@18.3.1): - dependencies: - '@babel/runtime': 7.23.9 - fbjs: 3.0.5 - invariant: 2.2.4 - nullthrows: 1.1.1 - react: 18.3.1 - relay-runtime: 16.2.0 - transitivePeerDependencies: - - encoding - react-remove-scroll-bar@2.3.6(@types/react@18.3.3)(react@18.3.1): dependencies: react: 18.3.1 @@ -26697,24 +24434,6 @@ snapshots: hast-util-to-string: 3.0.0 unist-util-visit: 5.0.0 - relay-compiler@16.2.0: {} - - relay-runtime@12.0.0: - dependencies: - '@babel/runtime': 7.26.0 - fbjs: 3.0.5 - invariant: 2.2.4 - transitivePeerDependencies: - - encoding - - relay-runtime@16.2.0: - dependencies: - '@babel/runtime': 7.23.9 - fbjs: 3.0.5 - invariant: 2.2.4 - transitivePeerDependencies: - - encoding - remark-breaks@4.0.0: dependencies: '@types/mdast': 4.0.2 @@ -26813,12 +24532,6 @@ snapshots: transitivePeerDependencies: - supports-color - remedial@1.0.8: {} - - remove-trailing-separator@1.1.0: {} - - remove-trailing-spaces@1.0.8: {} - require-directory@2.1.1: {} require-main-filename@2.0.0: {} @@ -26831,8 +24544,6 @@ snapshots: dependencies: resolve-from: 5.0.0 - resolve-from@3.0.0: {} - resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -26867,8 +24578,6 @@ snapshots: reusify@1.0.4: {} - rfdc@1.3.0: {} - rimraf@2.6.3: dependencies: glob: 7.2.3 @@ -26925,18 +24634,12 @@ snapshots: run-applescript@7.0.0: {} - run-async@2.4.1: {} - run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 rw@1.3.3: {} - rxjs@7.8.1: - dependencies: - tslib: 2.8.1 - sade@1.8.1: dependencies: mri: 1.2.0 @@ -26978,8 +24681,6 @@ snapshots: dependencies: compute-scroll-into-view: 3.1.0 - scuid@1.1.0: {} - section-matter@1.0.0: dependencies: extend-shallow: 2.0.1 @@ -27019,12 +24720,6 @@ snapshots: transitivePeerDependencies: - supports-color - sentence-case@3.0.4: - dependencies: - no-case: 3.0.4 - tslib: 2.8.1 - upper-case-first: 2.0.2 - serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -27067,8 +24762,6 @@ snapshots: set-harmonic-interval@1.0.1: {} - setimmediate@1.0.5: {} - setprototypeof@1.2.0: {} sha.js@2.4.11: @@ -27119,8 +24812,6 @@ snapshots: shebang-regex@3.0.0: {} - shell-quote@1.8.1: {} - shiki@0.14.7: dependencies: ansi-sequence-parser: 1.1.0 @@ -27166,8 +24857,6 @@ snapshots: signal-exit@4.1.0: {} - signedsource@1.0.0: {} - simple-concat@1.0.1: optional: true @@ -27195,18 +24884,6 @@ snapshots: slash@5.1.0: {} - slice-ansi@3.0.0: - dependencies: - ansi-styles: 4.3.0 - astral-regex: 2.0.0 - is-fullwidth-code-point: 3.0.0 - - slice-ansi@4.0.0: - dependencies: - ansi-styles: 4.3.0 - astral-regex: 2.0.0 - is-fullwidth-code-point: 3.0.0 - slice-ansi@5.0.0: dependencies: ansi-styles: 6.2.1 @@ -27226,11 +24903,6 @@ snapshots: wcwidth: 1.0.1 yargs: 15.4.1 - snake-case@3.0.4: - dependencies: - dot-case: 3.0.4 - tslib: 2.8.1 - source-map-generator@0.8.0: {} source-map-js@1.2.0: {} @@ -27276,10 +24948,6 @@ snapshots: spdx-license-ids@3.0.12: {} - sponge-case@1.0.1: - dependencies: - tslib: 2.8.1 - sprintf-js@1.0.3: {} stack-generator@2.0.10: @@ -27333,8 +25001,6 @@ snapshots: streamsearch@1.1.0: {} - string-env-interpolation@1.0.1: {} - string-length@4.0.2: dependencies: char-regex: 1.0.2 @@ -27490,10 +25156,6 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - swap-case@2.0.2: - dependencies: - tslib: 2.8.1 - symbol-tree@3.2.4: {} synckit@0.8.5: @@ -27677,8 +25339,6 @@ snapshots: readable-stream: 2.3.8 xtend: 4.0.2 - through@2.3.8: {} - tiny-glob@0.2.9: dependencies: globalyzer: 0.1.0 @@ -27693,10 +25353,6 @@ snapshots: tinyspy@2.2.1: {} - title-case@3.0.3: - dependencies: - tslib: 2.8.1 - titleize@3.0.0: {} tldts-core@6.1.58: {} @@ -27770,8 +25426,6 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-log@2.2.5: {} - ts-node@10.9.2(@types/node@20.12.7)(typescript@5.6.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -27959,8 +25613,6 @@ snapshots: typescript@5.6.3: {} - ua-parser-js@1.0.37: {} - uc.micro@1.0.6: {} ufo@1.3.2: {} @@ -27975,8 +25627,6 @@ snapshots: has-symbols: 1.0.3 which-boxed-primitive: 1.0.2 - unc-path-regex@0.1.2: {} - undefsafe@2.0.5: {} underscore@1.13.6: {} @@ -28109,10 +25759,6 @@ snapshots: universalify@2.0.1: {} - unixify@1.0.0: - dependencies: - normalize-path: 2.1.1 - unpipe@1.0.0: {} unplugin@1.10.1: @@ -28130,26 +25776,12 @@ snapshots: escalade: 3.1.1 picocolors: 1.1.1 - update-browserslist-db@1.1.0(browserslist@4.23.3): - dependencies: - browserslist: 4.23.3 - escalade: 3.2.0 - picocolors: 1.1.1 - update-browserslist-db@1.1.1(browserslist@4.24.2): dependencies: browserslist: 4.24.2 escalade: 3.2.0 picocolors: 1.1.1 - upper-case-first@2.0.2: - dependencies: - tslib: 2.8.1 - - upper-case@2.0.2: - dependencies: - tslib: 2.8.1 - uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -28165,10 +25797,6 @@ snapshots: dependencies: fast-url-parser: 1.1.3 - urlpattern-polyfill@8.0.2: {} - - urlpattern-polyfill@9.0.0: {} - use-callback-ref@1.3.2(@types/react@18.3.3)(react@18.3.1): dependencies: react: 18.3.1 @@ -28262,8 +25890,6 @@ snapshots: validate-npm-package-name@5.0.1: {} - value-or-promise@1.0.12: {} - vary@1.1.2: {} vfile-location@5.0.2: @@ -28378,20 +26004,10 @@ snapshots: web-namespaces@2.0.1: {} - web-streams-polyfill@3.2.1: {} - web-streams-polyfill@4.0.0-beta.3: {} web-worker@1.2.0: {} - webcrypto-core@1.7.7: - dependencies: - '@peculiar/asn1-schema': 2.3.6 - '@peculiar/json-schema': 1.1.12 - asn1js: 3.0.5 - pvtsutils: 1.3.5 - tslib: 2.8.1 - webidl-conversions@3.0.1: {} webidl-conversions@7.0.0: {} @@ -28535,10 +26151,6 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 - ws@8.14.1: {} - - ws@8.16.0: {} - ws@8.17.0: {} ws@8.18.0: {} @@ -28581,8 +26193,6 @@ snapshots: yallist@4.0.0: {} - yaml-ast-parser@0.0.43: {} - yaml@1.10.2: {} yaml@2.3.4: {} From 78d0e8b689c7d01a63021d966bc6860f6206bc95 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 29 Nov 2024 20:41:58 -0300 Subject: [PATCH 47/68] small fixes --- packages/hub/.gitignore | 4 ---- .../variables/[variableName]/VariableRevisionsPanel.tsx | 2 +- packages/hub/src/hooks/usePaginator.ts | 6 +++--- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/hub/.gitignore b/packages/hub/.gitignore index c04a9a2c17..2bfa825fa2 100644 --- a/packages/hub/.gitignore +++ b/packages/hub/.gitignore @@ -4,7 +4,3 @@ /tsconfig.tsbuildinfo /.next /.vscode - -# Ignore all of the generated files, but keep the directory -src/__generated__/* -!src/__generated__/.gitkeep diff --git a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/VariableRevisionsPanel.tsx b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/VariableRevisionsPanel.tsx index d17924e705..7647f292fb 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/VariableRevisionsPanel.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/VariableRevisionsPanel.tsx @@ -2,7 +2,6 @@ import clsx from "clsx"; import { format } from "date-fns"; -import Link from "next/link"; import { usePathname } from "next/navigation"; import { FC } from "react"; import { FaClock, FaMinusCircle } from "react-icons/fa"; @@ -10,6 +9,7 @@ import { FaClock, FaMinusCircle } from "react-icons/fa"; import { CheckIcon, XIcon } from "@quri/ui"; import { LoadMore } from "@/components/LoadMore"; +import { Link } from "@/components/ui/Link"; import { usePaginator } from "@/hooks/usePaginator"; import { exportTypeIcon } from "@/lib/typeIcon"; import { variableRevisionRoute } from "@/routes"; diff --git a/packages/hub/src/hooks/usePaginator.ts b/packages/hub/src/hooks/usePaginator.ts index 243e504aae..e04ff3d9b6 100644 --- a/packages/hub/src/hooks/usePaginator.ts +++ b/packages/hub/src/hooks/usePaginator.ts @@ -18,21 +18,21 @@ export function usePaginator(initialPage: Paginated): FullPaginated { const [{ items, loadMore }, setPage] = useState(initialPage); const append = useCallback((newItem: T) => { - setPage(({ items }) => ({ + setPage(({ items, loadMore }) => ({ items: [...items, newItem], loadMore, })); }, []); const remove = useCallback((compare: (item: T) => boolean) => { - setPage(({ items }) => ({ + setPage(({ items, loadMore }) => ({ items: items.filter((i) => !compare(i)), loadMore, })); }, []); const update = useCallback((update: (item: T) => T) => { - setPage(({ items }) => { + setPage(({ items, loadMore }) => { const newItems = { items: items.map(update), loadMore, From 265fcec623921524d7e4f840940fe421d7e36d25 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 29 Nov 2024 22:45:39 -0300 Subject: [PATCH 48/68] show model content while layout is loading --- .../app/models/[owner]/[slug]/FallbackLayout.tsx | 16 +++++++++++----- .../hub/src/app/models/[owner]/[slug]/layout.tsx | 8 +++++++- .../src/squiggle/components/ImportTooltip.tsx | 2 +- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/hub/src/app/models/[owner]/[slug]/FallbackLayout.tsx b/packages/hub/src/app/models/[owner]/[slug]/FallbackLayout.tsx index 49635f860c..f108d318a5 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/FallbackLayout.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/FallbackLayout.tsx @@ -1,17 +1,21 @@ "use client"; -import { FC } from "react"; +import { FC, PropsWithChildren } from "react"; import { EntityLayout } from "@/components/EntityLayout"; import { ModelEntityNodes } from "./ModelEntityNodes"; -type Props = { +type Props = PropsWithChildren<{ username: string; slug: string; -}; +}>; -export const FallbackModelLayout: FC = ({ username, slug }) => { +export const FallbackModelLayout: FC = ({ + username, + slug, + children, +}) => { return ( = ({ username, slug }) => { }} /> } - /> + > + {children} + ); }; diff --git a/packages/hub/src/app/models/[owner]/[slug]/layout.tsx b/packages/hub/src/app/models/[owner]/[slug]/layout.tsx index 96d22f8360..d8c0a982cd 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/layout.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/layout.tsx @@ -31,7 +31,13 @@ async function LoadedLayout({ params, children }: Props) { export default async function Layout({ params, children }: Props) { const { owner, slug } = await params; return ( - }> + + {children} + + } + > {children} ); diff --git a/packages/hub/src/squiggle/components/ImportTooltip.tsx b/packages/hub/src/squiggle/components/ImportTooltip.tsx index 43bb20429f..9d6baac7ba 100644 --- a/packages/hub/src/squiggle/components/ImportTooltip.tsx +++ b/packages/hub/src/squiggle/components/ImportTooltip.tsx @@ -23,7 +23,7 @@ export const ImportTooltip: FC = ({ importId }) => { // TODO - this is done with a server action, so it's not cached. // A route would be better. loadModelCardAction({ owner, slug }).then(setModel); - }, []); + }, [owner, slug]); return (
Date: Fri, 29 Nov 2024 22:51:11 -0300 Subject: [PATCH 49/68] turbo loose mode for CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a901ef0e6..d3dab20788 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,4 +32,4 @@ jobs: - name: Install dependencies run: pnpm install - name: Turbo run - run: npx turbo run build test lint + run: npx turbo run build test lint --env-mode=loose From 4298f7a0b59b6bb6aac3928fe19e3b5ca301525d Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 29 Nov 2024 22:55:46 -0300 Subject: [PATCH 50/68] remove unnecessary server-only imports --- packages/hub/src/components/WithAuth/index.tsx | 2 -- packages/hub/src/server/ai/analytics/index.ts | 2 -- packages/hub/src/server/globalStatistics.ts | 2 -- packages/hub/src/server/groups/data/card.ts | 2 -- packages/hub/src/server/models/data/authHelpers.ts | 2 -- packages/hub/src/server/models/data/byVersion.ts | 2 -- packages/hub/src/server/models/data/cards.ts | 2 -- packages/hub/src/server/models/data/full.ts | 2 -- packages/hub/src/server/owners/data/findOwners.ts | 2 -- packages/hub/src/server/relative-values/data/cards.ts | 2 -- packages/hub/src/server/runSquiggle.ts | 2 -- packages/hub/src/server/users/auth.ts | 2 -- packages/hub/src/server/variables/data/variableCards.ts | 2 -- 13 files changed, 26 deletions(-) diff --git a/packages/hub/src/components/WithAuth/index.tsx b/packages/hub/src/components/WithAuth/index.tsx index fcdccc2993..9552b1f952 100644 --- a/packages/hub/src/components/WithAuth/index.tsx +++ b/packages/hub/src/components/WithAuth/index.tsx @@ -1,5 +1,3 @@ -import "server-only"; - import { FC, PropsWithChildren } from "react"; import { auth } from "@/auth"; diff --git a/packages/hub/src/server/ai/analytics/index.ts b/packages/hub/src/server/ai/analytics/index.ts index 756295c346..279e1e7b43 100644 --- a/packages/hub/src/server/ai/analytics/index.ts +++ b/packages/hub/src/server/ai/analytics/index.ts @@ -1,5 +1,3 @@ -import "server-only"; - import * as Prisma from "@prisma/client"; import { CodeArtifact, Workflow } from "@quri/squiggle-ai/server"; diff --git a/packages/hub/src/server/globalStatistics.ts b/packages/hub/src/server/globalStatistics.ts index d51cf2ca1c..93ba77ea37 100644 --- a/packages/hub/src/server/globalStatistics.ts +++ b/packages/hub/src/server/globalStatistics.ts @@ -1,5 +1,3 @@ -import "server-only"; - import { prisma } from "@/prisma"; export async function getGlobalStatistics() { diff --git a/packages/hub/src/server/groups/data/card.ts b/packages/hub/src/server/groups/data/card.ts index 322da4e42c..b0ed1129ca 100644 --- a/packages/hub/src/server/groups/data/card.ts +++ b/packages/hub/src/server/groups/data/card.ts @@ -1,5 +1,3 @@ -import "server-only"; - import { Prisma } from "@prisma/client"; import { auth } from "@/auth"; diff --git a/packages/hub/src/server/models/data/authHelpers.ts b/packages/hub/src/server/models/data/authHelpers.ts index 843b7258ee..54c7fa85f3 100644 --- a/packages/hub/src/server/models/data/authHelpers.ts +++ b/packages/hub/src/server/models/data/authHelpers.ts @@ -1,5 +1,3 @@ -import "server-only"; - import { Prisma } from "@prisma/client"; import { auth } from "@/auth"; diff --git a/packages/hub/src/server/models/data/byVersion.ts b/packages/hub/src/server/models/data/byVersion.ts index fb0d4f44ec..bf530099a6 100644 --- a/packages/hub/src/server/models/data/byVersion.ts +++ b/packages/hub/src/server/models/data/byVersion.ts @@ -1,5 +1,3 @@ -import "server-only"; - import { prisma } from "@/prisma"; import { checkRootUser } from "@/server/users/auth"; diff --git a/packages/hub/src/server/models/data/cards.ts b/packages/hub/src/server/models/data/cards.ts index 8268f40c18..1bcf6bdd04 100644 --- a/packages/hub/src/server/models/data/cards.ts +++ b/packages/hub/src/server/models/data/cards.ts @@ -1,5 +1,3 @@ -import "server-only"; - import { Prisma } from "@prisma/client"; import { prisma } from "@/prisma"; diff --git a/packages/hub/src/server/models/data/full.ts b/packages/hub/src/server/models/data/full.ts index 1ce89c96f5..d22fdac4d7 100644 --- a/packages/hub/src/server/models/data/full.ts +++ b/packages/hub/src/server/models/data/full.ts @@ -1,5 +1,3 @@ -import "server-only"; - import { Prisma } from "@prisma/client"; import { prisma } from "@/prisma"; diff --git a/packages/hub/src/server/owners/data/findOwners.ts b/packages/hub/src/server/owners/data/findOwners.ts index ef017bc8bf..061966ecc9 100644 --- a/packages/hub/src/server/owners/data/findOwners.ts +++ b/packages/hub/src/server/owners/data/findOwners.ts @@ -1,5 +1,3 @@ -import "server-only"; - import { auth } from "@/auth"; import { prisma } from "@/prisma"; diff --git a/packages/hub/src/server/relative-values/data/cards.ts b/packages/hub/src/server/relative-values/data/cards.ts index 5c1d755638..c08919e3f9 100644 --- a/packages/hub/src/server/relative-values/data/cards.ts +++ b/packages/hub/src/server/relative-values/data/cards.ts @@ -1,5 +1,3 @@ -import "server-only"; - import { Prisma } from "@prisma/client"; import { prisma } from "@/prisma"; diff --git a/packages/hub/src/server/runSquiggle.ts b/packages/hub/src/server/runSquiggle.ts index 262cd8fc4e..88678bd128 100644 --- a/packages/hub/src/server/runSquiggle.ts +++ b/packages/hub/src/server/runSquiggle.ts @@ -1,5 +1,3 @@ -import "server-only"; - import { Prisma } from "@prisma/client"; import crypto from "crypto"; diff --git a/packages/hub/src/server/users/auth.ts b/packages/hub/src/server/users/auth.ts index 89aa286043..8b57dfc747 100644 --- a/packages/hub/src/server/users/auth.ts +++ b/packages/hub/src/server/users/auth.ts @@ -1,5 +1,3 @@ -import "server-only"; - import { User } from "@prisma/client"; import { Session } from "next-auth"; import { redirect } from "next/navigation"; diff --git a/packages/hub/src/server/variables/data/variableCards.ts b/packages/hub/src/server/variables/data/variableCards.ts index 9b91b7e4bb..d2f1a92561 100644 --- a/packages/hub/src/server/variables/data/variableCards.ts +++ b/packages/hub/src/server/variables/data/variableCards.ts @@ -1,5 +1,3 @@ -import "server-only"; - import { Prisma } from "@prisma/client"; import { prisma } from "@/prisma"; From 0c5613170f039d0f2a604e29ffa54f6a1f7cba5b Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Sat, 30 Nov 2024 00:20:00 -0300 Subject: [PATCH 51/68] reorganize files --- packages/hub/.prettierignore | 3 -- packages/hub/README.md | 19 +------- .../index.ts => ai/data/analytics.ts} | 8 ++-- .../ai/data.ts => ai/data/loadWorkflows.ts} | 4 +- .../hub/src/{server/ai => ai/data}/storage.ts | 4 +- .../hub/src/{server/ai => ai/data}/utils.ts | 0 .../hub/src/{server/ai => ai/data}/v1_0.ts | 0 .../hub/src/{server/ai => ai/data}/v2_0.ts | 0 .../src/app/(frontpage)/definitions/page.tsx | 2 +- .../hub/src/app/(frontpage)/groups/page.tsx | 2 +- packages/hub/src/app/(frontpage)/layout.tsx | 2 +- packages/hub/src/app/(frontpage)/page.tsx | 2 +- .../src/app/(frontpage)/variables/page.tsx | 2 +- packages/hub/src/app/about/page.tsx | 2 +- packages/hub/src/app/admin/dev/page.tsx | 2 +- packages/hub/src/app/admin/layout.tsx | 4 +- packages/hub/src/app/admin/search/page.tsx | 4 +- .../upgrade-versions/UpgradeVersionsPage.tsx | 6 +-- .../upgrade-versions/UpgradeableModel.tsx | 6 +-- .../src/app/admin/upgrade-versions/page.tsx | 4 +- .../app/ai/WorkflowViewer/ClientStepView.tsx | 2 +- .../hub/src/app/ai/WorkflowViewer/index.tsx | 2 +- .../src/app/ai/analytics/StepErrorList.tsx | 2 +- .../src/app/ai/analytics/code-errors/page.tsx | 2 +- packages/hub/src/app/ai/analytics/page.tsx | 2 +- .../src/app/ai/analytics/step-errors/page.tsx | 2 +- packages/hub/src/app/ai/api/create/route.ts | 8 ++-- packages/hub/src/app/ai/page.tsx | 2 +- .../src/app/api/auth/[...nextauth]/route.ts | 2 +- packages/hub/src/app/api/find-owners/route.ts | 2 +- .../src/app/api/find-relative-values/route.ts | 2 +- packages/hub/src/app/api/get-source/route.ts | 4 +- packages/hub/src/app/api/runSquiggle/route.ts | 2 +- packages/hub/src/app/api/search/route.ts | 6 +-- .../src/app/groups/[slug]/NewModelButton.tsx | 2 +- .../invite-link/AcceptGroupInvitePage.tsx | 6 +-- .../app/groups/[slug]/invite-link/page.tsx | 6 +-- packages/hub/src/app/groups/[slug]/layout.tsx | 6 +-- .../[slug]/members/AddUserToGroupAction.tsx | 4 +- .../[slug]/members/DeleteMembershipAction.tsx | 4 +- .../groups/[slug]/members/GroupMemberCard.tsx | 4 +- .../groups/[slug]/members/GroupMemberList.tsx | 6 +-- .../members/GroupReusableInviteSection.tsx | 6 +-- .../[slug]/members/MembershipRoleButton.tsx | 2 +- .../members/SetMembershipRoleAction.tsx | 4 +- .../src/app/groups/[slug]/members/page.tsx | 2 +- packages/hub/src/app/groups/[slug]/page.tsx | 4 +- .../[owner]/[slug]/DeleteModelAction.tsx | 6 +-- .../[slug]/EditSquiggleSnippetModel.tsx | 12 ++--- .../[owner]/[slug]/ModelEntityNodes.tsx | 2 +- .../app/models/[owner]/[slug]/ModelLayout.tsx | 8 ++-- .../[owner]/[slug]/ModelPrivacyControls.tsx | 4 +- .../[owner]/[slug]/ModelSettingsButton.tsx | 2 +- .../models/[owner]/[slug]/MoveModelAction.tsx | 6 +-- .../[slug]/SquiggleSnippetDraftDialog.tsx | 4 +- .../[owner]/[slug]/UpdateModelSlugAction.tsx | 6 +-- .../src/app/models/[owner]/[slug]/layout.tsx | 4 +- .../src/app/models/[owner]/[slug]/page.tsx | 2 +- .../BuildRelativeValuesCacheAction.tsx | 4 +- .../ClearRelativeValuesCacheAction.tsx | 4 +- .../[variableName]/CacheMenu/index.tsx | 4 +- .../RelativeValuesModelLayout.tsx | 4 +- .../relative-values/[variableName]/Tabs.tsx | 4 +- .../relative-values/[variableName]/layout.tsx | 10 ++-- .../[slug]/revisions/ModelRevisionsList.tsx | 12 ++--- .../[revisionId]/ModelRevisionView.tsx | 2 +- .../[slug]/revisions/[revisionId]/page.tsx | 6 +-- .../models/[owner]/[slug]/revisions/page.tsx | 4 +- .../[owner]/[slug]/useFixModelUrlCasing.ts | 4 +- .../[variableName]/VariableRevisionsPanel.tsx | 8 ++-- .../variables/[variableName]/layout.tsx | 2 +- .../[slug]/variables/[variableName]/page.tsx | 4 +- .../[revisionId]/VariableRevisionPage.tsx | 2 +- .../revisions/[revisionId]/page.tsx | 2 +- .../[slug]/view/ViewSquiggleSnippet.tsx | 2 +- .../app/models/[owner]/[slug]/view/page.tsx | 2 +- .../src/app/new/definition/NewDefinition.tsx | 4 +- packages/hub/src/app/new/group/NewGroup.tsx | 6 +-- packages/hub/src/app/new/model/NewModel.tsx | 6 +-- packages/hub/src/app/new/model/page.tsx | 4 +- .../[owner]/[slug]/DefinitionLayout.tsx | 4 +- .../DeleteRelativeValuesDefinitionAction.tsx | 2 +- .../[slug]/RelativeValuesDefinitionPage.tsx | 6 +-- .../edit/EditRelativeValuesDefinition.tsx | 6 +-- .../[owner]/[slug]/edit/page.tsx | 6 +-- .../relative-values/[owner]/[slug]/layout.tsx | 4 +- .../relative-values/[owner]/[slug]/page.tsx | 4 +- .../choose-username/ChooseUsername.tsx | 2 +- .../src/app/settings/choose-username/page.tsx | 2 +- packages/hub/src/app/status/page.tsx | 2 +- .../app/users/[username]/NewModelButton.tsx | 2 +- .../app/users/[username]/definitions/page.tsx | 2 +- .../src/app/users/[username]/groups/page.tsx | 2 +- .../hub/src/app/users/[username]/layout.tsx | 6 +-- .../hub/src/app/users/[username]/page.tsx | 2 +- .../app/users/[username]/variables/page.tsx | 2 +- packages/hub/src/components/GroupLink.tsx | 2 +- .../src/components/LoadMoreViaSearchParam.tsx | 2 +- packages/hub/src/components/UsernameLink.tsx | 2 +- .../hub/src/components/WithAuth/index.tsx | 6 +-- .../exports/EditRelativeValueExports.tsx | 6 +-- .../SelectRelativeValuesDefinition.tsx | 2 +- .../layout/RootLayout/MyGroupsMenu.tsx | 6 +-- .../layout/RootLayout/PageFooter.tsx | 8 +++- .../RootLayout/PageFooterIfNecessary.tsx | 2 +- .../components/layout/RootLayout/PageMenu.tsx | 8 ++-- .../layout/RootLayout/UserControlsMenu.tsx | 2 +- .../components/layout/RootLayout/index.tsx | 4 +- .../RootLayout/useForceChooseUsername.ts | 2 +- .../components/ui/ServerActionModalAction.tsx | 2 +- packages/hub/src/constants.ts | 10 ---- .../acceptReusableGroupInviteTokenAction.ts | 11 +++-- .../groups/actions/addUserToGroupAction.ts | 7 +-- .../groups/actions/createGroupAction.ts | 10 ++-- .../createReusableGroupInviteTokenAction.ts | 14 ++++-- .../groups/actions/deleteMembershipAction.ts | 11 +++-- .../deleteReusableGroupInviteTokenAction.ts | 10 ++-- .../actions/updateMembershipRoleAction.ts | 7 +-- .../hub/src/groups/components/GroupCard.tsx | 4 +- .../hub/src/groups/components/GroupList.tsx | 6 +-- .../card.ts => groups/data/groupCards.ts} | 7 ++- .../src/{server => }/groups/data/helpers.ts | 8 ++-- .../src/{server => }/groups/data/members.ts | 6 +-- .../groupHelpers.ts => groups/helpers.ts} | 4 +- .../hub/src/lib/{common.ts => constants.ts} | 11 +++++ packages/hub/src/lib/graphqlHelpers.ts | 48 ------------------- .../src/{ => lib}/hooks/useAvailableHeight.ts | 0 .../{ => lib}/hooks/useClientOnlyRender.ts | 0 .../hub/src/{ => lib}/hooks/usePaginator.ts | 2 +- .../{ => lib}/hooks/useServerActionForm.ts | 0 .../{ => lib}/hooks/useUpdateSearchParams.ts | 0 packages/hub/src/{ => lib}/routes.ts | 0 packages/hub/src/{ => lib/server}/auth.ts | 4 +- .../src/{ => lib}/server/globalStatistics.ts | 2 +- packages/hub/src/{ => lib/server}/prisma.ts | 0 .../hub/src/{ => lib}/server/runSquiggle.ts | 4 +- packages/hub/src/{ => lib}/server/utils.ts | 8 ---- packages/hub/src/{server => lib}/types.ts | 0 packages/hub/src/lib/zodUtils.ts | 8 ++++ .../20241012155427_workflow_format.ts | 2 +- .../actions/adminUpdateModelVersionAction.ts | 10 ++-- .../createSquiggleSnippetModelAction.ts | 11 +++-- .../models/actions/deleteModelAction.ts | 9 ++-- .../models/actions/loadModelCardAction.ts | 3 +- .../models/actions/loadModelFullAction.ts | 3 +- .../models/actions/moveModelAction.ts | 11 +++-- .../actions/updateModelPrivacyAction.ts | 11 +++-- .../models/actions/updateModelSlugAction.ts | 9 ++-- .../updateSquiggleSnippetModelAction.ts | 11 +++-- .../models/utils.ts => models/clientUtils.ts} | 0 .../hub/src/models/components/ModelCard.tsx | 6 +-- .../hub/src/models/components/ModelList.tsx | 6 +-- .../{server => }/models/data/authHelpers.ts | 2 +- .../src/{server => }/models/data/byVersion.ts | 4 +- .../hub/src/{server => }/models/data/cards.ts | 9 ++-- .../hub/src/{server => }/models/data/full.ts | 4 +- .../{server => }/models/data/fullRevision.ts | 2 +- .../src/{server => }/models/data/helpers.ts | 2 +- .../src/{server => }/models/data/revisions.ts | 4 +- .../modelHelpers.ts => models/utils.ts} | 2 +- .../{server/owners => owners/data}/auth.ts | 4 +- .../{server => }/owners/data/findOwners.ts | 4 +- .../{server => }/owners/data/typedOwner.ts | 2 +- .../actions/buildRelativeValuesCacheAction.ts | 8 ++-- .../actions/clearRelativeValuesCacheAction.ts | 11 ++--- .../relative-values/actions/common.ts | 2 +- .../createRelativeValuesDefinitionAction.ts | 10 ++-- .../deleteRelativeValuesDefinitionAction.tsx | 9 ++-- .../updateRelativeValuesDefinitionAction.ts | 8 ++-- .../RelativeValuesDefinitionCard.tsx | 4 +- .../RelativeValuesDefinitionList.tsx | 6 +-- .../RelativeValuesDefinitionRevision.tsx | 2 +- .../views/RelativeValuesProvider.tsx | 2 +- .../relative-values/data/cards.ts | 7 ++- .../relative-values/data/exports.ts | 4 +- .../data/findRelativeValuesForSelect.ts | 2 +- .../{server => }/relative-values/data/full.ts | 2 +- .../relative-values/data/fullExport.ts | 2 +- .../src/{server => }/relative-values/utils.ts | 4 +- .../relative-values/values/ModelEvaluator.ts | 2 +- .../createVariableRevision.ts | 2 +- .../buildRecentModelRevision/worker.ts | 4 +- .../actions/adminRebuildSearchIndexAction.ts | 4 +- .../hub/src/{server => }/search/helpers.ts | 2 +- .../src/squiggle/components/ImportTooltip.tsx | 4 +- .../hub/src/{server => }/users/actions.ts | 4 +- packages/hub/src/{server => }/users/auth.ts | 5 +- .../src/{server => }/users/data/layoutUser.ts | 4 +- .../src/variables/components/VariableCard.tsx | 4 +- .../src/variables/components/VariableList.tsx | 6 +-- .../components}/VariablesDropdown.tsx | 4 +- .../variables/data/fullVariableRevision.ts | 6 +-- .../variables/data/variableCards.ts | 7 ++- .../variables/data/variableRevisions.ts | 8 ++-- packages/hub/test/setup-db.ts | 2 +- 195 files changed, 433 insertions(+), 487 deletions(-) rename packages/hub/src/{server/ai/analytics/index.ts => ai/data/analytics.ts} (94%) rename packages/hub/src/{server/ai/data.ts => ai/data/loadWorkflows.ts} (85%) rename packages/hub/src/{server/ai => ai/data}/storage.ts (88%) rename packages/hub/src/{server/ai => ai/data}/utils.ts (100%) rename packages/hub/src/{server/ai => ai/data}/v1_0.ts (100%) rename packages/hub/src/{server/ai => ai/data}/v2_0.ts (100%) delete mode 100644 packages/hub/src/constants.ts rename packages/hub/src/{server => }/groups/actions/acceptReusableGroupInviteTokenAction.ts (80%) rename packages/hub/src/{server => }/groups/actions/addUserToGroupAction.ts (92%) rename packages/hub/src/{server => }/groups/actions/createGroupAction.ts (76%) rename packages/hub/src/{server => }/groups/actions/createReusableGroupInviteTokenAction.ts (71%) rename packages/hub/src/{server => }/groups/actions/deleteMembershipAction.ts (82%) rename packages/hub/src/{server => }/groups/actions/deleteReusableGroupInviteTokenAction.ts (70%) rename packages/hub/src/{server => }/groups/actions/updateMembershipRoleAction.ts (90%) rename packages/hub/src/{server/groups/data/card.ts => groups/data/groupCards.ts} (94%) rename packages/hub/src/{server => }/groups/data/helpers.ts (90%) rename packages/hub/src/{server => }/groups/data/members.ts (95%) rename packages/hub/src/{server/groups/groupHelpers.ts => groups/helpers.ts} (90%) rename packages/hub/src/lib/{common.ts => constants.ts} (54%) delete mode 100644 packages/hub/src/lib/graphqlHelpers.ts rename packages/hub/src/{ => lib}/hooks/useAvailableHeight.ts (100%) rename packages/hub/src/{ => lib}/hooks/useClientOnlyRender.ts (100%) rename packages/hub/src/{ => lib}/hooks/usePaginator.ts (97%) rename packages/hub/src/{ => lib}/hooks/useServerActionForm.ts (100%) rename packages/hub/src/{ => lib}/hooks/useUpdateSearchParams.ts (100%) rename packages/hub/src/{ => lib}/routes.ts (100%) rename packages/hub/src/{ => lib/server}/auth.ts (95%) rename packages/hub/src/{ => lib}/server/globalStatistics.ts (88%) rename packages/hub/src/{ => lib/server}/prisma.ts (100%) rename packages/hub/src/{ => lib}/server/runSquiggle.ts (98%) rename packages/hub/src/{ => lib}/server/utils.ts (86%) rename packages/hub/src/{server => lib}/types.ts (100%) rename packages/hub/src/{server => }/models/actions/adminUpdateModelVersionAction.ts (92%) rename packages/hub/src/{server => }/models/actions/createSquiggleSnippetModelAction.ts (86%) rename packages/hub/src/{server => }/models/actions/deleteModelAction.ts (62%) rename packages/hub/src/{server => }/models/actions/loadModelCardAction.ts (82%) rename packages/hub/src/{server => }/models/actions/loadModelFullAction.ts (83%) rename packages/hub/src/{server => }/models/actions/moveModelAction.ts (68%) rename packages/hub/src/{server => }/models/actions/updateModelPrivacyAction.ts (71%) rename packages/hub/src/{server => }/models/actions/updateModelSlugAction.ts (70%) rename packages/hub/src/{server => }/models/actions/updateSquiggleSnippetModelAction.ts (93%) rename packages/hub/src/{server/models/utils.ts => models/clientUtils.ts} (100%) rename packages/hub/src/{server => }/models/data/authHelpers.ts (93%) rename packages/hub/src/{server => }/models/data/byVersion.ts (93%) rename packages/hub/src/{server => }/models/data/cards.ts (94%) rename packages/hub/src/{server => }/models/data/full.ts (95%) rename packages/hub/src/{server => }/models/data/fullRevision.ts (97%) rename packages/hub/src/{server => }/models/data/helpers.ts (74%) rename packages/hub/src/{server => }/models/data/revisions.ts (97%) rename packages/hub/src/{graphql/helpers/modelHelpers.ts => models/utils.ts} (95%) rename packages/hub/src/{server/owners => owners/data}/auth.ts (95%) rename packages/hub/src/{server => }/owners/data/findOwners.ts (96%) rename packages/hub/src/{server => }/owners/data/typedOwner.ts (93%) rename packages/hub/src/{server => }/relative-values/actions/buildRelativeValuesCacheAction.ts (93%) rename packages/hub/src/{server => }/relative-values/actions/clearRelativeValuesCacheAction.ts (80%) rename packages/hub/src/{server => }/relative-values/actions/common.ts (96%) rename packages/hub/src/{server => }/relative-values/actions/createRelativeValuesDefinitionAction.ts (88%) rename packages/hub/src/{server => }/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx (65%) rename packages/hub/src/{server => }/relative-values/actions/updateRelativeValuesDefinitionAction.ts (89%) rename packages/hub/src/{server => }/relative-values/data/cards.ts (95%) rename packages/hub/src/{server => }/relative-values/data/exports.ts (93%) rename packages/hub/src/{server => }/relative-values/data/findRelativeValuesForSelect.ts (92%) rename packages/hub/src/{server => }/relative-values/data/full.ts (98%) rename packages/hub/src/{server => }/relative-values/data/fullExport.ts (97%) rename packages/hub/src/{server => }/relative-values/utils.ts (91%) rename packages/hub/src/{server => }/search/actions/adminRebuildSearchIndexAction.ts (75%) rename packages/hub/src/{server => }/search/helpers.ts (96%) rename packages/hub/src/{server => }/users/actions.ts (91%) rename packages/hub/src/{server => }/users/auth.ts (94%) rename packages/hub/src/{server => }/users/data/layoutUser.ts (94%) rename packages/hub/src/{lib => variables/components}/VariablesDropdown.tsx (97%) rename packages/hub/src/{server => }/variables/data/fullVariableRevision.ts (90%) rename packages/hub/src/{server => }/variables/data/variableCards.ts (93%) rename packages/hub/src/{server => }/variables/data/variableRevisions.ts (90%) diff --git a/packages/hub/.prettierignore b/packages/hub/.prettierignore index a4821cf008..f72a0b101a 100644 --- a/packages/hub/.prettierignore +++ b/packages/hub/.prettierignore @@ -1,5 +1,2 @@ .vscode .next -src/__generated__ -schema.graphql -test/gql-gen diff --git a/packages/hub/README.md b/packages/hub/README.md index 48ec19148b..44284e2c08 100644 --- a/packages/hub/README.md +++ b/packages/hub/README.md @@ -38,16 +38,7 @@ The basic loop is: ## Notes on changing the schema -`pnpm run gen` is a pipeline with three steps: - -1. First, `gen:prisma` generates `@prisma/client` from `prisma/schema.prisma`. -2. Then `gen:schema` generates `schema.graphql` from our GraphQL server code in `src/graphql/`. -3. Finally, `gen:relay` runs [relay-compiler](https://relay.dev/docs/guides/compiler/), which generates `src/__generated__` files based on `schema.graphql`. - -So: - -- for the database schema, `prisma/schema.prisma` is the source of truth -- for the GraphQL schema, the TypeScript code in `src/graphql/` is the source of truth, while `schema.graphql` is auto-generated (but it still should be committed to the repo) +`pnpm run gen` generates `@prisma/client` from `prisma/schema.prisma`. If it looks like VS Code doesn't see your latest changes, try this: @@ -56,14 +47,6 @@ If it looks like VS Code doesn't see your latest changes, try this: Note: the "Restart TS Server" step is necessary because `@prisma/client` code is out of the main source tree, and VS Code won't notice that it has updated. But restarting TS Server is slow, so a better solution is to keep `@prisma/client` source code open (open `src/prisma.ts`, then "Go to definition" on `PrismaClient`). Then VS Code will watch it for changes. -For Relay-generated files under `src/__generated__`, VS Code usually detects the changes automatically. - -Another note is that with the correct setup, out of `pnpm gen:prisma`, `pnpm gen:schema` and `pnpm gen:relay` pipeline steps, only `gen:schema` is necessary: - -- `@prisma/client` will be regenerated on `prisma db push` or `prisma migrate dev` -- `gen:schema`, which calls the `src/graphql/print-schema.ts` script, doesn't have the watch mode, so it _is_ necessary to call it after you edit any `src/graphql/` code -- `gen:relay` (`relay-compiler`) will run in watch mode if you use [Relay GraphQL extension](https://marketplace.visualstudio.com/items?itemName=meta.relay) and enable `relay.autoStartCompiler` option in VS Code settings - ## Other notes [Common workflow for updating Prisma schema](https://www.prisma.io/docs/orm/prisma-migrate/workflows/prototyping-your-schema) diff --git a/packages/hub/src/server/ai/analytics/index.ts b/packages/hub/src/ai/data/analytics.ts similarity index 94% rename from packages/hub/src/server/ai/analytics/index.ts rename to packages/hub/src/ai/data/analytics.ts index 279e1e7b43..823365e2b1 100644 --- a/packages/hub/src/server/ai/analytics/index.ts +++ b/packages/hub/src/ai/data/analytics.ts @@ -2,10 +2,10 @@ import * as Prisma from "@prisma/client"; import { CodeArtifact, Workflow } from "@quri/squiggle-ai/server"; -import { prisma } from "@/prisma"; -import { getAiCodec } from "@/server/ai/utils"; -import { v2WorkflowDataSchema } from "@/server/ai/v2_0"; -import { checkRootUser } from "@/server/users/auth"; +import { getAiCodec } from "@/ai/data/utils"; +import { v2WorkflowDataSchema } from "@/ai/data/v2_0"; +import { prisma } from "@/lib/server/prisma"; +import { checkRootUser } from "@/users/auth"; async function loadWorkflows() { await checkRootUser(); diff --git a/packages/hub/src/server/ai/data.ts b/packages/hub/src/ai/data/loadWorkflows.ts similarity index 85% rename from packages/hub/src/server/ai/data.ts rename to packages/hub/src/ai/data/loadWorkflows.ts index 21b227ee50..c5e7c20232 100644 --- a/packages/hub/src/server/ai/data.ts +++ b/packages/hub/src/ai/data/loadWorkflows.ts @@ -1,8 +1,8 @@ import "server-only"; -import { prisma } from "@/prisma"; +import { prisma } from "@/lib/server/prisma"; +import { getSessionUserOrRedirect } from "@/users/auth"; -import { getSessionUserOrRedirect } from "../users/auth"; import { decodeDbWorkflowToClientWorkflow } from "./storage"; export async function loadWorkflows({ diff --git a/packages/hub/src/server/ai/storage.ts b/packages/hub/src/ai/data/storage.ts similarity index 88% rename from packages/hub/src/server/ai/storage.ts rename to packages/hub/src/ai/data/storage.ts index b85050db48..8172ea9e1e 100644 --- a/packages/hub/src/server/ai/storage.ts +++ b/packages/hub/src/ai/data/storage.ts @@ -4,8 +4,8 @@ import { AiWorkflow as PrismaAiWorkflow } from "@prisma/client"; import { ClientWorkflow } from "@quri/squiggle-ai"; -import { decodeV1_0JsonToClientWorkflow } from "@/server/ai/v1_0"; -import { decodeV2_0JsonToClientWorkflow } from "@/server/ai/v2_0"; +import { decodeV1_0JsonToClientWorkflow } from "@/ai/data/v1_0"; +import { decodeV2_0JsonToClientWorkflow } from "@/ai/data/v2_0"; export function decodeDbWorkflowToClientWorkflow( row: PrismaAiWorkflow diff --git a/packages/hub/src/server/ai/utils.ts b/packages/hub/src/ai/data/utils.ts similarity index 100% rename from packages/hub/src/server/ai/utils.ts rename to packages/hub/src/ai/data/utils.ts diff --git a/packages/hub/src/server/ai/v1_0.ts b/packages/hub/src/ai/data/v1_0.ts similarity index 100% rename from packages/hub/src/server/ai/v1_0.ts rename to packages/hub/src/ai/data/v1_0.ts diff --git a/packages/hub/src/server/ai/v2_0.ts b/packages/hub/src/ai/data/v2_0.ts similarity index 100% rename from packages/hub/src/server/ai/v2_0.ts rename to packages/hub/src/ai/data/v2_0.ts diff --git a/packages/hub/src/app/(frontpage)/definitions/page.tsx b/packages/hub/src/app/(frontpage)/definitions/page.tsx index d0cac5566e..a320040352 100644 --- a/packages/hub/src/app/(frontpage)/definitions/page.tsx +++ b/packages/hub/src/app/(frontpage)/definitions/page.tsx @@ -1,5 +1,5 @@ import { RelativeValuesDefinitionList } from "@/relative-values/components/RelativeValuesDefinitionList"; -import { loadDefinitionCards } from "@/server/relative-values/data/cards"; +import { loadDefinitionCards } from "@/relative-values/data/cards"; export default async function DefinitionsPage() { const page = await loadDefinitionCards(); diff --git a/packages/hub/src/app/(frontpage)/groups/page.tsx b/packages/hub/src/app/(frontpage)/groups/page.tsx index d9a0b62464..3aa2e4aa7d 100644 --- a/packages/hub/src/app/(frontpage)/groups/page.tsx +++ b/packages/hub/src/app/(frontpage)/groups/page.tsx @@ -1,5 +1,5 @@ import { GroupList } from "@/groups/components/GroupList"; -import { loadGroupCards } from "@/server/groups/data/card"; +import { loadGroupCards } from "@/groups/data/groupCards"; export default async function OuterGroupsPage() { const page = await loadGroupCards(); diff --git a/packages/hub/src/app/(frontpage)/layout.tsx b/packages/hub/src/app/(frontpage)/layout.tsx index 2a12752081..0a8265b387 100644 --- a/packages/hub/src/app/(frontpage)/layout.tsx +++ b/packages/hub/src/app/(frontpage)/layout.tsx @@ -5,7 +5,7 @@ import { StyledTabLink, StyledTabLinkList, } from "@/components/ui/StyledTabLink"; -import { definitionsRoute, groupsRoute, variablesRoute } from "@/routes"; +import { definitionsRoute, groupsRoute, variablesRoute } from "@/lib/routes"; export default function FrontPageLayout({ children }: PropsWithChildren) { return ( diff --git a/packages/hub/src/app/(frontpage)/page.tsx b/packages/hub/src/app/(frontpage)/page.tsx index fd896f9923..b5f84bf8a4 100644 --- a/packages/hub/src/app/(frontpage)/page.tsx +++ b/packages/hub/src/app/(frontpage)/page.tsx @@ -1,5 +1,5 @@ import { ModelList } from "@/models/components/ModelList"; -import { loadModelCards } from "@/server/models/data/cards"; +import { loadModelCards } from "@/models/data/cards"; export default async function FrontPage() { const page = await loadModelCards(); diff --git a/packages/hub/src/app/(frontpage)/variables/page.tsx b/packages/hub/src/app/(frontpage)/variables/page.tsx index bde5166dc3..2f5b99954d 100644 --- a/packages/hub/src/app/(frontpage)/variables/page.tsx +++ b/packages/hub/src/app/(frontpage)/variables/page.tsx @@ -1,5 +1,5 @@ -import { loadVariableCards } from "@/server/variables/data/variableCards"; import { VariableList } from "@/variables/components/VariableList"; +import { loadVariableCards } from "@/variables/data/variableCards"; export default async function OuterVariablesPage() { const variables = await loadVariableCards(); diff --git a/packages/hub/src/app/about/page.tsx b/packages/hub/src/app/about/page.tsx index 977008f566..5932e0a4ab 100644 --- a/packages/hub/src/app/about/page.tsx +++ b/packages/hub/src/app/about/page.tsx @@ -9,7 +9,7 @@ import { GITHUB_URL, NEWSLETTER_URL, QURI_DONATE_URL, -} from "@/lib/common"; +} from "@/lib/constants"; const markdown = ` # About Squiggle Hub diff --git a/packages/hub/src/app/admin/dev/page.tsx b/packages/hub/src/app/admin/dev/page.tsx index fb07951d34..920a19cf1b 100644 --- a/packages/hub/src/app/admin/dev/page.tsx +++ b/packages/hub/src/app/admin/dev/page.tsx @@ -4,7 +4,7 @@ import { Button } from "@quri/ui"; import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; import { H2 } from "@/components/ui/Headers"; -import { resetPrisma } from "@/prisma"; +import { resetPrisma } from "@/lib/server/prisma"; export default async function () { if (process.env.NODE_ENV !== "development") { diff --git a/packages/hub/src/app/admin/layout.tsx b/packages/hub/src/app/admin/layout.tsx index cb43c102ef..3649116577 100644 --- a/packages/hub/src/app/admin/layout.tsx +++ b/packages/hub/src/app/admin/layout.tsx @@ -2,11 +2,11 @@ import { PropsWithChildren } from "react"; import { LockIcon } from "@quri/ui"; -import { auth } from "@/auth"; import { FullLayoutWithPadding } from "@/components/layout/FullLayoutWithPadding"; import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; import { H1 } from "@/components/ui/Headers"; -import { isRootEmail } from "@/server/users/auth"; +import { auth } from "@/lib/server/auth"; +import { isRootEmail } from "@/users/auth"; export default async function AdminLayout({ children }: PropsWithChildren) { const session = await auth(); diff --git a/packages/hub/src/app/admin/search/page.tsx b/packages/hub/src/app/admin/search/page.tsx index dba268bf74..e850f9bc71 100644 --- a/packages/hub/src/app/admin/search/page.tsx +++ b/packages/hub/src/app/admin/search/page.tsx @@ -1,7 +1,7 @@ import { H2 } from "@/components/ui/Headers"; import { ServerActionButton } from "@/components/ui/ServerActionButton"; -import { adminRebuildSearchIndexAction } from "@/server/search/actions/adminRebuildSearchIndexAction"; -import { checkRootUser } from "@/server/users/auth"; +import { adminRebuildSearchIndexAction } from "@/search/actions/adminRebuildSearchIndexAction"; +import { checkRootUser } from "@/users/auth"; export default async function AdminSearchPage() { await checkRootUser(); diff --git a/packages/hub/src/app/admin/upgrade-versions/UpgradeVersionsPage.tsx b/packages/hub/src/app/admin/upgrade-versions/UpgradeVersionsPage.tsx index 0bc321eec1..a14d7e95a9 100644 --- a/packages/hub/src/app/admin/upgrade-versions/UpgradeVersionsPage.tsx +++ b/packages/hub/src/app/admin/upgrade-versions/UpgradeVersionsPage.tsx @@ -12,9 +12,9 @@ import { defaultSquiggleVersion } from "@quri/versioned-squiggle-components"; import { H2 } from "@/components/ui/Headers"; import { ServerActionButton } from "@/components/ui/ServerActionButton"; import { StyledLink } from "@/components/ui/StyledLink"; -import { modelRoute } from "@/routes"; -import { adminUpdateModelVersionAction } from "@/server/models/actions/adminUpdateModelVersionAction"; -import { ModelByVersion } from "@/server/models/data/byVersion"; +import { modelRoute } from "@/lib/routes"; +import { adminUpdateModelVersionAction } from "@/models/actions/adminUpdateModelVersionAction"; +import { ModelByVersion } from "@/models/data/byVersion"; import { UpgradeableModel } from "./UpgradeableModel"; diff --git a/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx b/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx index 45be5dc150..6ea622c94f 100644 --- a/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx +++ b/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx @@ -10,9 +10,9 @@ import { } from "@quri/versioned-squiggle-components"; import { EditSquiggleSnippetModel } from "@/app/models/[owner]/[slug]/EditSquiggleSnippetModel"; -import { loadModelFullAction } from "@/server/models/actions/loadModelFullAction"; -import { ModelByVersion } from "@/server/models/data/byVersion"; -import { ModelFullDTO } from "@/server/models/data/full"; +import { loadModelFullAction } from "@/models/actions/loadModelFullAction"; +import { ModelByVersion } from "@/models/data/byVersion"; +import { ModelFullDTO } from "@/models/data/full"; import { sqProjectWithHubLinker } from "@/squiggle/components/linker"; const InnerUpgradeableModel: FC<{ diff --git a/packages/hub/src/app/admin/upgrade-versions/page.tsx b/packages/hub/src/app/admin/upgrade-versions/page.tsx index 7b4f182b34..b864a5e625 100644 --- a/packages/hub/src/app/admin/upgrade-versions/page.tsx +++ b/packages/hub/src/app/admin/upgrade-versions/page.tsx @@ -1,7 +1,7 @@ import { Metadata } from "next"; -import { loadModelsByVersion } from "@/server/models/data/byVersion"; -import { checkRootUser } from "@/server/users/auth"; +import { loadModelsByVersion } from "@/models/data/byVersion"; +import { checkRootUser } from "@/users/auth"; import { UpgradeVersionsPage } from "./UpgradeVersionsPage"; diff --git a/packages/hub/src/app/ai/WorkflowViewer/ClientStepView.tsx b/packages/hub/src/app/ai/WorkflowViewer/ClientStepView.tsx index 4988961395..be2400a713 100644 --- a/packages/hub/src/app/ai/WorkflowViewer/ClientStepView.tsx +++ b/packages/hub/src/app/ai/WorkflowViewer/ClientStepView.tsx @@ -4,7 +4,7 @@ import { FC, useMemo } from "react"; import { ClientArtifact, ClientStep } from "@quri/squiggle-ai"; import { ChevronLeftIcon, ChevronRightIcon } from "@quri/ui"; -import { useAvailableHeight } from "@/hooks/useAvailableHeight"; +import { useAvailableHeight } from "@/lib/hooks/useAvailableHeight"; import { SquigglePlaygroundForWorkflow } from "../SquigglePlaygroundForWorkflow"; import { stepNames } from "../utils"; diff --git a/packages/hub/src/app/ai/WorkflowViewer/index.tsx b/packages/hub/src/app/ai/WorkflowViewer/index.tsx index d48ac9949b..2b5e60c035 100644 --- a/packages/hub/src/app/ai/WorkflowViewer/index.tsx +++ b/packages/hub/src/app/ai/WorkflowViewer/index.tsx @@ -4,7 +4,7 @@ import { FC } from "react"; import { ClientWorkflow } from "@quri/squiggle-ai"; import { StyledTab } from "@quri/ui"; -import { useAvailableHeight } from "@/hooks/useAvailableHeight"; +import { useAvailableHeight } from "@/lib/hooks/useAvailableHeight"; import { LogsView } from "../LogsView"; import { SquigglePlaygroundForWorkflow } from "../SquigglePlaygroundForWorkflow"; diff --git a/packages/hub/src/app/ai/analytics/StepErrorList.tsx b/packages/hub/src/app/ai/analytics/StepErrorList.tsx index d59575761f..18f8cc9aee 100644 --- a/packages/hub/src/app/ai/analytics/StepErrorList.tsx +++ b/packages/hub/src/app/ai/analytics/StepErrorList.tsx @@ -1,7 +1,7 @@ import { FC, Fragment } from "react"; +import { type StepError } from "@/ai/data/analytics"; import { H2 } from "@/components/ui/Headers"; -import { type StepError } from "@/server/ai/analytics"; export const StepErrorList: FC<{ errors: StepError[]; diff --git a/packages/hub/src/app/ai/analytics/code-errors/page.tsx b/packages/hub/src/app/ai/analytics/code-errors/page.tsx index 8ffbe112f2..4b89a6ff7e 100644 --- a/packages/hub/src/app/ai/analytics/code-errors/page.tsx +++ b/packages/hub/src/app/ai/analytics/code-errors/page.tsx @@ -1,4 +1,4 @@ -import { getCodeErrors } from "@/server/ai/analytics"; +import { getCodeErrors } from "@/ai/data/analytics"; import { StepErrorList } from "../StepErrorList"; diff --git a/packages/hub/src/app/ai/analytics/page.tsx b/packages/hub/src/app/ai/analytics/page.tsx index cf13a29386..33c25b1534 100644 --- a/packages/hub/src/app/ai/analytics/page.tsx +++ b/packages/hub/src/app/ai/analytics/page.tsx @@ -1,7 +1,7 @@ import { Fragment } from "react"; +import { getTypeStats } from "@/ai/data/analytics"; import { H2 } from "@/components/ui/Headers"; -import { getTypeStats } from "@/server/ai/analytics"; export default async function () { const typeStats = await getTypeStats(); diff --git a/packages/hub/src/app/ai/analytics/step-errors/page.tsx b/packages/hub/src/app/ai/analytics/step-errors/page.tsx index 0ede8f95de..17750fcfdf 100644 --- a/packages/hub/src/app/ai/analytics/step-errors/page.tsx +++ b/packages/hub/src/app/ai/analytics/step-errors/page.tsx @@ -1,4 +1,4 @@ -import { getStepErrors } from "@/server/ai/analytics"; +import { getStepErrors } from "@/ai/data/analytics"; import { StepErrorList } from "../StepErrorList"; diff --git a/packages/hub/src/app/ai/api/create/route.ts b/packages/hub/src/app/ai/api/create/route.ts index 56e2b23257..d5c7a721d3 100644 --- a/packages/hub/src/app/ai/api/create/route.ts +++ b/packages/hub/src/app/ai/api/create/route.ts @@ -7,10 +7,10 @@ import { Workflow, } from "@quri/squiggle-ai/server"; -import { auth } from "@/auth"; -import { prisma } from "@/prisma"; -import { workflowToV2_0Json } from "@/server/ai/v2_0"; -import { getSelf, isSignedIn } from "@/server/users/auth"; +import { workflowToV2_0Json } from "@/ai/data/v2_0"; +import { auth } from "@/lib/server/auth"; +import { prisma } from "@/lib/server/prisma"; +import { getSelf, isSignedIn } from "@/users/auth"; import { AiRequestBody, aiRequestBodySchema } from "../../utils"; diff --git a/packages/hub/src/app/ai/page.tsx b/packages/hub/src/app/ai/page.tsx index 2f7ea6331a..b64761300e 100644 --- a/packages/hub/src/app/ai/page.tsx +++ b/packages/hub/src/app/ai/page.tsx @@ -1,7 +1,7 @@ import { z } from "zod"; +import { loadWorkflows } from "@/ai/data/loadWorkflows"; import { numberInString } from "@/lib/zodUtils"; -import { loadWorkflows } from "@/server/ai/data"; import { AiDashboard } from "./AiDashboard"; diff --git a/packages/hub/src/app/api/auth/[...nextauth]/route.ts b/packages/hub/src/app/api/auth/[...nextauth]/route.ts index a49631a69f..dffc891aed 100644 --- a/packages/hub/src/app/api/auth/[...nextauth]/route.ts +++ b/packages/hub/src/app/api/auth/[...nextauth]/route.ts @@ -1,3 +1,3 @@ -import { handlers } from "../../../../auth"; +import { handlers } from "../../../../lib/server/auth"; export const { GET, POST } = handlers; diff --git a/packages/hub/src/app/api/find-owners/route.ts b/packages/hub/src/app/api/find-owners/route.ts index 81a7656cc7..f720cf4a16 100644 --- a/packages/hub/src/app/api/find-owners/route.ts +++ b/packages/hub/src/app/api/find-owners/route.ts @@ -1,7 +1,7 @@ import { NextRequest } from "next/server"; import { z } from "zod"; -import { findOwnersForSelect } from "@/server/owners/data/findOwners"; +import { findOwnersForSelect } from "@/owners/data/findOwners"; // We're not calling this as a server actions because it'd be too slow (server actions are sequential). // TODO: it'd be good to use tRPC for this. diff --git a/packages/hub/src/app/api/find-relative-values/route.ts b/packages/hub/src/app/api/find-relative-values/route.ts index 496abc56ad..bbd40513b1 100644 --- a/packages/hub/src/app/api/find-relative-values/route.ts +++ b/packages/hub/src/app/api/find-relative-values/route.ts @@ -1,7 +1,7 @@ import { NextRequest } from "next/server"; import { z } from "zod"; -import { findRelativeValuesForSelect } from "@/server/relative-values/data/findRelativeValuesForSelect"; +import { findRelativeValuesForSelect } from "@/relative-values/data/findRelativeValuesForSelect"; // We're not calling this as a server actions because it'd be too slow (server actions are sequential). // TODO: it'd be good to use tRPC for this. diff --git a/packages/hub/src/app/api/get-source/route.ts b/packages/hub/src/app/api/get-source/route.ts index 1b12d62917..efaaf427e7 100644 --- a/packages/hub/src/app/api/get-source/route.ts +++ b/packages/hub/src/app/api/get-source/route.ts @@ -1,8 +1,8 @@ import { NextRequest } from "next/server"; import { z } from "zod"; -import { loadModelCard } from "@/server/models/data/cards"; -import { zSlug } from "@/server/utils"; +import { zSlug } from "@/lib/zodUtils"; +import { loadModelCard } from "@/models/data/cards"; export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); diff --git a/packages/hub/src/app/api/runSquiggle/route.ts b/packages/hub/src/app/api/runSquiggle/route.ts index 051a51d403..538a3f2588 100644 --- a/packages/hub/src/app/api/runSquiggle/route.ts +++ b/packages/hub/src/app/api/runSquiggle/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; -import { runSquiggleWithCache } from "@/server/runSquiggle"; +import { runSquiggleWithCache } from "@/lib/server/runSquiggle"; export async function POST(req: NextRequest) { // Assuming 'code' is sent in the request body and is a string diff --git a/packages/hub/src/app/api/search/route.ts b/packages/hub/src/app/api/search/route.ts index ec90e5d197..8d3884086b 100644 --- a/packages/hub/src/app/api/search/route.ts +++ b/packages/hub/src/app/api/search/route.ts @@ -2,14 +2,14 @@ import { Searchable as PrismaSearchable } from "@prisma/client"; import { NextRequest } from "next/server"; import { z } from "zod"; -import { auth } from "@/auth"; -import { prisma } from "@/prisma"; import { groupRoute, modelRoute, relativeValuesRoute, userRoute, -} from "@/routes"; +} from "@/lib/routes"; +import { auth } from "@/lib/server/auth"; +import { prisma } from "@/lib/server/prisma"; import { SearchResult } from "./schema"; diff --git a/packages/hub/src/app/groups/[slug]/NewModelButton.tsx b/packages/hub/src/app/groups/[slug]/NewModelButton.tsx index a5910303b0..e22e36f5fe 100644 --- a/packages/hub/src/app/groups/[slug]/NewModelButton.tsx +++ b/packages/hub/src/app/groups/[slug]/NewModelButton.tsx @@ -4,7 +4,7 @@ import { FC } from "react"; import { Button, PlusIcon } from "@quri/ui"; -import { newModelRoute } from "@/routes"; +import { newModelRoute } from "@/lib/routes"; // TODO - this could be a server component, if we had `` component export const NewModelButton: FC<{ group: string }> = ({ group }) => { diff --git a/packages/hub/src/app/groups/[slug]/invite-link/AcceptGroupInvitePage.tsx b/packages/hub/src/app/groups/[slug]/invite-link/AcceptGroupInvitePage.tsx index 88db42617b..ac2656438e 100644 --- a/packages/hub/src/app/groups/[slug]/invite-link/AcceptGroupInvitePage.tsx +++ b/packages/hub/src/app/groups/[slug]/invite-link/AcceptGroupInvitePage.tsx @@ -5,9 +5,9 @@ import { FC } from "react"; import { useToast } from "@quri/ui"; import { ServerActionButton } from "@/components/ui/ServerActionButton"; -import { groupRoute } from "@/routes"; -import { acceptReusableGroupInviteTokenAction } from "@/server/groups/actions/acceptReusableGroupInviteTokenAction"; -import { GroupCardDTO } from "@/server/groups/data/card"; +import { acceptReusableGroupInviteTokenAction } from "@/groups/actions/acceptReusableGroupInviteTokenAction"; +import { GroupCardDTO } from "@/groups/data/groupCards"; +import { groupRoute } from "@/lib/routes"; export const AcceptGroupInvitePage: FC<{ group: GroupCardDTO; diff --git a/packages/hub/src/app/groups/[slug]/invite-link/page.tsx b/packages/hub/src/app/groups/[slug]/invite-link/page.tsx index 26f56aca00..52992504c0 100644 --- a/packages/hub/src/app/groups/[slug]/invite-link/page.tsx +++ b/packages/hub/src/app/groups/[slug]/invite-link/page.tsx @@ -2,12 +2,12 @@ import { notFound, redirect } from "next/navigation"; import { z } from "zod"; import { WithAuth } from "@/components/WithAuth"; -import { groupRoute } from "@/routes"; -import { loadGroupCard } from "@/server/groups/data/card"; +import { loadGroupCard } from "@/groups/data/groupCards"; import { hasGroupMembership, validateReusableGroupInviteToken, -} from "@/server/groups/data/helpers"; +} from "@/groups/data/helpers"; +import { groupRoute } from "@/lib/routes"; import { AcceptGroupInvitePage } from "./AcceptGroupInvitePage"; diff --git a/packages/hub/src/app/groups/[slug]/layout.tsx b/packages/hub/src/app/groups/[slug]/layout.tsx index 63100a3eef..1d01d465f1 100644 --- a/packages/hub/src/app/groups/[slug]/layout.tsx +++ b/packages/hub/src/app/groups/[slug]/layout.tsx @@ -10,9 +10,9 @@ import { StyledTabLink, StyledTabLinkList, } from "@/components/ui/StyledTabLink"; -import { groupMembersRoute, groupRoute } from "@/routes"; -import { loadGroupCard } from "@/server/groups/data/card"; -import { hasGroupMembership } from "@/server/groups/data/helpers"; +import { loadGroupCard } from "@/groups/data/groupCards"; +import { hasGroupMembership } from "@/groups/data/helpers"; +import { groupMembersRoute, groupRoute } from "@/lib/routes"; import { NewModelButton } from "./NewModelButton"; diff --git a/packages/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx b/packages/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx index 7c936a9ae7..f4975f4999 100644 --- a/packages/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx +++ b/packages/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx @@ -5,8 +5,8 @@ import { PlusIcon, SelectStringFormField } from "@quri/ui"; import { SelectUser, SelectUserOption } from "@/components/SelectUser"; import { ServerActionModalAction } from "@/components/ui/ServerActionModalAction"; -import { addUserToGroupAction } from "@/server/groups/actions/addUserToGroupAction"; -import { GroupMemberDTO } from "@/server/groups/data/members"; +import { addUserToGroupAction } from "@/groups/actions/addUserToGroupAction"; +import { GroupMemberDTO } from "@/groups/data/members"; type Props = { groupSlug: string; diff --git a/packages/hub/src/app/groups/[slug]/members/DeleteMembershipAction.tsx b/packages/hub/src/app/groups/[slug]/members/DeleteMembershipAction.tsx index f7b4579397..4c5bcb7b31 100644 --- a/packages/hub/src/app/groups/[slug]/members/DeleteMembershipAction.tsx +++ b/packages/hub/src/app/groups/[slug]/members/DeleteMembershipAction.tsx @@ -3,8 +3,8 @@ import { FC } from "react"; import { TrashIcon } from "@quri/ui"; import { ServerActionDropdownAction } from "@/components/ui/ServerActionDropdownAction"; -import { deleteMembershipAction } from "@/server/groups/actions/deleteMembershipAction"; -import { GroupMemberDTO } from "@/server/groups/data/members"; +import { deleteMembershipAction } from "@/groups/actions/deleteMembershipAction"; +import { GroupMemberDTO } from "@/groups/data/members"; type Props = { groupSlug: string; diff --git a/packages/hub/src/app/groups/[slug]/members/GroupMemberCard.tsx b/packages/hub/src/app/groups/[slug]/members/GroupMemberCard.tsx index 7b0e4e80e0..6087034438 100644 --- a/packages/hub/src/app/groups/[slug]/members/GroupMemberCard.tsx +++ b/packages/hub/src/app/groups/[slug]/members/GroupMemberCard.tsx @@ -5,8 +5,8 @@ import { DropdownMenu } from "@quri/ui"; import { Card } from "@/components/ui/Card"; import { DotsDropdown } from "@/components/ui/DotsDropdown"; import { StyledLink } from "@/components/ui/StyledLink"; -import { userRoute } from "@/routes"; -import { GroupMemberDTO } from "@/server/groups/data/members"; +import { GroupMemberDTO } from "@/groups/data/members"; +import { userRoute } from "@/lib/routes"; import { DeleteMembershipAction } from "./DeleteMembershipAction"; import { MembershipRoleButton } from "./MembershipRoleButton"; diff --git a/packages/hub/src/app/groups/[slug]/members/GroupMemberList.tsx b/packages/hub/src/app/groups/[slug]/members/GroupMemberList.tsx index ccd052ced7..83df18c47c 100644 --- a/packages/hub/src/app/groups/[slug]/members/GroupMemberList.tsx +++ b/packages/hub/src/app/groups/[slug]/members/GroupMemberList.tsx @@ -6,9 +6,9 @@ import { DropdownMenu } from "@quri/ui"; import { LoadMore } from "@/components/LoadMore"; import { DotsDropdown } from "@/components/ui/DotsDropdown"; import { H2 } from "@/components/ui/Headers"; -import { usePaginator } from "@/hooks/usePaginator"; -import { GroupMemberDTO } from "@/server/groups/data/members"; -import { Paginated } from "@/server/types"; +import { GroupMemberDTO } from "@/groups/data/members"; +import { usePaginator } from "@/lib/hooks/usePaginator"; +import { Paginated } from "@/lib/types"; import { AddUserToGroupAction } from "./AddUserToGroupAction"; import { GroupMemberCard } from "./GroupMemberCard"; diff --git a/packages/hub/src/app/groups/[slug]/members/GroupReusableInviteSection.tsx b/packages/hub/src/app/groups/[slug]/members/GroupReusableInviteSection.tsx index 0d73d10677..b9001e88f8 100644 --- a/packages/hub/src/app/groups/[slug]/members/GroupReusableInviteSection.tsx +++ b/packages/hub/src/app/groups/[slug]/members/GroupReusableInviteSection.tsx @@ -6,9 +6,9 @@ import { ClipboardCopyIcon, TextTooltip, useToast } from "@quri/ui"; import { H2 } from "@/components/ui/Headers"; import { ServerActionButton } from "@/components/ui/ServerActionButton"; -import { groupInviteLink } from "@/routes"; -import { createReusableGroupInviteTokenAction } from "@/server/groups/actions/createReusableGroupInviteTokenAction"; -import { deleteReusableGroupInviteTokenAction } from "@/server/groups/actions/deleteReusableGroupInviteTokenAction"; +import { createReusableGroupInviteTokenAction } from "@/groups/actions/createReusableGroupInviteTokenAction"; +import { deleteReusableGroupInviteTokenAction } from "@/groups/actions/deleteReusableGroupInviteTokenAction"; +import { groupInviteLink } from "@/lib/routes"; type Props = { groupSlug: string; diff --git a/packages/hub/src/app/groups/[slug]/members/MembershipRoleButton.tsx b/packages/hub/src/app/groups/[slug]/members/MembershipRoleButton.tsx index 50a35e2101..0ea099986f 100644 --- a/packages/hub/src/app/groups/[slug]/members/MembershipRoleButton.tsx +++ b/packages/hub/src/app/groups/[slug]/members/MembershipRoleButton.tsx @@ -3,7 +3,7 @@ import { FC } from "react"; import { Button, Dropdown, DropdownMenu } from "@quri/ui"; -import { GroupMemberDTO } from "@/server/groups/data/members"; +import { GroupMemberDTO } from "@/groups/data/members"; import { SetMembershipRoleAction } from "./SetMembershipRoleAction"; diff --git a/packages/hub/src/app/groups/[slug]/members/SetMembershipRoleAction.tsx b/packages/hub/src/app/groups/[slug]/members/SetMembershipRoleAction.tsx index 4b21c20ea6..cad0a7c669 100644 --- a/packages/hub/src/app/groups/[slug]/members/SetMembershipRoleAction.tsx +++ b/packages/hub/src/app/groups/[slug]/members/SetMembershipRoleAction.tsx @@ -2,8 +2,8 @@ import { MembershipRole } from "@prisma/client"; import { FC } from "react"; import { ServerActionDropdownAction } from "@/components/ui/ServerActionDropdownAction"; -import { updateMembershipRoleAction } from "@/server/groups/actions/updateMembershipRoleAction"; -import { GroupMemberDTO } from "@/server/groups/data/members"; +import { updateMembershipRoleAction } from "@/groups/actions/updateMembershipRoleAction"; +import { GroupMemberDTO } from "@/groups/data/members"; type Props = { membership: GroupMemberDTO; diff --git a/packages/hub/src/app/groups/[slug]/members/page.tsx b/packages/hub/src/app/groups/[slug]/members/page.tsx index 0e2c417116..ce645624fc 100644 --- a/packages/hub/src/app/groups/[slug]/members/page.tsx +++ b/packages/hub/src/app/groups/[slug]/members/page.tsx @@ -2,7 +2,7 @@ import { loadGroupMembers, loadMyMembership, loadReusableInviteToken, -} from "@/server/groups/data/members"; +} from "@/groups/data/members"; import { GroupMemberList } from "./GroupMemberList"; import { GroupReusableInviteSection } from "./GroupReusableInviteSection"; diff --git a/packages/hub/src/app/groups/[slug]/page.tsx b/packages/hub/src/app/groups/[slug]/page.tsx index 395b93a6f3..a40bc7acce 100644 --- a/packages/hub/src/app/groups/[slug]/page.tsx +++ b/packages/hub/src/app/groups/[slug]/page.tsx @@ -1,6 +1,6 @@ +import { hasGroupMembership } from "@/groups/data/helpers"; import { ModelList } from "@/models/components/ModelList"; -import { hasGroupMembership } from "@/server/groups/data/helpers"; -import { loadModelCards } from "@/server/models/data/cards"; +import { loadModelCards } from "@/models/data/cards"; type Props = { params: Promise<{ slug: string }>; diff --git a/packages/hub/src/app/models/[owner]/[slug]/DeleteModelAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/DeleteModelAction.tsx index f5c8a8a52c..6699e4cd5c 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/DeleteModelAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/DeleteModelAction.tsx @@ -3,9 +3,9 @@ import { FC } from "react"; import { DropdownMenuAsyncActionItem, TrashIcon, useToast } from "@quri/ui"; -import { ownerRoute } from "@/routes"; -import { deleteModelAction } from "@/server/models/actions/deleteModelAction"; -import { ModelCardDTO } from "@/server/models/data/cards"; +import { ownerRoute } from "@/lib/routes"; +import { deleteModelAction } from "@/models/actions/deleteModelAction"; +import { ModelCardDTO } from "@/models/data/cards"; type Props = { model: ModelCardDTO; diff --git a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx index 8b4b4f2064..13d098aa62 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx @@ -38,12 +38,12 @@ import { useExitConfirmation } from "@/components/ExitConfirmationWrapper/hooks" import { EditRelativeValueExports } from "@/components/exports/EditRelativeValueExports"; import { ReactRoot } from "@/components/ReactRoot"; import { FormModal } from "@/components/ui/FormModal"; -import { SAMPLE_COUNT_DEFAULT, XY_POINT_LENGTH_DEFAULT } from "@/constants"; -import { useAvailableHeight } from "@/hooks/useAvailableHeight"; -import { useServerActionForm } from "@/hooks/useServerActionForm"; -import { modelRoute, variableRoute } from "@/routes"; -import { updateSquiggleSnippetModelAction } from "@/server/models/actions/updateSquiggleSnippetModelAction"; -import { ModelFullDTO } from "@/server/models/data/full"; +import { SAMPLE_COUNT_DEFAULT, XY_POINT_LENGTH_DEFAULT } from "@/lib/constants"; +import { useAvailableHeight } from "@/lib/hooks/useAvailableHeight"; +import { useServerActionForm } from "@/lib/hooks/useServerActionForm"; +import { modelRoute, variableRoute } from "@/lib/routes"; +import { updateSquiggleSnippetModelAction } from "@/models/actions/updateSquiggleSnippetModelAction"; +import { ModelFullDTO } from "@/models/data/full"; import { ImportTooltip } from "@/squiggle/components/ImportTooltip"; import { getHubLinker, diff --git a/packages/hub/src/app/models/[owner]/[slug]/ModelEntityNodes.tsx b/packages/hub/src/app/models/[owner]/[slug]/ModelEntityNodes.tsx index 5ef879cc98..fe304c9b14 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/ModelEntityNodes.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/ModelEntityNodes.tsx @@ -14,7 +14,7 @@ import { modelRoute, ownerRoute, variableRoute, -} from "@/routes"; +} from "@/lib/routes"; function hasTypename(owner: { __typename?: string; diff --git a/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx b/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx index d3bbd6b348..9e7cf349cf 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/ModelLayout.tsx @@ -6,14 +6,14 @@ import { CodeBracketSquareIcon, RectangleStackIcon, ShareIcon } from "@quri/ui"; import { EntityLayout } from "@/components/EntityLayout"; import { EntityTab } from "@/components/ui/EntityTab"; +import { modelRevisionsRoute, modelRoute } from "@/lib/routes"; +import { getExportedVariableNames } from "@/models/clientUtils"; +import { ModelCardDTO } from "@/models/data/cards"; import { totalImportLength, type VariableRevision, VariablesDropdown, -} from "@/lib/VariablesDropdown"; -import { modelRevisionsRoute, modelRoute } from "@/routes"; -import { ModelCardDTO } from "@/server/models/data/cards"; -import { getExportedVariableNames } from "@/server/models/utils"; +} from "@/variables/components/VariablesDropdown"; import { ModelEntityNodes } from "./ModelEntityNodes"; import { ModelPrivacyControls } from "./ModelPrivacyControls"; diff --git a/packages/hub/src/app/models/[owner]/[slug]/ModelPrivacyControls.tsx b/packages/hub/src/app/models/[owner]/[slug]/ModelPrivacyControls.tsx index a6ce61d13f..db1d65b745 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/ModelPrivacyControls.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/ModelPrivacyControls.tsx @@ -5,8 +5,8 @@ import { FC } from "react"; import { Dropdown, DropdownMenu, GlobeIcon, LockIcon } from "@quri/ui"; import { ServerActionDropdownAction } from "@/components/ui/ServerActionDropdownAction"; -import { updateModelPrivacyAction } from "@/server/models/actions/updateModelPrivacyAction"; -import { ModelCardDTO } from "@/server/models/data/cards"; +import { updateModelPrivacyAction } from "@/models/actions/updateModelPrivacyAction"; +import { ModelCardDTO } from "@/models/data/cards"; function getIconComponent(isPrivate: boolean) { return isPrivate ? LockIcon : GlobeIcon; diff --git a/packages/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx b/packages/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx index 0b956c0d5a..e8384ccf44 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx @@ -4,7 +4,7 @@ import { FC } from "react"; import { Cog8ToothIcon, Dropdown, DropdownMenu } from "@quri/ui"; import { EntityTab } from "@/components/ui/EntityTab"; -import { ModelCardDTO } from "@/server/models/data/cards"; +import { ModelCardDTO } from "@/models/data/cards"; import { DeleteModelAction } from "./DeleteModelAction"; import { MoveModelAction } from "./MoveModelAction"; diff --git a/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx index 0ef5988472..fc1127aa4e 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx @@ -5,9 +5,9 @@ import { RightArrowIcon } from "@quri/ui"; import { SelectOwner, SelectOwnerOption } from "@/components/SelectOwner"; import { ServerActionModalAction } from "@/components/ui/ServerActionModalAction"; -import { modelRoute } from "@/routes"; -import { moveModelAction } from "@/server/models/actions/moveModelAction"; -import { ModelCardDTO } from "@/server/models/data/cards"; +import { modelRoute } from "@/lib/routes"; +import { moveModelAction } from "@/models/actions/moveModelAction"; +import { ModelCardDTO } from "@/models/data/cards"; import { draftUtils, modelToDraftLocator } from "./SquiggleSnippetDraftDialog"; diff --git a/packages/hub/src/app/models/[owner]/[slug]/SquiggleSnippetDraftDialog.tsx b/packages/hub/src/app/models/[owner]/[slug]/SquiggleSnippetDraftDialog.tsx index dcd891fd52..e8f6b5ae4d 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/SquiggleSnippetDraftDialog.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/SquiggleSnippetDraftDialog.tsx @@ -2,8 +2,8 @@ import { FC, PropsWithChildren, useState } from "react"; import { Button, Modal } from "@quri/ui"; -import { useClientOnlyRender } from "@/hooks/useClientOnlyRender"; -import { ModelFullDTO } from "@/server/models/data/full"; +import { useClientOnlyRender } from "@/lib/hooks/useClientOnlyRender"; +import { ModelFullDTO } from "@/models/data/full"; import { SquiggleSnippetFormShape } from "./EditSquiggleSnippetModel"; diff --git a/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx index b8fb096914..3aa9222949 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx @@ -5,9 +5,9 @@ import { EditIcon } from "@quri/ui"; import { ServerActionModalAction } from "@/components/ui/ServerActionModalAction"; import { SlugFormField } from "@/components/ui/SlugFormField"; -import { modelRoute } from "@/routes"; -import { updateModelSlugAction } from "@/server/models/actions/updateModelSlugAction"; -import { ModelCardDTO } from "@/server/models/data/cards"; +import { modelRoute } from "@/lib/routes"; +import { updateModelSlugAction } from "@/models/actions/updateModelSlugAction"; +import { ModelCardDTO } from "@/models/data/cards"; import { draftUtils, modelToDraftLocator } from "./SquiggleSnippetDraftDialog"; diff --git a/packages/hub/src/app/models/[owner]/[slug]/layout.tsx b/packages/hub/src/app/models/[owner]/[slug]/layout.tsx index d8c0a982cd..71ec8671a3 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/layout.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/layout.tsx @@ -2,8 +2,8 @@ import { Metadata } from "next"; import { notFound } from "next/navigation"; import { PropsWithChildren, Suspense } from "react"; -import { loadModelCard } from "@/server/models/data/cards"; -import { isModelEditable } from "@/server/models/data/helpers"; +import { loadModelCard } from "@/models/data/cards"; +import { isModelEditable } from "@/models/data/helpers"; import { FallbackModelLayout } from "./FallbackLayout"; import { ModelLayout } from "./ModelLayout"; diff --git a/packages/hub/src/app/models/[owner]/[slug]/page.tsx b/packages/hub/src/app/models/[owner]/[slug]/page.tsx index b2c6e0e714..5c02ef44d8 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/page.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/page.tsx @@ -2,7 +2,7 @@ import { notFound } from "next/navigation"; import { Suspense } from "react"; import Skeleton from "react-loading-skeleton"; -import { loadModelFull } from "@/server/models/data/full"; +import { loadModelFull } from "@/models/data/full"; import { EditSquiggleSnippetModel } from "./EditSquiggleSnippetModel"; diff --git a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/BuildRelativeValuesCacheAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/BuildRelativeValuesCacheAction.tsx index 8e49cf4b7f..bda8ea8938 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/BuildRelativeValuesCacheAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/BuildRelativeValuesCacheAction.tsx @@ -4,8 +4,8 @@ import { FC } from "react"; import { RefreshIcon, useToast } from "@quri/ui"; import { ServerActionDropdownAction } from "@/components/ui/ServerActionDropdownAction"; -import { buildRelativeValuesCacheAction } from "@/server/relative-values/actions/buildRelativeValuesCacheAction"; -import { RelativeValuesExportFullDTO } from "@/server/relative-values/data/fullExport"; +import { buildRelativeValuesCacheAction } from "@/relative-values/actions/buildRelativeValuesCacheAction"; +import { RelativeValuesExportFullDTO } from "@/relative-values/data/fullExport"; export const BuildRelativeValuesCacheAction: FC<{ relativeValuesExport: RelativeValuesExportFullDTO; diff --git a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/ClearRelativeValuesCacheAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/ClearRelativeValuesCacheAction.tsx index 71e6c5d570..5748ad38a5 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/ClearRelativeValuesCacheAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/ClearRelativeValuesCacheAction.tsx @@ -4,8 +4,8 @@ import { FC } from "react"; import { TrashIcon, useToast } from "@quri/ui"; import { ServerActionDropdownAction } from "@/components/ui/ServerActionDropdownAction"; -import { clearRelativeValuesCacheAction } from "@/server/relative-values/actions/clearRelativeValuesCacheAction"; -import { RelativeValuesExportFullDTO } from "@/server/relative-values/data/fullExport"; +import { clearRelativeValuesCacheAction } from "@/relative-values/actions/clearRelativeValuesCacheAction"; +import { RelativeValuesExportFullDTO } from "@/relative-values/data/fullExport"; export const ClearRelativeValuesCacheAction: FC<{ relativeValuesExport: RelativeValuesExportFullDTO; diff --git a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/index.tsx b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/index.tsx index 600b222efe..ff22a4c5e1 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/index.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/index.tsx @@ -11,8 +11,8 @@ import { } from "@quri/ui"; import { CloseDropdownOnInvariantChange } from "@/components/ui/CloseDropdownOnInvariantChange"; -import { RelativeValuesDefinitionFullDTO } from "@/server/relative-values/data/full"; -import { RelativeValuesExportFullDTO } from "@/server/relative-values/data/fullExport"; +import { RelativeValuesDefinitionFullDTO } from "@/relative-values/data/full"; +import { RelativeValuesExportFullDTO } from "@/relative-values/data/fullExport"; import { BuildRelativeValuesCacheAction } from "./BuildRelativeValuesCacheAction"; import { ClearRelativeValuesCacheAction } from "./ClearRelativeValuesCacheAction"; diff --git a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/RelativeValuesModelLayout.tsx b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/RelativeValuesModelLayout.tsx index 8307bd20fc..d6880a84fa 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/RelativeValuesModelLayout.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/RelativeValuesModelLayout.tsx @@ -6,9 +6,9 @@ import Skeleton from "react-loading-skeleton"; import { result } from "@quri/squiggle-lang"; import { RelativeValuesProvider } from "@/relative-values/components/views/RelativeValuesProvider"; +import { RelativeValuesDefinitionFullDTO } from "@/relative-values/data/full"; +import { RelativeValuesExportFullDTO } from "@/relative-values/data/fullExport"; import { ModelEvaluator } from "@/relative-values/values/ModelEvaluator"; -import { RelativeValuesDefinitionFullDTO } from "@/server/relative-values/data/full"; -import { RelativeValuesExportFullDTO } from "@/server/relative-values/data/fullExport"; export const RelativeValuesModelLayout: FC< PropsWithChildren<{ diff --git a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/Tabs.tsx b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/Tabs.tsx index e2194ca7e5..046c39363a 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/Tabs.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/Tabs.tsx @@ -5,8 +5,8 @@ import { FC } from "react"; import { Bars4Icon, ScatterPlotIcon, TableCellsIcon } from "@quri/ui"; import { StyledTabLink } from "@/components/ui/StyledTabLink"; -import { modelForRelativeValuesExportRoute } from "@/routes"; -import { ModelCardDTO } from "@/server/models/data/cards"; +import { modelForRelativeValuesExportRoute } from "@/lib/routes"; +import { ModelCardDTO } from "@/models/data/cards"; // must be a client component because we can't pass icons from server components to client components export const RelativeValuesTabs: FC<{ diff --git a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/layout.tsx b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/layout.tsx index d2e2824e0e..715a7b7e11 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/layout.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/layout.tsx @@ -4,11 +4,11 @@ import { PropsWithChildren } from "react"; import { LinkIcon, ScaleIcon } from "@quri/ui"; import { StyledLink } from "@/components/ui/StyledLink"; -import { relativeValuesRoute } from "@/routes"; -import { loadModelCard } from "@/server/models/data/cards"; -import { isModelEditable } from "@/server/models/data/helpers"; -import { loadRelativeValuesDefinitionFull } from "@/server/relative-values/data/full"; -import { loadRelativeValuesExportFullFromModelRevision } from "@/server/relative-values/data/fullExport"; +import { relativeValuesRoute } from "@/lib/routes"; +import { loadModelCard } from "@/models/data/cards"; +import { isModelEditable } from "@/models/data/helpers"; +import { loadRelativeValuesDefinitionFull } from "@/relative-values/data/full"; +import { loadRelativeValuesExportFullFromModelRevision } from "@/relative-values/data/fullExport"; import { CacheMenu } from "./CacheMenu"; import { RelativeValuesModelLayout } from "./RelativeValuesModelLayout"; diff --git a/packages/hub/src/app/models/[owner]/[slug]/revisions/ModelRevisionsList.tsx b/packages/hub/src/app/models/[owner]/[slug]/revisions/ModelRevisionsList.tsx index bbd84dadcb..e7b1977860 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/revisions/ModelRevisionsList.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/revisions/ModelRevisionsList.tsx @@ -5,12 +5,12 @@ import { FC } from "react"; import { LoadMore } from "@/components/LoadMore"; import { StyledLink } from "@/components/ui/StyledLink"; import { UsernameLink } from "@/components/UsernameLink"; -import { usePaginator } from "@/hooks/usePaginator"; -import { commonDateFormat } from "@/lib/common"; -import { modelRevisionRoute } from "@/routes"; -import { ModelCardDTO } from "@/server/models/data/cards"; -import { ModelRevisionDTO } from "@/server/models/data/revisions"; -import { Paginated } from "@/server/types"; +import { commonDateFormat } from "@/lib/constants"; +import { usePaginator } from "@/lib/hooks/usePaginator"; +import { modelRevisionRoute } from "@/lib/routes"; +import { Paginated } from "@/lib/types"; +import { ModelCardDTO } from "@/models/data/cards"; +import { ModelRevisionDTO } from "@/models/data/revisions"; const ModelRevisionItem: FC<{ model: ModelCardDTO; diff --git a/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/ModelRevisionView.tsx b/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/ModelRevisionView.tsx index eb58c05166..0811fc1a29 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/ModelRevisionView.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/ModelRevisionView.tsx @@ -7,7 +7,7 @@ import { versionedSquigglePackages, } from "@quri/versioned-squiggle-components"; -import { ModelRevisionFullDTO } from "@/server/models/data/fullRevision"; +import { ModelRevisionFullDTO } from "@/models/data/fullRevision"; import { getHubLinker } from "@/squiggle/components/linker"; export const ModelRevisionView: FC<{ diff --git a/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/page.tsx b/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/page.tsx index c21ac83eb3..e92a2ff6d6 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/page.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/page.tsx @@ -4,9 +4,9 @@ import { notFound } from "next/navigation"; import { CommentIcon } from "@quri/ui"; import { StyledLink } from "@/components/ui/StyledLink"; -import { commonDateFormat } from "@/lib/common"; -import { modelRoute } from "@/routes"; -import { loadModelRevisionFull } from "@/server/models/data/fullRevision"; +import { commonDateFormat } from "@/lib/constants"; +import { modelRoute } from "@/lib/routes"; +import { loadModelRevisionFull } from "@/models/data/fullRevision"; import { ModelRevisionView } from "./ModelRevisionView"; diff --git a/packages/hub/src/app/models/[owner]/[slug]/revisions/page.tsx b/packages/hub/src/app/models/[owner]/[slug]/revisions/page.tsx index 7d993c403e..f351dea506 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/revisions/page.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/revisions/page.tsx @@ -3,8 +3,8 @@ import { Suspense } from "react"; import Skeleton from "react-loading-skeleton"; import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; -import { loadModelCard } from "@/server/models/data/cards"; -import { loadModelRevisions } from "@/server/models/data/revisions"; +import { loadModelCard } from "@/models/data/cards"; +import { loadModelRevisions } from "@/models/data/revisions"; import { ModelRevisionsList } from "./ModelRevisionsList"; diff --git a/packages/hub/src/app/models/[owner]/[slug]/useFixModelUrlCasing.ts b/packages/hub/src/app/models/[owner]/[slug]/useFixModelUrlCasing.ts index 7028701e21..b30e775153 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/useFixModelUrlCasing.ts +++ b/packages/hub/src/app/models/[owner]/[slug]/useFixModelUrlCasing.ts @@ -1,7 +1,7 @@ import { usePathname, useRouter } from "next/navigation"; -import { patchModelRoute } from "@/routes"; -import { ModelCardDTO } from "@/server/models/data/cards"; +import { patchModelRoute } from "@/lib/routes"; +import { ModelCardDTO } from "@/models/data/cards"; export function useFixModelUrlCasing(model: ModelCardDTO) { const router = useRouter(); diff --git a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/VariableRevisionsPanel.tsx b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/VariableRevisionsPanel.tsx index 7647f292fb..a857388a7a 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/VariableRevisionsPanel.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/VariableRevisionsPanel.tsx @@ -10,11 +10,11 @@ import { CheckIcon, XIcon } from "@quri/ui"; import { LoadMore } from "@/components/LoadMore"; import { Link } from "@/components/ui/Link"; -import { usePaginator } from "@/hooks/usePaginator"; +import { usePaginator } from "@/lib/hooks/usePaginator"; +import { variableRevisionRoute } from "@/lib/routes"; import { exportTypeIcon } from "@/lib/typeIcon"; -import { variableRevisionRoute } from "@/routes"; -import { Paginated } from "@/server/types"; -import { VariableRevisionDTO } from "@/server/variables/data/variableRevisions"; +import { Paginated } from "@/lib/types"; +import { VariableRevisionDTO } from "@/variables/data/variableRevisions"; const buildStatusIcon = (status: string) => { switch (status) { diff --git a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/layout.tsx b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/layout.tsx index f92474dd02..11cc682167 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/layout.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/layout.tsx @@ -1,6 +1,6 @@ import { PropsWithChildren } from "react"; -import { loadVariableRevisions } from "@/server/variables/data/variableRevisions"; +import { loadVariableRevisions } from "@/variables/data/variableRevisions"; import { VariableRevisionsPanel } from "./VariableRevisionsPanel"; diff --git a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/page.tsx b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/page.tsx index 07b3fca0f9..f1b7aa5a94 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/page.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/page.tsx @@ -1,7 +1,7 @@ import { notFound } from "next/navigation"; -import { loadVariableRevisionFull } from "@/server/variables/data/fullVariableRevision"; -import { loadVariableCard } from "@/server/variables/data/variableCards"; +import { loadVariableRevisionFull } from "@/variables/data/fullVariableRevision"; +import { loadVariableCard } from "@/variables/data/variableCards"; import { VariableRevisionPage } from "./revisions/[revisionId]/VariableRevisionPage"; diff --git a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/revisions/[revisionId]/VariableRevisionPage.tsx b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/revisions/[revisionId]/VariableRevisionPage.tsx index ee9b0426f0..05c2ea19ff 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/revisions/[revisionId]/VariableRevisionPage.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/revisions/[revisionId]/VariableRevisionPage.tsx @@ -8,8 +8,8 @@ import { versionSupportsSqPathV2, } from "@quri/versioned-squiggle-components"; -import { VariableRevisionFullDTO } from "@/server/variables/data/fullVariableRevision"; import { sqProjectWithHubLinker } from "@/squiggle/components/linker"; +import { VariableRevisionFullDTO } from "@/variables/data/fullVariableRevision"; type SquiggleProps = { variableName: string; diff --git a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/revisions/[revisionId]/page.tsx b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/revisions/[revisionId]/page.tsx index e3b7f40d9e..e31419374e 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/revisions/[revisionId]/page.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/revisions/[revisionId]/page.tsx @@ -1,6 +1,6 @@ import { notFound } from "next/navigation"; -import { loadVariableRevisionFull } from "@/server/variables/data/fullVariableRevision"; +import { loadVariableRevisionFull } from "@/variables/data/fullVariableRevision"; import { VariableRevisionPage } from "./VariableRevisionPage"; diff --git a/packages/hub/src/app/models/[owner]/[slug]/view/ViewSquiggleSnippet.tsx b/packages/hub/src/app/models/[owner]/[slug]/view/ViewSquiggleSnippet.tsx index 7ec61298ee..6b77c7fee1 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/view/ViewSquiggleSnippet.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/view/ViewSquiggleSnippet.tsx @@ -6,7 +6,7 @@ import { versionedSquigglePackages, } from "@quri/versioned-squiggle-components"; -import { ModelCardDTO } from "@/server/models/data/cards"; +import { ModelCardDTO } from "@/models/data/cards"; import { sqProjectWithHubLinker } from "@/squiggle/components/linker"; type Props = { diff --git a/packages/hub/src/app/models/[owner]/[slug]/view/page.tsx b/packages/hub/src/app/models/[owner]/[slug]/view/page.tsx index a4e61e1605..d62790678f 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/view/page.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/view/page.tsx @@ -1,7 +1,7 @@ import { notFound } from "next/navigation"; import { ViewSquiggleSnippet } from "@/app/models/[owner]/[slug]/view/ViewSquiggleSnippet"; -import { loadModelCard } from "@/server/models/data/cards"; +import { loadModelCard } from "@/models/data/cards"; type Props = { params: Promise<{ owner: string; slug: string }>; diff --git a/packages/hub/src/app/new/definition/NewDefinition.tsx b/packages/hub/src/app/new/definition/NewDefinition.tsx index 647252f811..2dda823d86 100644 --- a/packages/hub/src/app/new/definition/NewDefinition.tsx +++ b/packages/hub/src/app/new/definition/NewDefinition.tsx @@ -4,10 +4,10 @@ import { useRouter } from "next/navigation"; import { FC } from "react"; import { H1 } from "@/components/ui/Headers"; +import { relativeValuesRoute } from "@/lib/routes"; +import { createRelativeValuesDefinitionAction } from "@/relative-values/actions/createRelativeValuesDefinitionAction"; import { RelativeValuesDefinitionForm } from "@/relative-values/components/RelativeValuesDefinitionForm"; import { FormShape } from "@/relative-values/components/RelativeValuesDefinitionForm/FormShape"; -import { relativeValuesRoute } from "@/routes"; -import { createRelativeValuesDefinitionAction } from "@/server/relative-values/actions/createRelativeValuesDefinitionAction"; export const NewDefinition: FC = () => { const router = useRouter(); diff --git a/packages/hub/src/app/new/group/NewGroup.tsx b/packages/hub/src/app/new/group/NewGroup.tsx index a3f0770601..03fda16f2f 100644 --- a/packages/hub/src/app/new/group/NewGroup.tsx +++ b/packages/hub/src/app/new/group/NewGroup.tsx @@ -7,9 +7,9 @@ import { Button } from "@quri/ui"; import { H1 } from "@/components/ui/Headers"; import { SlugFormField } from "@/components/ui/SlugFormField"; -import { useServerActionForm } from "@/hooks/useServerActionForm"; -import { groupRoute } from "@/routes"; -import { createGroupAction } from "@/server/groups/actions/createGroupAction"; +import { createGroupAction } from "@/groups/actions/createGroupAction"; +import { useServerActionForm } from "@/lib/hooks/useServerActionForm"; +import { groupRoute } from "@/lib/routes"; export const NewGroup: FC = () => { const router = useRouter(); diff --git a/packages/hub/src/app/new/model/NewModel.tsx b/packages/hub/src/app/new/model/NewModel.tsx index 9797b593e7..7ec8e46c3c 100644 --- a/packages/hub/src/app/new/model/NewModel.tsx +++ b/packages/hub/src/app/new/model/NewModel.tsx @@ -10,9 +10,9 @@ import { defaultSquiggleVersion } from "@quri/versioned-squiggle-components"; import { SelectGroup, SelectGroupOption } from "@/components/SelectGroup"; import { H1 } from "@/components/ui/Headers"; import { SlugFormField } from "@/components/ui/SlugFormField"; -import { useServerActionForm } from "@/hooks/useServerActionForm"; -import { modelRoute } from "@/routes"; -import { createSquiggleSnippetModelAction } from "@/server/models/actions/createSquiggleSnippetModelAction"; +import { useServerActionForm } from "@/lib/hooks/useServerActionForm"; +import { modelRoute } from "@/lib/routes"; +import { createSquiggleSnippetModelAction } from "@/models/actions/createSquiggleSnippetModelAction"; const defaultCode = `/* Describe your code here diff --git a/packages/hub/src/app/new/model/page.tsx b/packages/hub/src/app/new/model/page.tsx index 94de025aa7..de28fdc54f 100644 --- a/packages/hub/src/app/new/model/page.tsx +++ b/packages/hub/src/app/new/model/page.tsx @@ -2,8 +2,8 @@ import { Metadata } from "next"; import { z } from "zod"; import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; -import { getMyGroup } from "@/server/groups/data/card"; -import { getSessionUserOrRedirect } from "@/server/users/auth"; +import { getMyGroup } from "@/groups/data/groupCards"; +import { getSessionUserOrRedirect } from "@/users/auth"; import { NewModel } from "./NewModel"; diff --git a/packages/hub/src/app/relative-values/[owner]/[slug]/DefinitionLayout.tsx b/packages/hub/src/app/relative-values/[owner]/[slug]/DefinitionLayout.tsx index b818ed22f6..f196853b2c 100644 --- a/packages/hub/src/app/relative-values/[owner]/[slug]/DefinitionLayout.tsx +++ b/packages/hub/src/app/relative-values/[owner]/[slug]/DefinitionLayout.tsx @@ -16,8 +16,8 @@ import { ownerRoute, relativeValuesEditRoute, relativeValuesRoute, -} from "@/routes"; -import { RelativeValuesDefinitionCardDTO } from "@/server/relative-values/data/cards"; +} from "@/lib/routes"; +import { RelativeValuesDefinitionCardDTO } from "@/relative-values/data/cards"; import { DeleteDefinitionAction } from "./DeleteRelativeValuesDefinitionAction"; diff --git a/packages/hub/src/app/relative-values/[owner]/[slug]/DeleteRelativeValuesDefinitionAction.tsx b/packages/hub/src/app/relative-values/[owner]/[slug]/DeleteRelativeValuesDefinitionAction.tsx index 55de367939..7dd94bf231 100644 --- a/packages/hub/src/app/relative-values/[owner]/[slug]/DeleteRelativeValuesDefinitionAction.tsx +++ b/packages/hub/src/app/relative-values/[owner]/[slug]/DeleteRelativeValuesDefinitionAction.tsx @@ -3,7 +3,7 @@ import { FC } from "react"; import { DropdownMenuAsyncActionItem, TrashIcon, useToast } from "@quri/ui"; -import { deleteRelativeValuesDefinitionAction } from "@/server/relative-values/actions/deleteRelativeValuesDefinitionAction"; +import { deleteRelativeValuesDefinitionAction } from "@/relative-values/actions/deleteRelativeValuesDefinitionAction"; type Props = { owner: string; diff --git a/packages/hub/src/app/relative-values/[owner]/[slug]/RelativeValuesDefinitionPage.tsx b/packages/hub/src/app/relative-values/[owner]/[slug]/RelativeValuesDefinitionPage.tsx index 5ad3953cd8..080199f2fb 100644 --- a/packages/hub/src/app/relative-values/[owner]/[slug]/RelativeValuesDefinitionPage.tsx +++ b/packages/hub/src/app/relative-values/[owner]/[slug]/RelativeValuesDefinitionPage.tsx @@ -5,10 +5,10 @@ import { LockIcon } from "@quri/ui"; import { H2 } from "@/components/ui/Headers"; import { StyledLink } from "@/components/ui/StyledLink"; +import { modelForRelativeValuesExportRoute } from "@/lib/routes"; import { RelativeValuesDefinitionRevision } from "@/relative-values/components/RelativeValuesDefinitionRevision"; -import { modelForRelativeValuesExportRoute } from "@/routes"; -import { RelativeValuesExportCardDTO } from "@/server/relative-values/data/exports"; -import { RelativeValuesDefinitionFullDTO } from "@/server/relative-values/data/full"; +import { RelativeValuesExportCardDTO } from "@/relative-values/data/exports"; +import { RelativeValuesDefinitionFullDTO } from "@/relative-values/data/full"; const ExportItem: FC<{ modelExport: RelativeValuesExportCardDTO; diff --git a/packages/hub/src/app/relative-values/[owner]/[slug]/edit/EditRelativeValuesDefinition.tsx b/packages/hub/src/app/relative-values/[owner]/[slug]/edit/EditRelativeValuesDefinition.tsx index e66d8a69a7..60761875f6 100644 --- a/packages/hub/src/app/relative-values/[owner]/[slug]/edit/EditRelativeValuesDefinition.tsx +++ b/packages/hub/src/app/relative-values/[owner]/[slug]/edit/EditRelativeValuesDefinition.tsx @@ -2,11 +2,11 @@ import { useRouter } from "next/navigation"; import { FC } from "react"; +import { relativeValuesRoute } from "@/lib/routes"; +import { updateRelativeValuesDefinitionAction } from "@/relative-values/actions/updateRelativeValuesDefinitionAction"; import { RelativeValuesDefinitionForm } from "@/relative-values/components/RelativeValuesDefinitionForm"; import { FormShape } from "@/relative-values/components/RelativeValuesDefinitionForm/FormShape"; -import { relativeValuesRoute } from "@/routes"; -import { updateRelativeValuesDefinitionAction } from "@/server/relative-values/actions/updateRelativeValuesDefinitionAction"; -import { RelativeValuesDefinitionFullDTO } from "@/server/relative-values/data/full"; +import { RelativeValuesDefinitionFullDTO } from "@/relative-values/data/full"; export const EditRelativeValuesDefinition: FC<{ definition: RelativeValuesDefinitionFullDTO; diff --git a/packages/hub/src/app/relative-values/[owner]/[slug]/edit/page.tsx b/packages/hub/src/app/relative-values/[owner]/[slug]/edit/page.tsx index 45a0f63340..6dff6690c5 100644 --- a/packages/hub/src/app/relative-values/[owner]/[slug]/edit/page.tsx +++ b/packages/hub/src/app/relative-values/[owner]/[slug]/edit/page.tsx @@ -1,9 +1,9 @@ import { notFound } from "next/navigation"; import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; -import { controlsOwnerId } from "@/server/owners/auth"; -import { loadRelativeValuesDefinitionFull } from "@/server/relative-values/data/full"; -import { getSessionUserOrRedirect } from "@/server/users/auth"; +import { controlsOwnerId } from "@/owners/data/auth"; +import { loadRelativeValuesDefinitionFull } from "@/relative-values/data/full"; +import { getSessionUserOrRedirect } from "@/users/auth"; import { EditRelativeValuesDefinition } from "./EditRelativeValuesDefinition"; diff --git a/packages/hub/src/app/relative-values/[owner]/[slug]/layout.tsx b/packages/hub/src/app/relative-values/[owner]/[slug]/layout.tsx index a5be319b27..b2d0fee3f6 100644 --- a/packages/hub/src/app/relative-values/[owner]/[slug]/layout.tsx +++ b/packages/hub/src/app/relative-values/[owner]/[slug]/layout.tsx @@ -2,8 +2,8 @@ import { Metadata } from "next"; import { notFound } from "next/navigation"; import { PropsWithChildren } from "react"; -import { controlsOwnerId } from "@/server/owners/auth"; -import { loadRelativeValuesDefinitionCard } from "@/server/relative-values/data/cards"; +import { controlsOwnerId } from "@/owners/data/auth"; +import { loadRelativeValuesDefinitionCard } from "@/relative-values/data/cards"; import { DefinitionLayout } from "./DefinitionLayout"; diff --git a/packages/hub/src/app/relative-values/[owner]/[slug]/page.tsx b/packages/hub/src/app/relative-values/[owner]/[slug]/page.tsx index 66c2ca6495..3da92c3d2d 100644 --- a/packages/hub/src/app/relative-values/[owner]/[slug]/page.tsx +++ b/packages/hub/src/app/relative-values/[owner]/[slug]/page.tsx @@ -1,7 +1,7 @@ import { notFound } from "next/navigation"; -import { loadRelativeValuesExportCardsFromDefinition } from "@/server/relative-values/data/exports"; -import { loadRelativeValuesDefinitionFull } from "@/server/relative-values/data/full"; +import { loadRelativeValuesExportCardsFromDefinition } from "@/relative-values/data/exports"; +import { loadRelativeValuesDefinitionFull } from "@/relative-values/data/full"; import { RelativeValuesDefinitionPage } from "./RelativeValuesDefinitionPage"; diff --git a/packages/hub/src/app/settings/choose-username/ChooseUsername.tsx b/packages/hub/src/app/settings/choose-username/ChooseUsername.tsx index 82168dc8da..2af45fc0c3 100644 --- a/packages/hub/src/app/settings/choose-username/ChooseUsername.tsx +++ b/packages/hub/src/app/settings/choose-username/ChooseUsername.tsx @@ -6,7 +6,7 @@ import { FormProvider, useForm } from "react-hook-form"; import { Button } from "@quri/ui"; import { SlugFormField } from "@/components/ui/SlugFormField"; -import { setUsername } from "@/server/users/actions"; +import { setUsername } from "@/users/actions"; export const ChooseUsername: FC = () => { const router = useRouter(); diff --git a/packages/hub/src/app/settings/choose-username/page.tsx b/packages/hub/src/app/settings/choose-username/page.tsx index b00d29ae4b..54359fa4a4 100644 --- a/packages/hub/src/app/settings/choose-username/page.tsx +++ b/packages/hub/src/app/settings/choose-username/page.tsx @@ -1,7 +1,7 @@ import { Metadata } from "next"; import { redirect } from "next/navigation"; -import { getSessionUserOrRedirect } from "@/server/users/auth"; +import { getSessionUserOrRedirect } from "@/users/auth"; import { ChooseUsername } from "./ChooseUsername"; diff --git a/packages/hub/src/app/status/page.tsx b/packages/hub/src/app/status/page.tsx index 9b978591a8..65b6f346ab 100644 --- a/packages/hub/src/app/status/page.tsx +++ b/packages/hub/src/app/status/page.tsx @@ -1,7 +1,7 @@ import { Metadata } from "next"; import { FC } from "react"; -import { getGlobalStatistics } from "@/server/globalStatistics"; +import { getGlobalStatistics } from "@/lib/server/globalStatistics"; const StatRow: FC<{ name: string; value: number }> = ({ name, value }) => ( diff --git a/packages/hub/src/app/users/[username]/NewModelButton.tsx b/packages/hub/src/app/users/[username]/NewModelButton.tsx index be7f2d3f5e..94efc5d467 100644 --- a/packages/hub/src/app/users/[username]/NewModelButton.tsx +++ b/packages/hub/src/app/users/[username]/NewModelButton.tsx @@ -4,7 +4,7 @@ import { FC } from "react"; import { Button, PlusIcon } from "@quri/ui"; -import { newDefinitionRoute, newGroupRoute, newModelRoute } from "@/routes"; +import { newDefinitionRoute, newGroupRoute, newModelRoute } from "@/lib/routes"; export const NewModelButton: FC = () => { const segment = useSelectedLayoutSegment(); diff --git a/packages/hub/src/app/users/[username]/definitions/page.tsx b/packages/hub/src/app/users/[username]/definitions/page.tsx index 327a82593a..f7747175ad 100644 --- a/packages/hub/src/app/users/[username]/definitions/page.tsx +++ b/packages/hub/src/app/users/[username]/definitions/page.tsx @@ -1,7 +1,7 @@ import { Metadata } from "next"; import { RelativeValuesDefinitionList } from "@/relative-values/components/RelativeValuesDefinitionList"; -import { loadDefinitionCards } from "@/server/relative-values/data/cards"; +import { loadDefinitionCards } from "@/relative-values/data/cards"; type Props = { params: Promise<{ username: string }>; diff --git a/packages/hub/src/app/users/[username]/groups/page.tsx b/packages/hub/src/app/users/[username]/groups/page.tsx index 7e1b12ef12..3125cdc281 100644 --- a/packages/hub/src/app/users/[username]/groups/page.tsx +++ b/packages/hub/src/app/users/[username]/groups/page.tsx @@ -1,7 +1,7 @@ import { Metadata } from "next"; import { GroupList } from "@/groups/components/GroupList"; -import { loadGroupCards } from "@/server/groups/data/card"; +import { loadGroupCards } from "@/groups/data/groupCards"; type Props = { params: Promise<{ username: string }>; diff --git a/packages/hub/src/app/users/[username]/layout.tsx b/packages/hub/src/app/users/[username]/layout.tsx index bd9e1e7147..2b7e4af270 100644 --- a/packages/hub/src/app/users/[username]/layout.tsx +++ b/packages/hub/src/app/users/[username]/layout.tsx @@ -4,7 +4,6 @@ import { PropsWithChildren } from "react"; import { UserIcon } from "@quri/ui"; -import { auth } from "@/auth"; import { NarrowPageLayout } from "@/components/layout/NarrowPageLayout"; import { H1 } from "@/components/ui/Headers"; import { @@ -16,8 +15,9 @@ import { userGroupsRoute, userRoute, userVariablesRoute, -} from "@/routes"; -import { loadLayoutUser } from "@/server/users/data/layoutUser"; +} from "@/lib/routes"; +import { auth } from "@/lib/server/auth"; +import { loadLayoutUser } from "@/users/data/layoutUser"; import { NewModelButton } from "./NewModelButton"; diff --git a/packages/hub/src/app/users/[username]/page.tsx b/packages/hub/src/app/users/[username]/page.tsx index 81a4f5e53d..3367968786 100644 --- a/packages/hub/src/app/users/[username]/page.tsx +++ b/packages/hub/src/app/users/[username]/page.tsx @@ -1,7 +1,7 @@ import { Metadata } from "next"; import { ModelList } from "@/models/components/ModelList"; -import { loadModelCards } from "@/server/models/data/cards"; +import { loadModelCards } from "@/models/data/cards"; type Props = { params: Promise<{ username: string }>; diff --git a/packages/hub/src/app/users/[username]/variables/page.tsx b/packages/hub/src/app/users/[username]/variables/page.tsx index 0b18b53afa..6217609a63 100644 --- a/packages/hub/src/app/users/[username]/variables/page.tsx +++ b/packages/hub/src/app/users/[username]/variables/page.tsx @@ -1,7 +1,7 @@ import { Metadata } from "next"; -import { loadVariableCards } from "@/server/variables/data/variableCards"; import { VariableList } from "@/variables/components/VariableList"; +import { loadVariableCards } from "@/variables/data/variableCards"; type Props = { params: Promise<{ username: string }>; diff --git a/packages/hub/src/components/GroupLink.tsx b/packages/hub/src/components/GroupLink.tsx index a26ca12200..d447185b68 100644 --- a/packages/hub/src/components/GroupLink.tsx +++ b/packages/hub/src/components/GroupLink.tsx @@ -1,7 +1,7 @@ import { FC } from "react"; import { StyledLink } from "@/components/ui/StyledLink"; -import { groupRoute } from "@/routes"; +import { groupRoute } from "@/lib/routes"; export const GroupLink: FC<{ slug: string }> = ({ slug }) => { return {slug}; diff --git a/packages/hub/src/components/LoadMoreViaSearchParam.tsx b/packages/hub/src/components/LoadMoreViaSearchParam.tsx index ef9423c8e6..f00d7e71cf 100644 --- a/packages/hub/src/components/LoadMoreViaSearchParam.tsx +++ b/packages/hub/src/components/LoadMoreViaSearchParam.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; -import { useUpdateSearchParams } from "@/hooks/useUpdateSearchParams"; +import { useUpdateSearchParams } from "@/lib/hooks/useUpdateSearchParams"; import { LoadMore } from "./LoadMore"; diff --git a/packages/hub/src/components/UsernameLink.tsx b/packages/hub/src/components/UsernameLink.tsx index df8a3e76dc..ea40eb3a01 100644 --- a/packages/hub/src/components/UsernameLink.tsx +++ b/packages/hub/src/components/UsernameLink.tsx @@ -1,7 +1,7 @@ import { FC } from "react"; import { StyledLink } from "@/components/ui/StyledLink"; -import { userRoute } from "@/routes"; +import { userRoute } from "@/lib/routes"; export const UsernameLink: FC<{ username: string }> = ({ username }) => { return {username}; diff --git a/packages/hub/src/components/WithAuth/index.tsx b/packages/hub/src/components/WithAuth/index.tsx index 9552b1f952..ec5fd6a16b 100644 --- a/packages/hub/src/components/WithAuth/index.tsx +++ b/packages/hub/src/components/WithAuth/index.tsx @@ -1,8 +1,8 @@ import { FC, PropsWithChildren } from "react"; -import { auth } from "@/auth"; -import { prisma } from "@/prisma"; -import { isRootEmail, isSignedIn } from "@/server/users/auth"; +import { auth } from "@/lib/server/auth"; +import { prisma } from "@/lib/server/prisma"; +import { isRootEmail, isSignedIn } from "@/users/auth"; import { RedirectToLogin } from "./RedirectToLogin"; diff --git a/packages/hub/src/components/exports/EditRelativeValueExports.tsx b/packages/hub/src/components/exports/EditRelativeValueExports.tsx index ed1fc3e510..0bfaaa664c 100644 --- a/packages/hub/src/components/exports/EditRelativeValueExports.tsx +++ b/packages/hub/src/components/exports/EditRelativeValueExports.tsx @@ -7,9 +7,9 @@ import { RelativeValuesExportInput } from "@/app/models/[owner]/[slug]/EditSquig import { modelForRelativeValuesExportRoute, relativeValuesRoute, -} from "@/routes"; -import { ModelFullDTO } from "@/server/models/data/full"; -import { FindRelativeValuesForSelectResult } from "@/server/relative-values/data/findRelativeValuesForSelect"; +} from "@/lib/routes"; +import { ModelFullDTO } from "@/models/data/full"; +import { FindRelativeValuesForSelectResult } from "@/relative-values/data/findRelativeValuesForSelect"; import { SelectOwner, SelectOwnerOption } from "../SelectOwner"; import { FormModal } from "../ui/FormModal"; diff --git a/packages/hub/src/components/exports/SelectRelativeValuesDefinition.tsx b/packages/hub/src/components/exports/SelectRelativeValuesDefinition.tsx index 6643e7c4d4..7696589155 100644 --- a/packages/hub/src/components/exports/SelectRelativeValuesDefinition.tsx +++ b/packages/hub/src/components/exports/SelectRelativeValuesDefinition.tsx @@ -4,7 +4,7 @@ import { z } from "zod"; import { SelectFormField } from "@quri/ui"; -import { FindRelativeValuesForSelectResult } from "@/server/relative-values/data/findRelativeValuesForSelect"; +import { FindRelativeValuesForSelectResult } from "@/relative-values/data/findRelativeValuesForSelect"; import { SelectOwnerOption } from "../SelectOwner"; diff --git a/packages/hub/src/components/layout/RootLayout/MyGroupsMenu.tsx b/packages/hub/src/components/layout/RootLayout/MyGroupsMenu.tsx index 36eca80f47..0566996f2d 100644 --- a/packages/hub/src/components/layout/RootLayout/MyGroupsMenu.tsx +++ b/packages/hub/src/components/layout/RootLayout/MyGroupsMenu.tsx @@ -3,9 +3,9 @@ import { FC } from "react"; import { DropdownMenuHeader, GroupIcon, PlusIcon } from "@quri/ui"; import { DropdownMenuNextLinkItem } from "@/components/ui/DropdownMenuNextLinkItem"; -import { groupRoute, newGroupRoute } from "@/routes"; -import { GroupCardDTO } from "@/server/groups/data/card"; -import { Paginated } from "@/server/types"; +import { GroupCardDTO } from "@/groups/data/groupCards"; +import { groupRoute, newGroupRoute } from "@/lib/routes"; +import { Paginated } from "@/lib/types"; type Props = { groups: Paginated; diff --git a/packages/hub/src/components/layout/RootLayout/PageFooter.tsx b/packages/hub/src/components/layout/RootLayout/PageFooter.tsx index fb062c63b2..984fa28af5 100644 --- a/packages/hub/src/components/layout/RootLayout/PageFooter.tsx +++ b/packages/hub/src/components/layout/RootLayout/PageFooter.tsx @@ -8,9 +8,13 @@ import { GITHUB_URL, NEWSLETTER_URL, QURI_DONATE_URL, -} from "@/lib/common"; +} from "@/lib/constants"; +import { + aboutRoute, + privacyPolicyRoute, + termsOfServiceRoute, +} from "@/lib/routes"; import logoPic from "@/public/logo.png"; -import { aboutRoute, privacyPolicyRoute, termsOfServiceRoute } from "@/routes"; const linkClasses = "items-center flex hover:text-gray-900"; diff --git a/packages/hub/src/components/layout/RootLayout/PageFooterIfNecessary.tsx b/packages/hub/src/components/layout/RootLayout/PageFooterIfNecessary.tsx index 59fe220686..cb09e9fbc3 100644 --- a/packages/hub/src/components/layout/RootLayout/PageFooterIfNecessary.tsx +++ b/packages/hub/src/components/layout/RootLayout/PageFooterIfNecessary.tsx @@ -3,7 +3,7 @@ import { usePathname } from "next/navigation"; import { FC } from "react"; -import { isAiRoute, isModelRoute } from "@/routes"; +import { isAiRoute, isModelRoute } from "@/lib/routes"; import { PageFooter } from "./PageFooter"; diff --git a/packages/hub/src/components/layout/RootLayout/PageMenu.tsx b/packages/hub/src/components/layout/RootLayout/PageMenu.tsx index e29d6cacec..8a8a82e028 100644 --- a/packages/hub/src/components/layout/RootLayout/PageMenu.tsx +++ b/packages/hub/src/components/layout/RootLayout/PageMenu.tsx @@ -17,10 +17,10 @@ import { UserCircleIcon, } from "@quri/ui"; -import { SQUIGGLE_DOCS_URL } from "@/lib/common"; -import { aboutRoute, aiRoute, newModelRoute } from "@/routes"; -import { GroupCardDTO } from "@/server/groups/data/card"; -import { Paginated } from "@/server/types"; +import { GroupCardDTO } from "@/groups/data/groupCards"; +import { SQUIGGLE_DOCS_URL } from "@/lib/constants"; +import { aboutRoute, aiRoute, newModelRoute } from "@/lib/routes"; +import { Paginated } from "@/lib/types"; import { GlobalSearch } from "../../GlobalSearch"; import { DesktopUserControls } from "./DesktopUserControls"; diff --git a/packages/hub/src/components/layout/RootLayout/UserControlsMenu.tsx b/packages/hub/src/components/layout/RootLayout/UserControlsMenu.tsx index 728dfb0398..0497afb145 100644 --- a/packages/hub/src/components/layout/RootLayout/UserControlsMenu.tsx +++ b/packages/hub/src/components/layout/RootLayout/UserControlsMenu.tsx @@ -10,7 +10,7 @@ import { } from "@quri/ui"; import { DropdownMenuNextLinkItem } from "@/components/ui/DropdownMenuNextLinkItem"; -import { newDefinitionRoute, userRoute } from "@/routes"; +import { newDefinitionRoute, userRoute } from "@/lib/routes"; type Props = { close: () => void; diff --git a/packages/hub/src/components/layout/RootLayout/index.tsx b/packages/hub/src/components/layout/RootLayout/index.tsx index 8f8484eb7b..a96ac6a73b 100644 --- a/packages/hub/src/components/layout/RootLayout/index.tsx +++ b/packages/hub/src/components/layout/RootLayout/index.tsx @@ -1,8 +1,8 @@ import { FC, PropsWithChildren, Suspense } from "react"; -import { auth } from "@/auth"; import { Link } from "@/components/ui/Link"; -import { loadGroupCards } from "@/server/groups/data/card"; +import { loadGroupCards } from "@/groups/data/groupCards"; +import { auth } from "@/lib/server/auth"; import { ReactRoot } from "../../ReactRoot"; import { PageFooterIfNecessary } from "./PageFooterIfNecessary"; diff --git a/packages/hub/src/components/layout/RootLayout/useForceChooseUsername.ts b/packages/hub/src/components/layout/RootLayout/useForceChooseUsername.ts index cc0d4d5df1..1dd5427d36 100644 --- a/packages/hub/src/components/layout/RootLayout/useForceChooseUsername.ts +++ b/packages/hub/src/components/layout/RootLayout/useForceChooseUsername.ts @@ -2,7 +2,7 @@ import { Session } from "next-auth"; import { usePathname, useRouter } from "next/navigation"; import { useEffect } from "react"; -import { chooseUsernameRoute } from "@/routes"; +import { chooseUsernameRoute } from "@/lib/routes"; export function useForceChooseUsername(session: Session | null) { const pathname = usePathname(); diff --git a/packages/hub/src/components/ui/ServerActionModalAction.tsx b/packages/hub/src/components/ui/ServerActionModalAction.tsx index 11bbc03f7e..97526f5670 100644 --- a/packages/hub/src/components/ui/ServerActionModalAction.tsx +++ b/packages/hub/src/components/ui/ServerActionModalAction.tsx @@ -8,7 +8,7 @@ import { } from "@quri/ui"; import { FormModal } from "@/components/ui/FormModal"; -import { useServerActionForm } from "@/hooks/useServerActionForm"; +import { useServerActionForm } from "@/lib/hooks/useServerActionForm"; type CommonProps< TFormShape extends FieldValues, diff --git a/packages/hub/src/constants.ts b/packages/hub/src/constants.ts deleted file mode 100644 index 9c9ca38036..0000000000 --- a/packages/hub/src/constants.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Don't try to destructure this, `const { NEXT_PUBLIC_FOO } = process.env` won't work correctly. - -// Note that only `NEXT_PUBLIC_*` vars are affected; others can be used through `process.env.FOO` without issues. - -export const VERCEL_URL = process.env["NEXT_PUBLIC_VERCEL_BRANCH_URL"]; - -export const SAMPLE_COUNT_DEFAULT = 1000; -export const XY_POINT_LENGTH_DEFAULT = 1000; - -export const DEFAULT_SEED = "DEFAULT_SEED"; diff --git a/packages/hub/src/server/groups/actions/acceptReusableGroupInviteTokenAction.ts b/packages/hub/src/groups/actions/acceptReusableGroupInviteTokenAction.ts similarity index 80% rename from packages/hub/src/server/groups/actions/acceptReusableGroupInviteTokenAction.ts rename to packages/hub/src/groups/actions/acceptReusableGroupInviteTokenAction.ts index 55975d2671..c51ba2b590 100644 --- a/packages/hub/src/server/groups/actions/acceptReusableGroupInviteTokenAction.ts +++ b/packages/hub/src/groups/actions/acceptReusableGroupInviteTokenAction.ts @@ -3,11 +3,12 @@ import { revalidatePath } from "next/cache"; import { z } from "zod"; -import { prisma } from "@/prisma"; -import { groupMembersRoute } from "@/routes"; -import { getMyMembership } from "@/server/groups/groupHelpers"; -import { getSessionOrRedirect } from "@/server/users/auth"; -import { makeServerAction, zSlug } from "@/server/utils"; +import { getMyMembership } from "@/groups/helpers"; +import { groupMembersRoute } from "@/lib/routes"; +import { prisma } from "@/lib/server/prisma"; +import { makeServerAction } from "@/lib/server/utils"; +import { zSlug } from "@/lib/zodUtils"; +import { getSessionOrRedirect } from "@/users/auth"; import { validateReusableGroupInviteToken } from "../data/helpers"; diff --git a/packages/hub/src/server/groups/actions/addUserToGroupAction.ts b/packages/hub/src/groups/actions/addUserToGroupAction.ts similarity index 92% rename from packages/hub/src/server/groups/actions/addUserToGroupAction.ts rename to packages/hub/src/groups/actions/addUserToGroupAction.ts index 658ab43f1b..4bb1031094 100644 --- a/packages/hub/src/server/groups/actions/addUserToGroupAction.ts +++ b/packages/hub/src/groups/actions/addUserToGroupAction.ts @@ -3,9 +3,10 @@ import { MembershipRole } from "@prisma/client"; import { z } from "zod"; -import { prisma } from "@/prisma"; -import { getSessionOrRedirect } from "@/server/users/auth"; -import { makeServerAction, zSlug } from "@/server/utils"; +import { prisma } from "@/lib/server/prisma"; +import { makeServerAction } from "@/lib/server/utils"; +import { zSlug } from "@/lib/zodUtils"; +import { getSessionOrRedirect } from "@/users/auth"; import { GroupMemberDTO, diff --git a/packages/hub/src/server/groups/actions/createGroupAction.ts b/packages/hub/src/groups/actions/createGroupAction.ts similarity index 76% rename from packages/hub/src/server/groups/actions/createGroupAction.ts rename to packages/hub/src/groups/actions/createGroupAction.ts index 5d5e23fbc4..66326c0073 100644 --- a/packages/hub/src/server/groups/actions/createGroupAction.ts +++ b/packages/hub/src/groups/actions/createGroupAction.ts @@ -1,11 +1,11 @@ "use server"; import { z } from "zod"; -import { prisma } from "@/prisma"; -import { getSessionOrRedirect } from "@/server/users/auth"; -import { makeServerAction, rethrowOnConstraint, zSlug } from "@/server/utils"; - -import { indexGroupId } from "../../search/helpers"; +import { prisma } from "@/lib/server/prisma"; +import { makeServerAction, rethrowOnConstraint } from "@/lib/server/utils"; +import { zSlug } from "@/lib/zodUtils"; +import { indexGroupId } from "@/search/helpers"; +import { getSessionOrRedirect } from "@/users/auth"; export const createGroupAction = makeServerAction( z.object({ diff --git a/packages/hub/src/server/groups/actions/createReusableGroupInviteTokenAction.ts b/packages/hub/src/groups/actions/createReusableGroupInviteTokenAction.ts similarity index 71% rename from packages/hub/src/server/groups/actions/createReusableGroupInviteTokenAction.ts rename to packages/hub/src/groups/actions/createReusableGroupInviteTokenAction.ts index a2b7d6ea72..5cc8855785 100644 --- a/packages/hub/src/server/groups/actions/createReusableGroupInviteTokenAction.ts +++ b/packages/hub/src/groups/actions/createReusableGroupInviteTokenAction.ts @@ -3,12 +3,20 @@ import crypto from "crypto"; import { revalidatePath } from "next/cache"; import { z } from "zod"; -import { prisma } from "@/prisma"; -import { groupMembersRoute } from "@/routes"; -import { makeServerAction, zSlug } from "@/server/utils"; +import { groupMembersRoute } from "@/lib/routes"; +import { prisma } from "@/lib/server/prisma"; +import { makeServerAction } from "@/lib/server/utils"; +import { zSlug } from "@/lib/zodUtils"; import { loadMyMembership } from "../data/members"; +/* + * Create or replace a reusable invite token for a group, available as + * \`reusableInviteToken\` field on group object. + * + * You must be an admin of the group to call this mutation. Previous invite + * token, if it existed, will stop working. + */ export const createReusableGroupInviteTokenAction = makeServerAction( z.object({ slug: zSlug, diff --git a/packages/hub/src/server/groups/actions/deleteMembershipAction.ts b/packages/hub/src/groups/actions/deleteMembershipAction.ts similarity index 82% rename from packages/hub/src/server/groups/actions/deleteMembershipAction.ts rename to packages/hub/src/groups/actions/deleteMembershipAction.ts index 8dacf40688..87c01f8fd6 100644 --- a/packages/hub/src/server/groups/actions/deleteMembershipAction.ts +++ b/packages/hub/src/groups/actions/deleteMembershipAction.ts @@ -3,11 +3,12 @@ import { revalidatePath } from "next/cache"; import { z } from "zod"; -import { prisma } from "@/prisma"; -import { groupMembersRoute } from "@/routes"; -import { getMembership, getMyMembership } from "@/server/groups/groupHelpers"; -import { getSessionOrRedirect } from "@/server/users/auth"; -import { makeServerAction, zSlug } from "@/server/utils"; +import { getMembership, getMyMembership } from "@/groups/helpers"; +import { groupMembersRoute } from "@/lib/routes"; +import { prisma } from "@/lib/server/prisma"; +import { makeServerAction } from "@/lib/server/utils"; +import { zSlug } from "@/lib/zodUtils"; +import { getSessionOrRedirect } from "@/users/auth"; import { groupHasAdminsBesidesUser } from "../data/helpers"; diff --git a/packages/hub/src/server/groups/actions/deleteReusableGroupInviteTokenAction.ts b/packages/hub/src/groups/actions/deleteReusableGroupInviteTokenAction.ts similarity index 70% rename from packages/hub/src/server/groups/actions/deleteReusableGroupInviteTokenAction.ts rename to packages/hub/src/groups/actions/deleteReusableGroupInviteTokenAction.ts index 00bcc6208b..3eb3819b8b 100644 --- a/packages/hub/src/server/groups/actions/deleteReusableGroupInviteTokenAction.ts +++ b/packages/hub/src/groups/actions/deleteReusableGroupInviteTokenAction.ts @@ -2,14 +2,14 @@ import { revalidatePath } from "next/cache"; import { z } from "zod"; -import { prisma } from "@/prisma"; -import { groupMembersRoute } from "@/routes"; -import { makeServerAction, zSlug } from "@/server/utils"; +import { groupMembersRoute } from "@/lib/routes"; +import { prisma } from "@/lib/server/prisma"; +import { makeServerAction } from "@/lib/server/utils"; +import { zSlug } from "@/lib/zodUtils"; import { loadMyMembership } from "../data/members"; -// Create or replace a reusable invite token for a group, available as \`reusableInviteToken\` field on group object. -// You must be an admin of the group to call this action. Previous invite token, if it existed, will stop working. +// Disable a reusable invite token for a group. export const deleteReusableGroupInviteTokenAction = makeServerAction( z.object({ slug: zSlug, diff --git a/packages/hub/src/server/groups/actions/updateMembershipRoleAction.ts b/packages/hub/src/groups/actions/updateMembershipRoleAction.ts similarity index 90% rename from packages/hub/src/server/groups/actions/updateMembershipRoleAction.ts rename to packages/hub/src/groups/actions/updateMembershipRoleAction.ts index 3865ae79da..d094e4adb1 100644 --- a/packages/hub/src/server/groups/actions/updateMembershipRoleAction.ts +++ b/packages/hub/src/groups/actions/updateMembershipRoleAction.ts @@ -3,9 +3,10 @@ import { MembershipRole } from "@prisma/client"; import { z } from "zod"; -import { prisma } from "@/prisma"; -import { getSessionOrRedirect } from "@/server/users/auth"; -import { makeServerAction, zSlug } from "@/server/utils"; +import { prisma } from "@/lib/server/prisma"; +import { makeServerAction } from "@/lib/server/utils"; +import { zSlug } from "@/lib/zodUtils"; +import { getSessionOrRedirect } from "@/users/auth"; import { groupHasAdminsBesidesUser } from "../data/helpers"; import { diff --git a/packages/hub/src/groups/components/GroupCard.tsx b/packages/hub/src/groups/components/GroupCard.tsx index 8a62220037..5330178fad 100644 --- a/packages/hub/src/groups/components/GroupCard.tsx +++ b/packages/hub/src/groups/components/GroupCard.tsx @@ -1,8 +1,8 @@ import { FC } from "react"; import { EntityCard, UpdatedStatus } from "@/components/EntityCard"; -import { groupRoute } from "@/routes"; -import { GroupCardDTO } from "@/server/groups/data/card"; +import { GroupCardDTO } from "@/groups/data/groupCards"; +import { groupRoute } from "@/lib/routes"; type Props = { group: GroupCardDTO; diff --git a/packages/hub/src/groups/components/GroupList.tsx b/packages/hub/src/groups/components/GroupList.tsx index 1a1b9cd1fc..9db4797dc0 100644 --- a/packages/hub/src/groups/components/GroupList.tsx +++ b/packages/hub/src/groups/components/GroupList.tsx @@ -2,9 +2,9 @@ import { FC } from "react"; import { LoadMore } from "@/components/LoadMore"; -import { usePaginator } from "@/hooks/usePaginator"; -import { GroupCardDTO } from "@/server/groups/data/card"; -import { Paginated } from "@/server/types"; +import { GroupCardDTO } from "@/groups/data/groupCards"; +import { usePaginator } from "@/lib/hooks/usePaginator"; +import { Paginated } from "@/lib/types"; import { GroupCard } from "./GroupCard"; diff --git a/packages/hub/src/server/groups/data/card.ts b/packages/hub/src/groups/data/groupCards.ts similarity index 94% rename from packages/hub/src/server/groups/data/card.ts rename to packages/hub/src/groups/data/groupCards.ts index b0ed1129ca..64c39b261a 100644 --- a/packages/hub/src/server/groups/data/card.ts +++ b/packages/hub/src/groups/data/groupCards.ts @@ -1,9 +1,8 @@ import { Prisma } from "@prisma/client"; -import { auth } from "@/auth"; -import { prisma } from "@/prisma"; - -import { Paginated } from "../../types"; +import { auth } from "@/lib/server/auth"; +import { prisma } from "@/lib/server/prisma"; +import { Paginated } from "@/lib/types"; const select = { id: true, diff --git a/packages/hub/src/server/groups/data/helpers.ts b/packages/hub/src/groups/data/helpers.ts similarity index 90% rename from packages/hub/src/server/groups/data/helpers.ts rename to packages/hub/src/groups/data/helpers.ts index 5af5aa04d6..fdd42938f8 100644 --- a/packages/hub/src/server/groups/data/helpers.ts +++ b/packages/hub/src/groups/data/helpers.ts @@ -1,10 +1,10 @@ import { MembershipRole } from "@prisma/client"; -import { auth } from "@/auth"; -import { prisma } from "@/prisma"; -import { getSessionUserOrRedirect } from "@/server/users/auth"; +import { auth } from "@/lib/server/auth"; +import { prisma } from "@/lib/server/prisma"; +import { getSessionUserOrRedirect } from "@/users/auth"; -import { getMyGroup } from "./card"; +import { getMyGroup } from "./groupCards"; export async function hasGroupMembership(groupSlug: string): Promise { // TODO - could be optimized diff --git a/packages/hub/src/server/groups/data/members.ts b/packages/hub/src/groups/data/members.ts similarity index 95% rename from packages/hub/src/server/groups/data/members.ts rename to packages/hub/src/groups/data/members.ts index afaa73b745..bf5ab74eff 100644 --- a/packages/hub/src/server/groups/data/members.ts +++ b/packages/hub/src/groups/data/members.ts @@ -1,8 +1,8 @@ import { MembershipRole, Prisma } from "@prisma/client"; -import { auth } from "@/auth"; -import { prisma } from "@/prisma"; -import { Paginated } from "@/server/types"; +import { auth } from "@/lib/server/auth"; +import { prisma } from "@/lib/server/prisma"; +import { Paginated } from "@/lib/types"; export type GroupMemberDTO = { id: string; diff --git a/packages/hub/src/server/groups/groupHelpers.ts b/packages/hub/src/groups/helpers.ts similarity index 90% rename from packages/hub/src/server/groups/groupHelpers.ts rename to packages/hub/src/groups/helpers.ts index 70c8b76a34..ef4d26d336 100644 --- a/packages/hub/src/server/groups/groupHelpers.ts +++ b/packages/hub/src/groups/helpers.ts @@ -1,5 +1,5 @@ -import { auth } from "@/auth"; -import { prisma } from "@/prisma"; +import { auth } from "@/lib/server/auth"; +import { prisma } from "@/lib/server/prisma"; import { isSignedIn } from "../users/auth"; diff --git a/packages/hub/src/lib/common.ts b/packages/hub/src/lib/constants.ts similarity index 54% rename from packages/hub/src/lib/common.ts rename to packages/hub/src/lib/constants.ts index 2023ae8c9e..24c081347c 100644 --- a/packages/hub/src/lib/common.ts +++ b/packages/hub/src/lib/constants.ts @@ -1,6 +1,7 @@ // for format() from date-fns export const commonDateFormat = "MMM d yyyy, H:mm"; +// Footer links, etc. export const DISCORD_URL = "https://discord.gg/nsTnQTgtG6"; export const GITHUB_URL = "https://github.com/quantified-uncertainty/squiggle"; export const GITHUB_DISCUSSION_URL = @@ -10,3 +11,13 @@ export const NEWSLETTER_URL = "https://quri.substack.com/t/squiggle"; export const QURI_DONATE_URL = "https://quantifieduncertainty.org/donate"; export const SQUIGGLE_DOCS_URL = "https://www.squiggle-language.com/docs/Api/Dist"; + +// Don't try to destructure this, `const { NEXT_PUBLIC_FOO } = process.env` won't work correctly. +// Note that only `NEXT_PUBLIC_*` vars are affected; others can be used through `process.env.FOO` without issues. +export const VERCEL_URL = process.env["NEXT_PUBLIC_VERCEL_BRANCH_URL"]; + +// Squiggle defaults +export const SAMPLE_COUNT_DEFAULT = 1000; +export const XY_POINT_LENGTH_DEFAULT = 1000; + +export const DEFAULT_SEED = "DEFAULT_SEED"; diff --git a/packages/hub/src/lib/graphqlHelpers.ts b/packages/hub/src/lib/graphqlHelpers.ts deleted file mode 100644 index 83486d26f8..0000000000 --- a/packages/hub/src/lib/graphqlHelpers.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { notFound } from "next/navigation"; - -type ErrorUnionNode = - | { - readonly __typename: "BaseError"; - readonly message: string; - } - | { - readonly __typename: "NotFoundError"; - readonly message: string; - } - | { - readonly __typename: "%other"; - } - | ({ - readonly __typename: OkTypename; - } & Content) - | null; // useful when node is obtained through top-level `node { ... }` - -/* - * This function will render 404 on NotFoundError and throw on other errors; - * Otherwise, it will extract the value for a given typename. - * - * Note: implementation is not very type-safe. - * Maybe because `OkTypename` can overlap with other `__typename` values? - * I tried to do `OkTypename extends "BaseError" ? never : OkTypename`, but it didn't help. - */ -export function extractFromGraphqlErrorUnion< - OkTypename extends string, - Content, ->(node: ErrorUnionNode, typename: OkTypename) { - if (node === null || node.__typename === "NotFoundError") { - notFound(); - } - if (node.__typename === "BaseError") { - throw new Error( - // somehow Typescript fails to infer this - ( - node as { readonly __typename: "BaseError"; readonly message: string } - ).message - ); - } - if (node.__typename !== typename) { - throw new Error("Unexpected typename"); - } - - return node as { readonly __typename: OkTypename } & Content; -} diff --git a/packages/hub/src/hooks/useAvailableHeight.ts b/packages/hub/src/lib/hooks/useAvailableHeight.ts similarity index 100% rename from packages/hub/src/hooks/useAvailableHeight.ts rename to packages/hub/src/lib/hooks/useAvailableHeight.ts diff --git a/packages/hub/src/hooks/useClientOnlyRender.ts b/packages/hub/src/lib/hooks/useClientOnlyRender.ts similarity index 100% rename from packages/hub/src/hooks/useClientOnlyRender.ts rename to packages/hub/src/lib/hooks/useClientOnlyRender.ts diff --git a/packages/hub/src/hooks/usePaginator.ts b/packages/hub/src/lib/hooks/usePaginator.ts similarity index 97% rename from packages/hub/src/hooks/usePaginator.ts rename to packages/hub/src/lib/hooks/usePaginator.ts index e04ff3d9b6..18bf1f5403 100644 --- a/packages/hub/src/hooks/usePaginator.ts +++ b/packages/hub/src/lib/hooks/usePaginator.ts @@ -1,6 +1,6 @@ import { useCallback, useState } from "react"; -import { Paginated } from "@/server/types"; +import { Paginated } from "@/lib/types"; type FullPaginated = { items: T[]; diff --git a/packages/hub/src/hooks/useServerActionForm.ts b/packages/hub/src/lib/hooks/useServerActionForm.ts similarity index 100% rename from packages/hub/src/hooks/useServerActionForm.ts rename to packages/hub/src/lib/hooks/useServerActionForm.ts diff --git a/packages/hub/src/hooks/useUpdateSearchParams.ts b/packages/hub/src/lib/hooks/useUpdateSearchParams.ts similarity index 100% rename from packages/hub/src/hooks/useUpdateSearchParams.ts rename to packages/hub/src/lib/hooks/useUpdateSearchParams.ts diff --git a/packages/hub/src/routes.ts b/packages/hub/src/lib/routes.ts similarity index 100% rename from packages/hub/src/routes.ts rename to packages/hub/src/lib/routes.ts diff --git a/packages/hub/src/auth.ts b/packages/hub/src/lib/server/auth.ts similarity index 95% rename from packages/hub/src/auth.ts rename to packages/hub/src/lib/server/auth.ts index 34bc300d39..2036be7e9e 100644 --- a/packages/hub/src/auth.ts +++ b/packages/hub/src/lib/server/auth.ts @@ -7,8 +7,8 @@ import GithubProvider from "next-auth/providers/github"; import { Provider } from "next-auth/providers/index"; import { cache } from "react"; -import { prisma } from "@/prisma"; -import { indexUserId } from "@/server/search/helpers"; +import { prisma } from "@/lib/server/prisma"; +import { indexUserId } from "@/search/helpers"; function buildAuthConfig(): NextAuthConfig { const providers: Provider[] = []; diff --git a/packages/hub/src/server/globalStatistics.ts b/packages/hub/src/lib/server/globalStatistics.ts similarity index 88% rename from packages/hub/src/server/globalStatistics.ts rename to packages/hub/src/lib/server/globalStatistics.ts index 93ba77ea37..95fcff7094 100644 --- a/packages/hub/src/server/globalStatistics.ts +++ b/packages/hub/src/lib/server/globalStatistics.ts @@ -1,4 +1,4 @@ -import { prisma } from "@/prisma"; +import { prisma } from "@/lib/server/prisma"; export async function getGlobalStatistics() { const userCount = await prisma.user.count(); diff --git a/packages/hub/src/prisma.ts b/packages/hub/src/lib/server/prisma.ts similarity index 100% rename from packages/hub/src/prisma.ts rename to packages/hub/src/lib/server/prisma.ts diff --git a/packages/hub/src/server/runSquiggle.ts b/packages/hub/src/lib/server/runSquiggle.ts similarity index 98% rename from packages/hub/src/server/runSquiggle.ts rename to packages/hub/src/lib/server/runSquiggle.ts index 88678bd128..bc726769d0 100644 --- a/packages/hub/src/server/runSquiggle.ts +++ b/packages/hub/src/lib/server/runSquiggle.ts @@ -9,8 +9,8 @@ import { SqValue, } from "@quri/squiggle-lang"; -import { SAMPLE_COUNT_DEFAULT, XY_POINT_LENGTH_DEFAULT } from "@/constants"; -import { prisma } from "@/prisma"; +import { SAMPLE_COUNT_DEFAULT, XY_POINT_LENGTH_DEFAULT } from "@/lib/constants"; +import { prisma } from "@/lib/server/prisma"; import { parseSourceId } from "@/squiggle/components/linker"; function getKey(code: string, seed: string): string { diff --git a/packages/hub/src/server/utils.ts b/packages/hub/src/lib/server/utils.ts similarity index 86% rename from packages/hub/src/server/utils.ts rename to packages/hub/src/lib/server/utils.ts index a4cca688e9..17af20a73a 100644 --- a/packages/hub/src/server/utils.ts +++ b/packages/hub/src/lib/server/utils.ts @@ -1,14 +1,6 @@ import { Prisma } from "@prisma/client"; import { z } from "zod"; -export const zSlug = z.string().regex(/^\w[\w\-]*$/, { - message: "Must be alphanumerical", -}); - -export const zColor = z.string().regex(/^#[0-9a-fA-F]{6}$/, { - message: "Must be a valid hex color", -}); - export type DeepReadonly = T extends (infer R)[] ? DeepReadonlyArray : T extends object diff --git a/packages/hub/src/server/types.ts b/packages/hub/src/lib/types.ts similarity index 100% rename from packages/hub/src/server/types.ts rename to packages/hub/src/lib/types.ts diff --git a/packages/hub/src/lib/zodUtils.ts b/packages/hub/src/lib/zodUtils.ts index 1f6958a75d..c61447b7d6 100644 --- a/packages/hub/src/lib/zodUtils.ts +++ b/packages/hub/src/lib/zodUtils.ts @@ -16,3 +16,11 @@ export const numberInString = z.string().transform((val, ctx) => { } return parsed; }); + +export const zSlug = z.string().regex(/^\w[\w\-]*$/, { + message: "Must be alphanumerical", +}); + +export const zColor = z.string().regex(/^#[0-9a-fA-F]{6}$/, { + message: "Must be a valid hex color", +}); diff --git a/packages/hub/src/migrations/20241012155427_workflow_format.ts b/packages/hub/src/migrations/20241012155427_workflow_format.ts index b3e2daf6c7..36691649c1 100644 --- a/packages/hub/src/migrations/20241012155427_workflow_format.ts +++ b/packages/hub/src/migrations/20241012155427_workflow_format.ts @@ -1,4 +1,4 @@ -import { prisma } from "@/prisma"; +import { prisma } from "@/lib/server/prisma"; export async function migrate() { const v1Workflows = await prisma.aiWorkflow.findMany({ diff --git a/packages/hub/src/server/models/actions/adminUpdateModelVersionAction.ts b/packages/hub/src/models/actions/adminUpdateModelVersionAction.ts similarity index 92% rename from packages/hub/src/server/models/actions/adminUpdateModelVersionAction.ts rename to packages/hub/src/models/actions/adminUpdateModelVersionAction.ts index b3da993f11..dec6aeb4e8 100644 --- a/packages/hub/src/server/models/actions/adminUpdateModelVersionAction.ts +++ b/packages/hub/src/models/actions/adminUpdateModelVersionAction.ts @@ -1,13 +1,9 @@ "use server"; import { z } from "zod"; -import { prisma } from "@/prisma"; -import { - checkRootUser, - getSelf, - getSessionOrRedirect, -} from "@/server/users/auth"; -import { makeServerAction } from "@/server/utils"; +import { prisma } from "@/lib/server/prisma"; +import { makeServerAction } from "@/lib/server/utils"; +import { checkRootUser, getSelf, getSessionOrRedirect } from "@/users/auth"; // Admin-only query for upgrading model versions export const adminUpdateModelVersionAction = makeServerAction( diff --git a/packages/hub/src/server/models/actions/createSquiggleSnippetModelAction.ts b/packages/hub/src/models/actions/createSquiggleSnippetModelAction.ts similarity index 86% rename from packages/hub/src/server/models/actions/createSquiggleSnippetModelAction.ts rename to packages/hub/src/models/actions/createSquiggleSnippetModelAction.ts index 7411b20b92..8fb68e8923 100644 --- a/packages/hub/src/server/models/actions/createSquiggleSnippetModelAction.ts +++ b/packages/hub/src/models/actions/createSquiggleSnippetModelAction.ts @@ -2,11 +2,12 @@ import { z } from "zod"; -import { prisma } from "@/prisma"; -import { getWriteableOwner } from "@/server/owners/auth"; -import { indexModelId } from "@/server/search/helpers"; -import { getSelf, getSessionOrRedirect } from "@/server/users/auth"; -import { makeServerAction, rethrowOnConstraint, zSlug } from "@/server/utils"; +import { prisma } from "@/lib/server/prisma"; +import { makeServerAction, rethrowOnConstraint } from "@/lib/server/utils"; +import { zSlug } from "@/lib/zodUtils"; +import { getWriteableOwner } from "@/owners/data/auth"; +import { indexModelId } from "@/search/helpers"; +import { getSelf, getSessionOrRedirect } from "@/users/auth"; export const createSquiggleSnippetModelAction = makeServerAction( z.object({ diff --git a/packages/hub/src/server/models/actions/deleteModelAction.ts b/packages/hub/src/models/actions/deleteModelAction.ts similarity index 62% rename from packages/hub/src/server/models/actions/deleteModelAction.ts rename to packages/hub/src/models/actions/deleteModelAction.ts index e218090b02..e184f36bc6 100644 --- a/packages/hub/src/server/models/actions/deleteModelAction.ts +++ b/packages/hub/src/models/actions/deleteModelAction.ts @@ -2,10 +2,11 @@ import { z } from "zod"; -import { getWriteableModel } from "@/graphql/helpers/modelHelpers"; -import { prisma } from "@/prisma"; -import { getSessionOrRedirect } from "@/server/users/auth"; -import { makeServerAction, zSlug } from "@/server/utils"; +import { prisma } from "@/lib/server/prisma"; +import { makeServerAction } from "@/lib/server/utils"; +import { zSlug } from "@/lib/zodUtils"; +import { getWriteableModel } from "@/models/utils"; +import { getSessionOrRedirect } from "@/users/auth"; export const deleteModelAction = makeServerAction( z.object({ diff --git a/packages/hub/src/server/models/actions/loadModelCardAction.ts b/packages/hub/src/models/actions/loadModelCardAction.ts similarity index 82% rename from packages/hub/src/server/models/actions/loadModelCardAction.ts rename to packages/hub/src/models/actions/loadModelCardAction.ts index 108ff8395f..fad32f5922 100644 --- a/packages/hub/src/server/models/actions/loadModelCardAction.ts +++ b/packages/hub/src/models/actions/loadModelCardAction.ts @@ -1,7 +1,8 @@ "use server"; import { z } from "zod"; -import { makeServerAction, zSlug } from "@/server/utils"; +import { makeServerAction } from "@/lib/server/utils"; +import { zSlug } from "@/lib/zodUtils"; import { loadModelCard, ModelCardDTO } from "../data/cards"; diff --git a/packages/hub/src/server/models/actions/loadModelFullAction.ts b/packages/hub/src/models/actions/loadModelFullAction.ts similarity index 83% rename from packages/hub/src/server/models/actions/loadModelFullAction.ts rename to packages/hub/src/models/actions/loadModelFullAction.ts index 2b4a0a57ed..d476bae0ba 100644 --- a/packages/hub/src/server/models/actions/loadModelFullAction.ts +++ b/packages/hub/src/models/actions/loadModelFullAction.ts @@ -1,7 +1,8 @@ "use server"; import { z } from "zod"; -import { makeServerAction, zSlug } from "@/server/utils"; +import { makeServerAction } from "@/lib/server/utils"; +import { zSlug } from "@/lib/zodUtils"; import { loadModelFull, ModelFullDTO } from "../data/full"; diff --git a/packages/hub/src/server/models/actions/moveModelAction.ts b/packages/hub/src/models/actions/moveModelAction.ts similarity index 68% rename from packages/hub/src/server/models/actions/moveModelAction.ts rename to packages/hub/src/models/actions/moveModelAction.ts index 893da4898c..b140b73290 100644 --- a/packages/hub/src/server/models/actions/moveModelAction.ts +++ b/packages/hub/src/models/actions/moveModelAction.ts @@ -2,11 +2,12 @@ import { z } from "zod"; -import { getWriteableModel } from "@/graphql/helpers/modelHelpers"; -import { prisma } from "@/prisma"; -import { getWriteableOwnerBySlug } from "@/server/owners/auth"; -import { getSessionOrRedirect } from "@/server/users/auth"; -import { makeServerAction, zSlug } from "@/server/utils"; +import { prisma } from "@/lib/server/prisma"; +import { makeServerAction } from "@/lib/server/utils"; +import { zSlug } from "@/lib/zodUtils"; +import { getWriteableModel } from "@/models/utils"; +import { getWriteableOwnerBySlug } from "@/owners/data/auth"; +import { getSessionOrRedirect } from "@/users/auth"; export const moveModelAction = makeServerAction( z.object({ diff --git a/packages/hub/src/server/models/actions/updateModelPrivacyAction.ts b/packages/hub/src/models/actions/updateModelPrivacyAction.ts similarity index 71% rename from packages/hub/src/server/models/actions/updateModelPrivacyAction.ts rename to packages/hub/src/models/actions/updateModelPrivacyAction.ts index 4795a7394d..cacfa2a086 100644 --- a/packages/hub/src/server/models/actions/updateModelPrivacyAction.ts +++ b/packages/hub/src/models/actions/updateModelPrivacyAction.ts @@ -3,11 +3,12 @@ import { revalidatePath } from "next/cache"; import { z } from "zod"; -import { getWriteableModel } from "@/graphql/helpers/modelHelpers"; -import { prisma } from "@/prisma"; -import { modelRoute } from "@/routes"; -import { getSessionOrRedirect } from "@/server/users/auth"; -import { makeServerAction, zSlug } from "@/server/utils"; +import { modelRoute } from "@/lib/routes"; +import { prisma } from "@/lib/server/prisma"; +import { makeServerAction } from "@/lib/server/utils"; +import { zSlug } from "@/lib/zodUtils"; +import { getWriteableModel } from "@/models/utils"; +import { getSessionOrRedirect } from "@/users/auth"; export const updateModelPrivacyAction = makeServerAction( z.object({ diff --git a/packages/hub/src/server/models/actions/updateModelSlugAction.ts b/packages/hub/src/models/actions/updateModelSlugAction.ts similarity index 70% rename from packages/hub/src/server/models/actions/updateModelSlugAction.ts rename to packages/hub/src/models/actions/updateModelSlugAction.ts index 12ff1b9de6..d2f8a5b69d 100644 --- a/packages/hub/src/server/models/actions/updateModelSlugAction.ts +++ b/packages/hub/src/models/actions/updateModelSlugAction.ts @@ -2,10 +2,11 @@ import { z } from "zod"; -import { getWriteableModel } from "@/graphql/helpers/modelHelpers"; -import { prisma } from "@/prisma"; -import { getSessionOrRedirect } from "@/server/users/auth"; -import { makeServerAction, zSlug } from "@/server/utils"; +import { prisma } from "@/lib/server/prisma"; +import { makeServerAction } from "@/lib/server/utils"; +import { zSlug } from "@/lib/zodUtils"; +import { getWriteableModel } from "@/models/utils"; +import { getSessionOrRedirect } from "@/users/auth"; export const updateModelSlugAction = makeServerAction( z.object({ diff --git a/packages/hub/src/server/models/actions/updateSquiggleSnippetModelAction.ts b/packages/hub/src/models/actions/updateSquiggleSnippetModelAction.ts similarity index 93% rename from packages/hub/src/server/models/actions/updateSquiggleSnippetModelAction.ts rename to packages/hub/src/models/actions/updateSquiggleSnippetModelAction.ts index a598f65754..a830643462 100644 --- a/packages/hub/src/server/models/actions/updateSquiggleSnippetModelAction.ts +++ b/packages/hub/src/models/actions/updateSquiggleSnippetModelAction.ts @@ -5,12 +5,13 @@ import { z } from "zod"; import { squiggleVersions } from "@quri/versioned-squiggle-components"; -import { prisma } from "@/prisma"; -import { modelRoute } from "@/routes"; -import { getSelf, getSessionOrRedirect } from "@/server/users/auth"; -import { makeServerAction, zSlug } from "@/server/utils"; +import { modelRoute } from "@/lib/routes"; +import { prisma } from "@/lib/server/prisma"; +import { makeServerAction } from "@/lib/server/utils"; +import { zSlug } from "@/lib/zodUtils"; +import { getSelf, getSessionOrRedirect } from "@/users/auth"; -import { getWriteableModel } from "../../../graphql/helpers/modelHelpers"; +import { getWriteableModel } from "../utils"; export const updateSquiggleSnippetModelAction = makeServerAction( z.object({ diff --git a/packages/hub/src/server/models/utils.ts b/packages/hub/src/models/clientUtils.ts similarity index 100% rename from packages/hub/src/server/models/utils.ts rename to packages/hub/src/models/clientUtils.ts diff --git a/packages/hub/src/models/components/ModelCard.tsx b/packages/hub/src/models/components/ModelCard.tsx index a7ad20bf2c..97d1fa59f8 100644 --- a/packages/hub/src/models/components/ModelCard.tsx +++ b/packages/hub/src/models/components/ModelCard.tsx @@ -11,13 +11,13 @@ import { UpdatedStatus, } from "@/components/EntityCard"; import { Link } from "@/components/ui/Link"; +import { modelRoute, ownerRoute } from "@/lib/routes"; +import { ModelCardDTO } from "@/models/data/cards"; import { totalImportLength, VariableRevision, VariablesDropdown, -} from "@/lib/VariablesDropdown"; -import { modelRoute, ownerRoute } from "@/routes"; -import { ModelCardDTO } from "@/server/models/data/cards"; +} from "@/variables/components/VariablesDropdown"; type Props = { model: ModelCardDTO; diff --git a/packages/hub/src/models/components/ModelList.tsx b/packages/hub/src/models/components/ModelList.tsx index aa50a5fe6d..891734166f 100644 --- a/packages/hub/src/models/components/ModelList.tsx +++ b/packages/hub/src/models/components/ModelList.tsx @@ -2,9 +2,9 @@ import { FC } from "react"; import { LoadMore } from "@/components/LoadMore"; -import { usePaginator } from "@/hooks/usePaginator"; -import { ModelCardDTO } from "@/server/models/data/cards"; -import { Paginated } from "@/server/types"; +import { usePaginator } from "@/lib/hooks/usePaginator"; +import { Paginated } from "@/lib/types"; +import { ModelCardDTO } from "@/models/data/cards"; import { ModelCard } from "./ModelCard"; diff --git a/packages/hub/src/server/models/data/authHelpers.ts b/packages/hub/src/models/data/authHelpers.ts similarity index 93% rename from packages/hub/src/server/models/data/authHelpers.ts rename to packages/hub/src/models/data/authHelpers.ts index 54c7fa85f3..028f33bfe8 100644 --- a/packages/hub/src/server/models/data/authHelpers.ts +++ b/packages/hub/src/models/data/authHelpers.ts @@ -1,6 +1,6 @@ import { Prisma } from "@prisma/client"; -import { auth } from "@/auth"; +import { auth } from "@/lib/server/auth"; export async function modelWhereHasAccess(): Promise { const session = await auth(); diff --git a/packages/hub/src/server/models/data/byVersion.ts b/packages/hub/src/models/data/byVersion.ts similarity index 93% rename from packages/hub/src/server/models/data/byVersion.ts rename to packages/hub/src/models/data/byVersion.ts index bf530099a6..d365cda0c8 100644 --- a/packages/hub/src/server/models/data/byVersion.ts +++ b/packages/hub/src/models/data/byVersion.ts @@ -1,5 +1,5 @@ -import { prisma } from "@/prisma"; -import { checkRootUser } from "@/server/users/auth"; +import { prisma } from "@/lib/server/prisma"; +import { checkRootUser } from "@/users/auth"; // Admin-only, for listing models in /admin UI export async function loadModelsByVersion() { diff --git a/packages/hub/src/server/models/data/cards.ts b/packages/hub/src/models/data/cards.ts similarity index 94% rename from packages/hub/src/server/models/data/cards.ts rename to packages/hub/src/models/data/cards.ts index 1bcf6bdd04..ddcca98a8a 100644 --- a/packages/hub/src/server/models/data/cards.ts +++ b/packages/hub/src/models/data/cards.ts @@ -1,12 +1,9 @@ import { Prisma } from "@prisma/client"; -import { prisma } from "@/prisma"; -import { - selectTypedOwner, - toTypedOwnerDTO, -} from "@/server/owners/data/typedOwner"; +import { prisma } from "@/lib/server/prisma"; +import { Paginated } from "@/lib/types"; +import { selectTypedOwner, toTypedOwnerDTO } from "@/owners/data/typedOwner"; -import { Paginated } from "../../types"; import { modelWhereHasAccess } from "./authHelpers"; // FIXME - explicit ModelCardDTO diff --git a/packages/hub/src/server/models/data/full.ts b/packages/hub/src/models/data/full.ts similarity index 95% rename from packages/hub/src/server/models/data/full.ts rename to packages/hub/src/models/data/full.ts index d22fdac4d7..ec0503121a 100644 --- a/packages/hub/src/server/models/data/full.ts +++ b/packages/hub/src/models/data/full.ts @@ -1,7 +1,7 @@ import { Prisma } from "@prisma/client"; -import { prisma } from "@/prisma"; -import { controlsOwnerId } from "@/server/owners/auth"; +import { prisma } from "@/lib/server/prisma"; +import { controlsOwnerId } from "@/owners/data/auth"; import { modelWhereHasAccess } from "./authHelpers"; import { diff --git a/packages/hub/src/server/models/data/fullRevision.ts b/packages/hub/src/models/data/fullRevision.ts similarity index 97% rename from packages/hub/src/server/models/data/fullRevision.ts rename to packages/hub/src/models/data/fullRevision.ts index e0ed8504a2..10db8a86f0 100644 --- a/packages/hub/src/server/models/data/fullRevision.ts +++ b/packages/hub/src/models/data/fullRevision.ts @@ -1,6 +1,6 @@ import { Prisma } from "@prisma/client"; -import { prisma } from "@/prisma"; +import { prisma } from "@/lib/server/prisma"; import { modelWhereHasAccess } from "./authHelpers"; diff --git a/packages/hub/src/server/models/data/helpers.ts b/packages/hub/src/models/data/helpers.ts similarity index 74% rename from packages/hub/src/server/models/data/helpers.ts rename to packages/hub/src/models/data/helpers.ts index 18d4127758..2f12804af9 100644 --- a/packages/hub/src/server/models/data/helpers.ts +++ b/packages/hub/src/models/data/helpers.ts @@ -1,4 +1,4 @@ -import { controlsOwnerId } from "@/server/owners/auth"; +import { controlsOwnerId } from "@/owners/data/auth"; import { ModelCardDTO } from "./cards"; diff --git a/packages/hub/src/server/models/data/revisions.ts b/packages/hub/src/models/data/revisions.ts similarity index 97% rename from packages/hub/src/server/models/data/revisions.ts rename to packages/hub/src/models/data/revisions.ts index fb9b1a7018..c52c0f2da3 100644 --- a/packages/hub/src/server/models/data/revisions.ts +++ b/packages/hub/src/models/data/revisions.ts @@ -1,7 +1,7 @@ import { Prisma } from "@prisma/client"; -import { prisma } from "@/prisma"; -import { Paginated } from "@/server/types"; +import { prisma } from "@/lib/server/prisma"; +import { Paginated } from "@/lib/types"; import { modelWhereHasAccess } from "./authHelpers"; diff --git a/packages/hub/src/graphql/helpers/modelHelpers.ts b/packages/hub/src/models/utils.ts similarity index 95% rename from packages/hub/src/graphql/helpers/modelHelpers.ts rename to packages/hub/src/models/utils.ts index df4111b345..13a5e5c788 100644 --- a/packages/hub/src/graphql/helpers/modelHelpers.ts +++ b/packages/hub/src/models/utils.ts @@ -1,7 +1,7 @@ import { Model, Prisma } from "@prisma/client"; import { Session } from "next-auth"; -import { prisma } from "@/prisma"; +import { prisma } from "@/lib/server/prisma"; export async function getWriteableModel({ session, diff --git a/packages/hub/src/server/owners/auth.ts b/packages/hub/src/owners/data/auth.ts similarity index 95% rename from packages/hub/src/server/owners/auth.ts rename to packages/hub/src/owners/data/auth.ts index 9ad4872d0f..ff26da93d1 100644 --- a/packages/hub/src/server/owners/auth.ts +++ b/packages/hub/src/owners/data/auth.ts @@ -1,7 +1,7 @@ import { Session } from "next-auth"; -import { auth } from "@/auth"; -import { prisma } from "@/prisma"; +import { auth } from "@/lib/server/auth"; +import { prisma } from "@/lib/server/prisma"; export async function controlsOwnerId(ownerId: string): Promise { const session = await auth(); diff --git a/packages/hub/src/server/owners/data/findOwners.ts b/packages/hub/src/owners/data/findOwners.ts similarity index 96% rename from packages/hub/src/server/owners/data/findOwners.ts rename to packages/hub/src/owners/data/findOwners.ts index 061966ecc9..785b14ccc6 100644 --- a/packages/hub/src/server/owners/data/findOwners.ts +++ b/packages/hub/src/owners/data/findOwners.ts @@ -1,5 +1,5 @@ -import { auth } from "@/auth"; -import { prisma } from "@/prisma"; +import { auth } from "@/lib/server/auth"; +import { prisma } from "@/lib/server/prisma"; type OwnerForSelect = { __typename: "User" | "Group"; diff --git a/packages/hub/src/server/owners/data/typedOwner.ts b/packages/hub/src/owners/data/typedOwner.ts similarity index 93% rename from packages/hub/src/server/owners/data/typedOwner.ts rename to packages/hub/src/owners/data/typedOwner.ts index 2dcb22e6d2..75332616e7 100644 --- a/packages/hub/src/server/owners/data/typedOwner.ts +++ b/packages/hub/src/owners/data/typedOwner.ts @@ -1,6 +1,6 @@ import { Prisma } from "@prisma/client"; -import { prisma } from "@/prisma"; +import { prisma } from "@/lib/server/prisma"; export type TypedOwner = { __typename: "User" | "Group"; diff --git a/packages/hub/src/server/relative-values/actions/buildRelativeValuesCacheAction.ts b/packages/hub/src/relative-values/actions/buildRelativeValuesCacheAction.ts similarity index 93% rename from packages/hub/src/server/relative-values/actions/buildRelativeValuesCacheAction.ts rename to packages/hub/src/relative-values/actions/buildRelativeValuesCacheAction.ts index 3ec15d2acf..e0204a311b 100644 --- a/packages/hub/src/server/relative-values/actions/buildRelativeValuesCacheAction.ts +++ b/packages/hub/src/relative-values/actions/buildRelativeValuesCacheAction.ts @@ -2,13 +2,13 @@ import { revalidatePath } from "next/cache"; import { z } from "zod"; -import { prisma } from "@/prisma"; +import { modelForRelativeValuesExportRoute } from "@/lib/routes"; +import { prisma } from "@/lib/server/prisma"; +import { makeServerAction } from "@/lib/server/utils"; import { cartesianProduct } from "@/relative-values/lib/utils"; import { relativeValuesItemsSchema } from "@/relative-values/types"; import { ModelEvaluator } from "@/relative-values/values/ModelEvaluator"; -import { modelForRelativeValuesExportRoute } from "@/routes"; -import { getSessionOrRedirect } from "@/server/users/auth"; -import { makeServerAction } from "@/server/utils"; +import { getSessionOrRedirect } from "@/users/auth"; import { getRelativeValuesExportForWriteableModel } from "../utils"; diff --git a/packages/hub/src/server/relative-values/actions/clearRelativeValuesCacheAction.ts b/packages/hub/src/relative-values/actions/clearRelativeValuesCacheAction.ts similarity index 80% rename from packages/hub/src/server/relative-values/actions/clearRelativeValuesCacheAction.ts rename to packages/hub/src/relative-values/actions/clearRelativeValuesCacheAction.ts index 66171fbb21..d4ca88d447 100644 --- a/packages/hub/src/server/relative-values/actions/clearRelativeValuesCacheAction.ts +++ b/packages/hub/src/relative-values/actions/clearRelativeValuesCacheAction.ts @@ -2,12 +2,11 @@ import { revalidatePath } from "next/cache"; import { z } from "zod"; -import { prisma } from "@/prisma"; -import { modelForRelativeValuesExportRoute } from "@/routes"; - -import { getSessionOrRedirect } from "../../users/auth"; -import { makeServerAction } from "../../utils"; -import { getRelativeValuesExportForWriteableModel } from "../utils"; +import { modelForRelativeValuesExportRoute } from "@/lib/routes"; +import { prisma } from "@/lib/server/prisma"; +import { makeServerAction } from "@/lib/server/utils"; +import { getRelativeValuesExportForWriteableModel } from "@/relative-values/utils"; +import { getSessionOrRedirect } from "@/users/auth"; export const clearRelativeValuesCacheAction = makeServerAction( z.object({ diff --git a/packages/hub/src/server/relative-values/actions/common.ts b/packages/hub/src/relative-values/actions/common.ts similarity index 96% rename from packages/hub/src/server/relative-values/actions/common.ts rename to packages/hub/src/relative-values/actions/common.ts index 4f776c6bee..3f900dd0d9 100644 --- a/packages/hub/src/server/relative-values/actions/common.ts +++ b/packages/hub/src/relative-values/actions/common.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { zColor, zSlug } from "@/server/utils"; +import { zColor, zSlug } from "@/lib/zodUtils"; // Appropriate both for create and update actions. export const inputSchema = z.object({ diff --git a/packages/hub/src/server/relative-values/actions/createRelativeValuesDefinitionAction.ts b/packages/hub/src/relative-values/actions/createRelativeValuesDefinitionAction.ts similarity index 88% rename from packages/hub/src/server/relative-values/actions/createRelativeValuesDefinitionAction.ts rename to packages/hub/src/relative-values/actions/createRelativeValuesDefinitionAction.ts index 9c342dbf57..b9a65c7fcb 100644 --- a/packages/hub/src/server/relative-values/actions/createRelativeValuesDefinitionAction.ts +++ b/packages/hub/src/relative-values/actions/createRelativeValuesDefinitionAction.ts @@ -1,10 +1,10 @@ "use server"; -import { prisma } from "@/prisma"; -import { getWriteableOwnerBySlug } from "@/server/owners/auth"; -import { indexDefinitionId } from "@/server/search/helpers"; -import { getSessionOrRedirect } from "@/server/users/auth"; -import { makeServerAction, rethrowOnConstraint } from "@/server/utils"; +import { prisma } from "@/lib/server/prisma"; +import { makeServerAction, rethrowOnConstraint } from "@/lib/server/utils"; +import { getWriteableOwnerBySlug } from "@/owners/data/auth"; +import { indexDefinitionId } from "@/search/helpers"; +import { getSessionOrRedirect } from "@/users/auth"; import { inputSchema, validateRelativeValuesDefinition } from "./common"; diff --git a/packages/hub/src/server/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx b/packages/hub/src/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx similarity index 65% rename from packages/hub/src/server/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx rename to packages/hub/src/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx index 00005c768f..037c0f4c83 100644 --- a/packages/hub/src/server/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx +++ b/packages/hub/src/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx @@ -1,10 +1,11 @@ "use server"; import { z } from "zod"; -import { prisma } from "@/prisma"; -import { getWriteableOwnerBySlug } from "@/server/owners/auth"; -import { getSessionOrRedirect } from "@/server/users/auth"; -import { makeServerAction, zSlug } from "@/server/utils"; +import { prisma } from "@/lib/server/prisma"; +import { makeServerAction } from "@/lib/server/utils"; +import { zSlug } from "@/lib/zodUtils"; +import { getWriteableOwnerBySlug } from "@/owners/data/auth"; +import { getSessionOrRedirect } from "@/users/auth"; export const deleteRelativeValuesDefinitionAction = makeServerAction( z.object({ diff --git a/packages/hub/src/server/relative-values/actions/updateRelativeValuesDefinitionAction.ts b/packages/hub/src/relative-values/actions/updateRelativeValuesDefinitionAction.ts similarity index 89% rename from packages/hub/src/server/relative-values/actions/updateRelativeValuesDefinitionAction.ts rename to packages/hub/src/relative-values/actions/updateRelativeValuesDefinitionAction.ts index 6ae19827db..f62013f273 100644 --- a/packages/hub/src/server/relative-values/actions/updateRelativeValuesDefinitionAction.ts +++ b/packages/hub/src/relative-values/actions/updateRelativeValuesDefinitionAction.ts @@ -1,8 +1,8 @@ "use server"; -import { prisma } from "@/prisma"; -import { getWriteableOwnerBySlug } from "@/server/owners/auth"; -import { getSessionOrRedirect } from "@/server/users/auth"; -import { makeServerAction } from "@/server/utils"; +import { prisma } from "@/lib/server/prisma"; +import { makeServerAction } from "@/lib/server/utils"; +import { getWriteableOwnerBySlug } from "@/owners/data/auth"; +import { getSessionOrRedirect } from "@/users/auth"; import { inputSchema, validateRelativeValuesDefinition } from "./common"; diff --git a/packages/hub/src/relative-values/components/RelativeValuesDefinitionCard.tsx b/packages/hub/src/relative-values/components/RelativeValuesDefinitionCard.tsx index f1492eab41..a76ccd6e83 100644 --- a/packages/hub/src/relative-values/components/RelativeValuesDefinitionCard.tsx +++ b/packages/hub/src/relative-values/components/RelativeValuesDefinitionCard.tsx @@ -1,8 +1,8 @@ import { FC } from "react"; import { EntityCard, UpdatedStatus } from "@/components/EntityCard"; -import { relativeValuesRoute } from "@/routes"; -import { RelativeValuesDefinitionCardDTO } from "@/server/relative-values/data/cards"; +import { relativeValuesRoute } from "@/lib/routes"; +import { RelativeValuesDefinitionCardDTO } from "@/relative-values/data/cards"; type Props = { definition: RelativeValuesDefinitionCardDTO; diff --git a/packages/hub/src/relative-values/components/RelativeValuesDefinitionList.tsx b/packages/hub/src/relative-values/components/RelativeValuesDefinitionList.tsx index 317beb2050..fa80b73388 100644 --- a/packages/hub/src/relative-values/components/RelativeValuesDefinitionList.tsx +++ b/packages/hub/src/relative-values/components/RelativeValuesDefinitionList.tsx @@ -3,9 +3,9 @@ import { FC } from "react"; import { LoadMore } from "@/components/LoadMore"; -import { usePaginator } from "@/hooks/usePaginator"; -import { RelativeValuesDefinitionCardDTO } from "@/server/relative-values/data/cards"; -import { Paginated } from "@/server/types"; +import { usePaginator } from "@/lib/hooks/usePaginator"; +import { Paginated } from "@/lib/types"; +import { RelativeValuesDefinitionCardDTO } from "@/relative-values/data/cards"; import { RelativeValuesDefinitionCard } from "./RelativeValuesDefinitionCard"; diff --git a/packages/hub/src/relative-values/components/RelativeValuesDefinitionRevision.tsx b/packages/hub/src/relative-values/components/RelativeValuesDefinitionRevision.tsx index c95a297664..0263cc14ab 100644 --- a/packages/hub/src/relative-values/components/RelativeValuesDefinitionRevision.tsx +++ b/packages/hub/src/relative-values/components/RelativeValuesDefinitionRevision.tsx @@ -3,7 +3,7 @@ import { FC, Fragment } from "react"; import { StyledTab, StyledTextArea } from "@quri/ui"; import { H2 } from "@/components/ui/Headers"; -import { RelativeValuesDefinitionFullDTO } from "@/server/relative-values/data/full"; +import { RelativeValuesDefinitionFullDTO } from "@/relative-values/data/full"; import { ClusterInfo } from "./common/ClusterInfo"; diff --git a/packages/hub/src/relative-values/components/views/RelativeValuesProvider.tsx b/packages/hub/src/relative-values/components/views/RelativeValuesProvider.tsx index 1bdd9b3928..4170a9e641 100644 --- a/packages/hub/src/relative-values/components/views/RelativeValuesProvider.tsx +++ b/packages/hub/src/relative-values/components/views/RelativeValuesProvider.tsx @@ -3,8 +3,8 @@ import { FC, PropsWithChildren, Reducer } from "react"; import { generateProvider } from "@quri/ui"; +import { RelativeValuesDefinitionFullDTO } from "@/relative-values/data/full"; import { ModelEvaluator } from "@/relative-values/values/ModelEvaluator"; -import { RelativeValuesDefinitionFullDTO } from "@/server/relative-values/data/full"; import { Filter } from "./types"; diff --git a/packages/hub/src/server/relative-values/data/cards.ts b/packages/hub/src/relative-values/data/cards.ts similarity index 95% rename from packages/hub/src/server/relative-values/data/cards.ts rename to packages/hub/src/relative-values/data/cards.ts index c08919e3f9..aff0f63b13 100644 --- a/packages/hub/src/server/relative-values/data/cards.ts +++ b/packages/hub/src/relative-values/data/cards.ts @@ -1,13 +1,12 @@ import { Prisma } from "@prisma/client"; -import { prisma } from "@/prisma"; +import { prisma } from "@/lib/server/prisma"; +import { Paginated } from "@/lib/types"; import { selectTypedOwner, toTypedOwnerDTO, TypedOwner, -} from "@/server/owners/data/typedOwner"; - -import { Paginated } from "../../types"; +} from "@/owners/data/typedOwner"; export const definitionCardSelect = { id: true, diff --git a/packages/hub/src/server/relative-values/data/exports.ts b/packages/hub/src/relative-values/data/exports.ts similarity index 93% rename from packages/hub/src/server/relative-values/data/exports.ts rename to packages/hub/src/relative-values/data/exports.ts index f0eb4e6741..d08babc902 100644 --- a/packages/hub/src/server/relative-values/data/exports.ts +++ b/packages/hub/src/relative-values/data/exports.ts @@ -1,7 +1,7 @@ import { Prisma } from "@prisma/client"; -import { prisma } from "@/prisma"; -import { modelWhereHasAccess } from "@/server/models/data/authHelpers"; +import { prisma } from "@/lib/server/prisma"; +import { modelWhereHasAccess } from "@/models/data/authHelpers"; import { RelativeValuesDefinitionFullDTO } from "./full"; diff --git a/packages/hub/src/server/relative-values/data/findRelativeValuesForSelect.ts b/packages/hub/src/relative-values/data/findRelativeValuesForSelect.ts similarity index 92% rename from packages/hub/src/server/relative-values/data/findRelativeValuesForSelect.ts rename to packages/hub/src/relative-values/data/findRelativeValuesForSelect.ts index bf85cb8819..e162c474f3 100644 --- a/packages/hub/src/server/relative-values/data/findRelativeValuesForSelect.ts +++ b/packages/hub/src/relative-values/data/findRelativeValuesForSelect.ts @@ -1,4 +1,4 @@ -import { prisma } from "@/prisma"; +import { prisma } from "@/lib/server/prisma"; export type FindRelativeValuesForSelectResult = { id: string; diff --git a/packages/hub/src/server/relative-values/data/full.ts b/packages/hub/src/relative-values/data/full.ts similarity index 98% rename from packages/hub/src/server/relative-values/data/full.ts rename to packages/hub/src/relative-values/data/full.ts index eca472f4ed..b9212f6848 100644 --- a/packages/hub/src/server/relative-values/data/full.ts +++ b/packages/hub/src/relative-values/data/full.ts @@ -1,7 +1,7 @@ import { Prisma } from "@prisma/client"; import { z } from "zod"; -import { prisma } from "@/prisma"; +import { prisma } from "@/lib/server/prisma"; import { relativeValuesClustersSchema, relativeValuesItemsSchema, diff --git a/packages/hub/src/server/relative-values/data/fullExport.ts b/packages/hub/src/relative-values/data/fullExport.ts similarity index 97% rename from packages/hub/src/server/relative-values/data/fullExport.ts rename to packages/hub/src/relative-values/data/fullExport.ts index 2276c879d8..aacfa72ca3 100644 --- a/packages/hub/src/server/relative-values/data/fullExport.ts +++ b/packages/hub/src/relative-values/data/fullExport.ts @@ -1,6 +1,6 @@ import { Prisma } from "@prisma/client"; -import { prisma } from "@/prisma"; +import { prisma } from "@/lib/server/prisma"; const select = { id: true, diff --git a/packages/hub/src/server/relative-values/utils.ts b/packages/hub/src/relative-values/utils.ts similarity index 91% rename from packages/hub/src/server/relative-values/utils.ts rename to packages/hub/src/relative-values/utils.ts index f2c6a17281..9c504e950c 100644 --- a/packages/hub/src/server/relative-values/utils.ts +++ b/packages/hub/src/relative-values/utils.ts @@ -1,7 +1,7 @@ import { Session } from "next-auth"; -import { getWriteableModel } from "@/graphql/helpers/modelHelpers"; -import { prisma } from "@/prisma"; +import { prisma } from "@/lib/server/prisma"; +import { getWriteableModel } from "@/models/utils"; export async function getRelativeValuesExportForWriteableModel({ exportId, diff --git a/packages/hub/src/relative-values/values/ModelEvaluator.ts b/packages/hub/src/relative-values/values/ModelEvaluator.ts index baf86aecee..97cd4e857a 100644 --- a/packages/hub/src/relative-values/values/ModelEvaluator.ts +++ b/packages/hub/src/relative-values/values/ModelEvaluator.ts @@ -8,7 +8,7 @@ import { SqStringValue, } from "@quri/squiggle-lang"; -import { RelativeValuesExportFullDTO } from "@/server/relative-values/data/fullExport"; +import { RelativeValuesExportFullDTO } from "@/relative-values/data/fullExport"; import { cartesianProduct } from "../lib/utils"; import { diff --git a/packages/hub/src/scripts/buildRecentModelRevision/createVariableRevision.ts b/packages/hub/src/scripts/buildRecentModelRevision/createVariableRevision.ts index 65bc2b2821..218b112227 100644 --- a/packages/hub/src/scripts/buildRecentModelRevision/createVariableRevision.ts +++ b/packages/hub/src/scripts/buildRecentModelRevision/createVariableRevision.ts @@ -1,4 +1,4 @@ -import { prisma } from "@/prisma"; +import { prisma } from "@/lib/server/prisma"; import { type VariableRevisionInput } from "./worker"; diff --git a/packages/hub/src/scripts/buildRecentModelRevision/worker.ts b/packages/hub/src/scripts/buildRecentModelRevision/worker.ts index cef0df6bfb..166e0372c2 100644 --- a/packages/hub/src/scripts/buildRecentModelRevision/worker.ts +++ b/packages/hub/src/scripts/buildRecentModelRevision/worker.ts @@ -1,5 +1,5 @@ -import { prisma } from "@/prisma"; -import { runSquiggle } from "@/server/runSquiggle"; +import { prisma } from "@/lib/server/prisma"; +import { runSquiggle } from "@/lib/server/runSquiggle"; export type VariableRevisionInput = { variableName: string; diff --git a/packages/hub/src/server/search/actions/adminRebuildSearchIndexAction.ts b/packages/hub/src/search/actions/adminRebuildSearchIndexAction.ts similarity index 75% rename from packages/hub/src/server/search/actions/adminRebuildSearchIndexAction.ts rename to packages/hub/src/search/actions/adminRebuildSearchIndexAction.ts index 838349e868..1a5952b845 100644 --- a/packages/hub/src/server/search/actions/adminRebuildSearchIndexAction.ts +++ b/packages/hub/src/search/actions/adminRebuildSearchIndexAction.ts @@ -2,8 +2,8 @@ import { z } from "zod"; -import { checkRootUser } from "@/server/users/auth"; -import { makeServerAction } from "@/server/utils"; +import { makeServerAction } from "@/lib/server/utils"; +import { checkRootUser } from "@/users/auth"; import { rebuildSearchableTable } from "../helpers"; diff --git a/packages/hub/src/server/search/helpers.ts b/packages/hub/src/search/helpers.ts similarity index 96% rename from packages/hub/src/server/search/helpers.ts rename to packages/hub/src/search/helpers.ts index 33de3c6409..ef9291a185 100644 --- a/packages/hub/src/server/search/helpers.ts +++ b/packages/hub/src/search/helpers.ts @@ -1,4 +1,4 @@ -import { prisma } from "@/prisma"; +import { prisma } from "@/lib/server/prisma"; export async function rebuildSearchableTable() { await prisma.$transaction(async (tx) => { diff --git a/packages/hub/src/squiggle/components/ImportTooltip.tsx b/packages/hub/src/squiggle/components/ImportTooltip.tsx index 9d6baac7ba..609847ad10 100644 --- a/packages/hub/src/squiggle/components/ImportTooltip.tsx +++ b/packages/hub/src/squiggle/components/ImportTooltip.tsx @@ -2,9 +2,9 @@ import clsx from "clsx"; import { FC, useEffect, useState } from "react"; import Skeleton from "react-loading-skeleton"; +import { loadModelCardAction } from "@/models/actions/loadModelCardAction"; import { ModelCard } from "@/models/components/ModelCard"; -import { loadModelCardAction } from "@/server/models/actions/loadModelCardAction"; -import { ModelCardDTO } from "@/server/models/data/cards"; +import { ModelCardDTO } from "@/models/data/cards"; import { parseSourceId } from "./linker"; diff --git a/packages/hub/src/server/users/actions.ts b/packages/hub/src/users/actions.ts similarity index 91% rename from packages/hub/src/server/users/actions.ts rename to packages/hub/src/users/actions.ts index 8ddf977ead..e17dbf0848 100644 --- a/packages/hub/src/server/users/actions.ts +++ b/packages/hub/src/users/actions.ts @@ -2,8 +2,8 @@ import { z } from "zod"; -import { auth } from "@/auth"; -import { prisma } from "@/prisma"; +import { auth } from "@/lib/server/auth"; +import { prisma } from "@/lib/server/prisma"; const schema = z.object({ username: z.string().min(1), diff --git a/packages/hub/src/server/users/auth.ts b/packages/hub/src/users/auth.ts similarity index 94% rename from packages/hub/src/server/users/auth.ts rename to packages/hub/src/users/auth.ts index 8b57dfc747..5bc4f40fa9 100644 --- a/packages/hub/src/server/users/auth.ts +++ b/packages/hub/src/users/auth.ts @@ -2,9 +2,8 @@ import { User } from "@prisma/client"; import { Session } from "next-auth"; import { redirect } from "next/navigation"; -import { prisma } from "@/prisma"; - -import { auth } from "../../auth"; +import { auth } from "@/lib/server/auth"; +import { prisma } from "@/lib/server/prisma"; export async function getSessionOrRedirect() { const session = await auth(); diff --git a/packages/hub/src/server/users/data/layoutUser.ts b/packages/hub/src/users/data/layoutUser.ts similarity index 94% rename from packages/hub/src/server/users/data/layoutUser.ts rename to packages/hub/src/users/data/layoutUser.ts index 5e86bf652e..519c417dff 100644 --- a/packages/hub/src/server/users/data/layoutUser.ts +++ b/packages/hub/src/users/data/layoutUser.ts @@ -1,7 +1,7 @@ import { Prisma } from "@prisma/client"; -import { prisma } from "@/prisma"; -import { modelWhereHasAccess } from "@/server/models/data/authHelpers"; +import { prisma } from "@/lib/server/prisma"; +import { modelWhereHasAccess } from "@/models/data/authHelpers"; const getSelect = async () => ({ diff --git a/packages/hub/src/variables/components/VariableCard.tsx b/packages/hub/src/variables/components/VariableCard.tsx index 6727afd54c..29a52f50ab 100644 --- a/packages/hub/src/variables/components/VariableCard.tsx +++ b/packages/hub/src/variables/components/VariableCard.tsx @@ -12,9 +12,9 @@ import { UpdatedStatus, } from "@/components/EntityCard"; import { Link } from "@/components/ui/Link"; +import { modelRoute, variableRoute } from "@/lib/routes"; import { exportTypeIcon } from "@/lib/typeIcon"; -import { modelRoute, variableRoute } from "@/routes"; -import { VariableCardDTO } from "@/server/variables/data/variableCards"; +import { VariableCardDTO } from "@/variables/data/variableCards"; type Props = { variable: VariableCardDTO; diff --git a/packages/hub/src/variables/components/VariableList.tsx b/packages/hub/src/variables/components/VariableList.tsx index 7777e78adb..2fb08a8ff0 100644 --- a/packages/hub/src/variables/components/VariableList.tsx +++ b/packages/hub/src/variables/components/VariableList.tsx @@ -2,9 +2,9 @@ import { FC } from "react"; import { LoadMore } from "@/components/LoadMore"; -import { usePaginator } from "@/hooks/usePaginator"; -import { Paginated } from "@/server/types"; -import { VariableCardDTO } from "@/server/variables/data/variableCards"; +import { usePaginator } from "@/lib/hooks/usePaginator"; +import { Paginated } from "@/lib/types"; +import { VariableCardDTO } from "@/variables/data/variableCards"; import { VariableCard } from "./VariableCard"; diff --git a/packages/hub/src/lib/VariablesDropdown.tsx b/packages/hub/src/variables/components/VariablesDropdown.tsx similarity index 97% rename from packages/hub/src/lib/VariablesDropdown.tsx rename to packages/hub/src/variables/components/VariablesDropdown.tsx index 146191c947..3f0cdffd3b 100644 --- a/packages/hub/src/lib/VariablesDropdown.tsx +++ b/packages/hub/src/variables/components/VariablesDropdown.tsx @@ -8,9 +8,9 @@ import { } from "@quri/ui"; import { DropdownMenuNextLinkItem } from "@/components/ui/DropdownMenuNextLinkItem"; -import { modelForRelativeValuesExportRoute, variableRoute } from "@/routes"; +import { modelForRelativeValuesExportRoute, variableRoute } from "@/lib/routes"; -import { exportTypeIcon } from "./typeIcon"; +import { exportTypeIcon } from "../../lib/typeIcon"; export type VariableRevision = { title?: string; diff --git a/packages/hub/src/server/variables/data/fullVariableRevision.ts b/packages/hub/src/variables/data/fullVariableRevision.ts similarity index 90% rename from packages/hub/src/server/variables/data/fullVariableRevision.ts rename to packages/hub/src/variables/data/fullVariableRevision.ts index 041abe58b0..cef237da73 100644 --- a/packages/hub/src/server/variables/data/fullVariableRevision.ts +++ b/packages/hub/src/variables/data/fullVariableRevision.ts @@ -1,12 +1,12 @@ import { Prisma } from "@prisma/client"; -import { prisma } from "@/prisma"; -import { modelWhereHasAccess } from "@/server/models/data/authHelpers"; +import { prisma } from "@/lib/server/prisma"; +import { modelWhereHasAccess } from "@/models/data/authHelpers"; import { ModelRevisionFullDTO, modelRevisionFullToDTO, selectModelRevisionFull, -} from "@/server/models/data/fullRevision"; +} from "@/models/data/fullRevision"; const select = { id: true, diff --git a/packages/hub/src/server/variables/data/variableCards.ts b/packages/hub/src/variables/data/variableCards.ts similarity index 93% rename from packages/hub/src/server/variables/data/variableCards.ts rename to packages/hub/src/variables/data/variableCards.ts index d2f1a92561..397840e11a 100644 --- a/packages/hub/src/server/variables/data/variableCards.ts +++ b/packages/hub/src/variables/data/variableCards.ts @@ -1,9 +1,8 @@ import { Prisma } from "@prisma/client"; -import { prisma } from "@/prisma"; - -import { modelWhereHasAccess } from "../../models/data/authHelpers"; -import { Paginated } from "../../types"; +import { prisma } from "@/lib/server/prisma"; +import { Paginated } from "@/lib/types"; +import { modelWhereHasAccess } from "@/models/data/authHelpers"; const variableCardSelect = { id: true, diff --git a/packages/hub/src/server/variables/data/variableRevisions.ts b/packages/hub/src/variables/data/variableRevisions.ts similarity index 90% rename from packages/hub/src/server/variables/data/variableRevisions.ts rename to packages/hub/src/variables/data/variableRevisions.ts index 10da3e4a15..2f2deaa532 100644 --- a/packages/hub/src/server/variables/data/variableRevisions.ts +++ b/packages/hub/src/variables/data/variableRevisions.ts @@ -1,13 +1,13 @@ import { Prisma } from "@prisma/client"; -import { prisma } from "@/prisma"; -import { modelWhereHasAccess } from "@/server/models/data/authHelpers"; +import { prisma } from "@/lib/server/prisma"; +import { Paginated } from "@/lib/types"; +import { modelWhereHasAccess } from "@/models/data/authHelpers"; import { ModelRevisionDTO, modelRevisionToDTO, selectModelRevision, -} from "@/server/models/data/revisions"; -import { Paginated } from "@/server/types"; +} from "@/models/data/revisions"; const select = { id: true, diff --git a/packages/hub/test/setup-db.ts b/packages/hub/test/setup-db.ts index 64effcef0e..cc50cfb496 100644 --- a/packages/hub/test/setup-db.ts +++ b/packages/hub/test/setup-db.ts @@ -1,4 +1,4 @@ -import { prisma } from "@/prisma"; +import { prisma } from "@/lib/server/prisma"; if (!process.env["DATABASE_URL"]?.includes("quri-test")) { throw new Error("Expected quri-test database, probable misconfiguration"); From 15d4c11b5fb8634a09a48233eb5d4b235d15858e Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Sat, 30 Nov 2024 01:26:02 -0300 Subject: [PATCH 52/68] more file renames --- packages/hub/src/app/README.md | 7 ++++ .../upgrade-versions/UpgradeableModel.tsx | 2 +- .../[slug]/EditSquiggleSnippetModel.tsx | 2 +- .../[revisionId]/ModelRevisionView.tsx | 2 +- .../[revisionId]/VariableRevisionPage.tsx | 2 +- .../[slug]/view/ViewSquiggleSnippet.tsx | 2 +- packages/hub/src/groups/data/helpers.ts | 38 ------------------- packages/hub/src/lib/README.md | 7 ++++ packages/hub/src/lib/server/runSquiggle.ts | 2 +- .../src/squiggle/components/ImportTooltip.tsx | 2 +- .../src/squiggle/{components => }/linker.ts | 0 11 files changed, 21 insertions(+), 45 deletions(-) create mode 100644 packages/hub/src/app/README.md create mode 100644 packages/hub/src/lib/README.md rename packages/hub/src/squiggle/{components => }/linker.ts (100%) diff --git a/packages/hub/src/app/README.md b/packages/hub/src/app/README.md new file mode 100644 index 0000000000..e8c1f414c1 --- /dev/null +++ b/packages/hub/src/app/README.md @@ -0,0 +1,7 @@ +Next.js app router root. + +Conventions: + +- store single use components in this folder, next to their `page.tsx` and `layout.tsx` +- if the component is shared between multiple pages, store it in `src/{topic}/components`, where `{topic}` is something like "models" or "relative-values" +- if the component doesn't have an obvious topic, e.g. if it's a generic UI component, store it in `src/components` diff --git a/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx b/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx index 6ea622c94f..a5636d74b3 100644 --- a/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx +++ b/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx @@ -13,7 +13,7 @@ import { EditSquiggleSnippetModel } from "@/app/models/[owner]/[slug]/EditSquigg import { loadModelFullAction } from "@/models/actions/loadModelFullAction"; import { ModelByVersion } from "@/models/data/byVersion"; import { ModelFullDTO } from "@/models/data/full"; -import { sqProjectWithHubLinker } from "@/squiggle/components/linker"; +import { sqProjectWithHubLinker } from "@/squiggle/linker"; const InnerUpgradeableModel: FC<{ model: ModelFullDTO; diff --git a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx index 13d098aa62..2d6bf6f8d4 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx @@ -49,7 +49,7 @@ import { getHubLinker, parseSourceId, serializeSourceId, -} from "@/squiggle/components/linker"; +} from "@/squiggle/linker"; import { Draft, diff --git a/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/ModelRevisionView.tsx b/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/ModelRevisionView.tsx index 0811fc1a29..3cc757cc41 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/ModelRevisionView.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/revisions/[revisionId]/ModelRevisionView.tsx @@ -8,7 +8,7 @@ import { } from "@quri/versioned-squiggle-components"; import { ModelRevisionFullDTO } from "@/models/data/fullRevision"; -import { getHubLinker } from "@/squiggle/components/linker"; +import { getHubLinker } from "@/squiggle/linker"; export const ModelRevisionView: FC<{ revision: ModelRevisionFullDTO; diff --git a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/revisions/[revisionId]/VariableRevisionPage.tsx b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/revisions/[revisionId]/VariableRevisionPage.tsx index 05c2ea19ff..ad2b319e45 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/revisions/[revisionId]/VariableRevisionPage.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/variables/[variableName]/revisions/[revisionId]/VariableRevisionPage.tsx @@ -8,7 +8,7 @@ import { versionSupportsSqPathV2, } from "@quri/versioned-squiggle-components"; -import { sqProjectWithHubLinker } from "@/squiggle/components/linker"; +import { sqProjectWithHubLinker } from "@/squiggle/linker"; import { VariableRevisionFullDTO } from "@/variables/data/fullVariableRevision"; type SquiggleProps = { diff --git a/packages/hub/src/app/models/[owner]/[slug]/view/ViewSquiggleSnippet.tsx b/packages/hub/src/app/models/[owner]/[slug]/view/ViewSquiggleSnippet.tsx index 6b77c7fee1..efddd32a03 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/view/ViewSquiggleSnippet.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/view/ViewSquiggleSnippet.tsx @@ -7,7 +7,7 @@ import { } from "@quri/versioned-squiggle-components"; import { ModelCardDTO } from "@/models/data/cards"; -import { sqProjectWithHubLinker } from "@/squiggle/components/linker"; +import { sqProjectWithHubLinker } from "@/squiggle/linker"; type Props = { data: NonNullable; diff --git a/packages/hub/src/groups/data/helpers.ts b/packages/hub/src/groups/data/helpers.ts index fdd42938f8..80bd4ebcd7 100644 --- a/packages/hub/src/groups/data/helpers.ts +++ b/packages/hub/src/groups/data/helpers.ts @@ -1,6 +1,3 @@ -import { MembershipRole } from "@prisma/client"; - -import { auth } from "@/lib/server/auth"; import { prisma } from "@/lib/server/prisma"; import { getSessionUserOrRedirect } from "@/users/auth"; @@ -11,41 +8,6 @@ export async function hasGroupMembership(groupSlug: string): Promise { return !!(await getMyGroup(groupSlug)); } -export type GroupInviteDTO = { - id: string; - role: MembershipRole; -}; - -export async function loadInviteForMe( - groupSlug: string -): Promise { - const session = await auth(); - if (!session?.user.email) { - return null; - } - - const invite = await prisma.groupInvite.findFirst({ - select: { - id: true, - role: true, - }, - where: { - group: { asOwner: { slug: groupSlug } }, - user: { email: session.user.email }, - status: "Pending", - }, - }); - - if (!invite) { - return null; - } - - return { - id: invite.id, - role: invite.role, - }; -} - export async function validateReusableGroupInviteToken(input: { groupSlug: string; inviteToken: string; diff --git a/packages/hub/src/lib/README.md b/packages/hub/src/lib/README.md new file mode 100644 index 0000000000..3c9c0d4f16 --- /dev/null +++ b/packages/hub/src/lib/README.md @@ -0,0 +1,7 @@ +Files that are shared between different parts of the codebase. + +`lib/server` is for server-only code. + +`lib/hooks` for common React hooks. + +Other files are shared. diff --git a/packages/hub/src/lib/server/runSquiggle.ts b/packages/hub/src/lib/server/runSquiggle.ts index bc726769d0..a80e760b40 100644 --- a/packages/hub/src/lib/server/runSquiggle.ts +++ b/packages/hub/src/lib/server/runSquiggle.ts @@ -11,7 +11,7 @@ import { import { SAMPLE_COUNT_DEFAULT, XY_POINT_LENGTH_DEFAULT } from "@/lib/constants"; import { prisma } from "@/lib/server/prisma"; -import { parseSourceId } from "@/squiggle/components/linker"; +import { parseSourceId } from "@/squiggle/linker"; function getKey(code: string, seed: string): string { return crypto diff --git a/packages/hub/src/squiggle/components/ImportTooltip.tsx b/packages/hub/src/squiggle/components/ImportTooltip.tsx index 609847ad10..df26b0dfa0 100644 --- a/packages/hub/src/squiggle/components/ImportTooltip.tsx +++ b/packages/hub/src/squiggle/components/ImportTooltip.tsx @@ -6,7 +6,7 @@ import { loadModelCardAction } from "@/models/actions/loadModelCardAction"; import { ModelCard } from "@/models/components/ModelCard"; import { ModelCardDTO } from "@/models/data/cards"; -import { parseSourceId } from "./linker"; +import { parseSourceId } from "../linker"; type Props = { importId: string; diff --git a/packages/hub/src/squiggle/components/linker.ts b/packages/hub/src/squiggle/linker.ts similarity index 100% rename from packages/hub/src/squiggle/components/linker.ts rename to packages/hub/src/squiggle/linker.ts From bf6ceac07e28509323570aab0dc791943ea8fecc Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Sat, 30 Nov 2024 17:51:37 -0300 Subject: [PATCH 53/68] upgrade to react 19 --- packages/components/package.json | 6 +- packages/hub/package.json | 6 +- packages/ui/package.json | 11 +- packages/ui/src/forms/common/FormInput.tsx | 7 +- pnpm-lock.yaml | 980 ++++++++++++++++----- 5 files changed, 789 insertions(+), 221 deletions(-) diff --git a/packages/components/package.json b/packages/components/package.json index 353557f47c..f1fffc6477 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -36,7 +36,7 @@ "lodash": "^4.17.21", "prettier": "^3.3.3", "react-draggable": "^4.4.6", - "react-hook-form": "^7.50.0", + "react-hook-form": "^7.53.2", "react-markdown": "^9.0.1", "reactflow": "^11.11.4", "remark-gfm": "^4.0.0", @@ -82,8 +82,8 @@ "jsdom": "^25.0.1", "postcss": "^8.4.38", "postcss-cli": "^11.0.0", - "react": "^18.2.0", - "react-dom": "^18.3.1", + "react": "19.0.0-rc-66855b96-20241106", + "react-dom": "19.0.0-rc-66855b96-20241106", "rollup-plugin-node-builtins": "^2.1.2", "storybook": "^8.1.6", "tailwindcss": "^3.4.3", diff --git a/packages/hub/package.json b/packages/hub/package.json index 3adb04f172..22cd7b96fe 100644 --- a/packages/hub/package.json +++ b/packages/hub/package.json @@ -43,9 +43,9 @@ "next-auth": "5.0.0-beta.25", "nodemailer": "^6.9.13", "pako": "^2.1.0", - "react": "^18.2.0", - "react-dom": "^18.3.1", - "react-hook-form": "^7.50.0", + "react": "19.0.0-rc-66855b96-20241106", + "react-dom": "19.0.0-rc-66855b96-20241106", + "react-hook-form": "^7.53.2", "react-icons": "^5.2.1", "react-loading-skeleton": "^3.4.0", "react-markdown": "^9.0.1", diff --git a/packages/ui/package.json b/packages/ui/package.json index 4b8be2b1f8..9bcbbb74cd 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -41,8 +41,9 @@ "types": "./dist/index.d.ts", "source": "./src/index.ts", "peerDependencies": { - "react": "^18", - "react-dom": "^18" + "react": "^18 | ^19", + "react-dom": "^18 | ^19", + "react-hook-form": "^7.53.2" }, "dependencies": { "@floating-ui/react": "^0.26.16", @@ -50,7 +51,6 @@ "clsx": "^2.1.1", "framer-motion": "^11.0.3", "react-colorful": "^5.6.1", - "react-hook-form": "^7.50.0", "react-select": "^5.8.0", "react-textarea-autosize": "8.5.4", "react-use": "^17.5.0" @@ -77,8 +77,9 @@ "postcss-cli": "^11.0.0", "prettier": "^3.3.3", "prop-types": "^15.8.1", - "react": "^18.2.0", - "react-dom": "^18.3.1", + "react": "19.0.0-rc-66855b96-20241106", + "react-dom": "19.0.0-rc-66855b96-20241106", + "react-hook-form": "^7.53.2", "rollup-plugin-node-builtins": "^2.1.2", "storybook": "^8.1.5", "tailwindcss": "^3.4.14", diff --git a/packages/ui/src/forms/common/FormInput.tsx b/packages/ui/src/forms/common/FormInput.tsx index b991b71d60..16e43f176c 100644 --- a/packages/ui/src/forms/common/FormInput.tsx +++ b/packages/ui/src/forms/common/FormInput.tsx @@ -24,7 +24,12 @@ export function FormInput< TValues extends FieldValues, TFieldName extends FieldPath = FieldPath, >({ name, rules, children }: Props) { - const { register } = useFormContext(); + const formContext = useFormContext(); + if (!formContext) { + throw new Error("FormInput must be used within a FormProvider"); + } + + const { register } = formContext; return ( name={name}> diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b625f8eb12..a5802c3191 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -155,13 +155,13 @@ importers: version: 1.1.4 '@floating-ui/react': specifier: ^0.26.16 - version: 0.26.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 0.26.16(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@heroicons/react': specifier: ^1.0.6 - version: 1.0.6(react@18.3.1) + version: 1.0.6(react@19.0.0-rc-66855b96-20241106) '@hookform/resolvers': specifier: ^3.3.4 - version: 3.3.4(react-hook-form@7.50.0(react@18.3.1)) + version: 3.3.4(react-hook-form@7.53.2(react@19.0.0-rc-66855b96-20241106)) '@lezer/common': specifier: ^1.2.2 version: 1.2.3 @@ -197,16 +197,16 @@ importers: version: 3.3.3 react-draggable: specifier: ^4.4.6 - version: 4.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 4.4.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) react-hook-form: - specifier: ^7.50.0 - version: 7.50.0(react@18.3.1) + specifier: ^7.53.2 + version: 7.53.2(react@19.0.0-rc-66855b96-20241106) react-markdown: specifier: ^9.0.1 - version: 9.0.1(@types/react@18.3.3)(react@18.3.1) + version: 9.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) reactflow: specifier: ^11.11.4 - version: 11.11.4(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 11.11.4(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) remark-gfm: specifier: ^4.0.0 version: 4.0.0 @@ -249,22 +249,22 @@ importers: version: link:../configs '@storybook/addon-essentials': specifier: ^8.1.6 - version: 8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/addon-links': specifier: ^8.1.6 - version: 8.1.6(react@18.3.1) + version: 8.1.6(react@19.0.0-rc-66855b96-20241106) '@storybook/react': specifier: ^8.1.6 - version: 8.1.6(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + version: 8.1.6(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.6.3) '@storybook/react-vite': specifier: ^8.1.6 - version: 8.1.6(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.17.2)(typescript@5.6.3)(vite@5.2.10(@types/node@20.12.7)) + version: 8.1.6(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(rollup@4.17.2)(typescript@5.6.3)(vite@5.2.10(@types/node@20.12.7)) '@testing-library/jest-dom': specifier: ^6.4.2 version: 6.4.2(@jest/globals@29.7.0)(@types/jest@29.5.12)(jest@29.7.0(@types/node@20.12.7)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.6.3))) '@testing-library/react': specifier: ^16.0.1 - version: 16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@testing-library/user-event': specifier: ^14.5.2 version: 14.5.2(@testing-library/dom@10.4.0) @@ -332,17 +332,17 @@ importers: specifier: ^11.0.0 version: 11.0.0(jiti@1.21.0)(postcss@8.4.38) react: - specifier: ^18.2.0 - version: 18.3.1 + specifier: 19.0.0-rc-66855b96-20241106 + version: 19.0.0-rc-66855b96-20241106 react-dom: - specifier: ^18.3.1 - version: 18.3.1(react@18.3.1) + specifier: 19.0.0-rc-66855b96-20241106 + version: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) rollup-plugin-node-builtins: specifier: ^2.1.2 version: 2.1.2 storybook: specifier: ^8.1.6 - version: 8.1.6(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 8.1.6(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) tailwindcss: specifier: ^3.4.3 version: 3.4.3(ts-node@10.9.2(@types/node@20.12.7)(typescript@5.6.3)) @@ -432,7 +432,7 @@ importers: version: link:../versioned-components '@vercel/analytics': specifier: ^1.3.1 - version: 1.3.1(next@15.0.3(@babel/core@7.26.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 1.3.1(next@15.0.3(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) base64-js: specifier: ^1.5.1 version: 1.5.1 @@ -456,10 +456,13 @@ importers: version: 4.17.21 next: specifier: ^15.0.3 - version: 15.0.3(@babel/core@7.26.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 15.0.3(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) next-auth: specifier: 5.0.0-beta.25 - version: 5.0.0-beta.25(next@15.0.3(@babel/core@7.26.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@6.9.13)(react@18.3.1) + version: 5.0.0-beta.25(next@15.0.3(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(nodemailer@6.9.13)(react@19.0.0-rc-66855b96-20241106) + next-safe-action: + specifier: ^7.9.9 + version: 7.9.9(next@15.0.3(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(zod@3.23.8) nodemailer: specifier: ^6.9.13 version: 6.9.13 @@ -467,26 +470,26 @@ importers: specifier: ^2.1.0 version: 2.1.0 react: - specifier: ^18.2.0 - version: 18.3.1 + specifier: 19.0.0-rc-66855b96-20241106 + version: 19.0.0-rc-66855b96-20241106 react-dom: - specifier: ^18.3.1 - version: 18.3.1(react@18.3.1) + specifier: 19.0.0-rc-66855b96-20241106 + version: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) react-hook-form: - specifier: ^7.50.0 - version: 7.50.0(react@18.3.1) + specifier: ^7.53.2 + version: 7.53.2(react@19.0.0-rc-66855b96-20241106) react-icons: specifier: ^5.2.1 - version: 5.2.1(react@18.3.1) + version: 5.2.1(react@19.0.0-rc-66855b96-20241106) react-loading-skeleton: specifier: ^3.4.0 - version: 3.4.0(react@18.3.1) + version: 3.4.0(react@19.0.0-rc-66855b96-20241106) react-markdown: specifier: ^9.0.1 - version: 9.0.1(@types/react@18.3.3)(react@18.3.1) + version: 9.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) react-select: specifier: ^5.8.0 - version: 5.8.0(patch_hash=pok3nxq32ihaf3qpdecuz4j5ea)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 5.8.0(patch_hash=pok3nxq32ihaf3qpdecuz4j5ea)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) remark-breaks: specifier: ^4.0.0 version: 4.0.0 @@ -817,31 +820,28 @@ importers: dependencies: '@floating-ui/react': specifier: ^0.26.16 - version: 0.26.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 0.26.16(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@headlessui/react': specifier: ^2.2.0 - version: 2.2.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.2.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) clsx: specifier: ^2.1.1 version: 2.1.1 framer-motion: specifier: ^11.0.3 - version: 11.0.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 11.0.3(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) react-colorful: specifier: ^5.6.1 - version: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-hook-form: - specifier: ^7.50.0 - version: 7.50.0(react@18.3.1) + version: 5.6.1(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) react-select: specifier: ^5.8.0 - version: 5.8.0(patch_hash=pok3nxq32ihaf3qpdecuz4j5ea)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 5.8.0(patch_hash=pok3nxq32ihaf3qpdecuz4j5ea)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) react-textarea-autosize: specifier: 8.5.4 - version: 8.5.4(@types/react@18.3.3)(react@18.3.1) + version: 8.5.4(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) react-use: specifier: ^17.5.0 - version: 17.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 17.5.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) devDependencies: '@quri/configs': specifier: workspace:* @@ -851,22 +851,22 @@ importers: version: 8.1.6(@types/react-dom@18.3.0)(prettier@3.3.3) '@storybook/addon-essentials': specifier: ^8.1.5 - version: 8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/addon-interactions': specifier: ^8.1.5 version: 8.1.6(@jest/globals@29.7.0)(@types/jest@29.5.14)(jest@29.7.0(@types/node@22.9.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.9.0)(typescript@5.6.3))) '@storybook/addon-links': specifier: ^8.0.9 - version: 8.0.9(react@18.3.1) + version: 8.0.9(react@19.0.0-rc-66855b96-20241106) '@storybook/blocks': specifier: ^8.0.9 - version: 8.0.9(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 8.0.9(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/react': specifier: ^8.1.5 - version: 8.1.6(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + version: 8.1.6(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.6.3) '@storybook/react-vite': specifier: ^8.0.9 - version: 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.17.2)(typescript@5.6.3)(vite@5.2.11(@types/node@22.9.0)) + version: 8.0.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(rollup@4.17.2)(typescript@5.6.3)(vite@5.2.11(@types/node@22.9.0)) '@storybook/testing-library': specifier: ^0.2.2 version: 0.2.2 @@ -907,17 +907,20 @@ importers: specifier: ^15.8.1 version: 15.8.1 react: - specifier: ^18.2.0 - version: 18.3.1 + specifier: 19.0.0-rc-66855b96-20241106 + version: 19.0.0-rc-66855b96-20241106 react-dom: - specifier: ^18.3.1 - version: 18.3.1(react@18.3.1) + specifier: 19.0.0-rc-66855b96-20241106 + version: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + react-hook-form: + specifier: ^7.53.2 + version: 7.53.2(react@19.0.0-rc-66855b96-20241106) rollup-plugin-node-builtins: specifier: ^2.1.2 version: 2.1.2 storybook: specifier: ^8.1.5 - version: 8.1.6(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 8.1.6(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) tailwindcss: specifier: ^3.4.14 version: 3.4.14(ts-node@10.9.2(@types/node@22.9.0)(typescript@5.6.3)) @@ -9316,6 +9319,27 @@ packages: nodemailer: optional: true + next-safe-action@7.9.9: + resolution: {integrity: sha512-wFKKCgfHNsObfbDrbOQV8WAE6RnVx7dwmuUazqdNaTL3ZdDzUlRTnIIVI36qSjmgA3zwwxj3nvfxgK9d0fWr5w==} + engines: {node: '>=18.17'} + peerDependencies: + '@sinclair/typebox': '>= 0.33.3' + next: '>= 14.0.0' + react: '>= 18.2.0' + react-dom: '>= 18.2.0' + valibot: '>= 0.36.0' + yup: '>= 1.0.0' + zod: '>= 3.0.0' + peerDependenciesMeta: + '@sinclair/typebox': + optional: true + valibot: + optional: true + yup: + optional: true + zod: + optional: true + next-themes@0.3.0: resolution: {integrity: sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==} peerDependencies: @@ -10145,6 +10169,11 @@ packages: peerDependencies: react: ^18.3.1 + react-dom@19.0.0-rc-66855b96-20241106: + resolution: {integrity: sha512-D25vdaytZ1wFIRiwNU98NPQ/upS2P8Co4/oNoa02PzHbh8deWdepjm5qwZM/46OdSiGv4WSWwxP55RO9obqJEQ==} + peerDependencies: + react: 19.0.0-rc-66855b96-20241106 + react-dom@19.0.0-rc-cae764ce-20241025: resolution: {integrity: sha512-e3CVe2+ojMe4dz8E/WsV9bkRj+lZt5ms+rhTFHEqIAHv4/PDdXa7P4uJXNhfik+ZYF4Wg5wCDVP4l7cgaudCpg==} peerDependencies: @@ -10167,11 +10196,11 @@ packages: peerDependencies: react: '>=16.13.1' - react-hook-form@7.50.0: - resolution: {integrity: sha512-AOhuzM3RdP09ZCnq+Z0yvKGHK25yiOX5phwxjV9L7U6HMla10ezkBnvQ+Pk4GTuDfsC5P2zza3k8mawFwFLVuQ==} - engines: {node: '>=12.22.0'} + react-hook-form@7.53.2: + resolution: {integrity: sha512-YVel6fW5sOeedd1524pltpHX+jgU2u3DSDtXEaBORNdqiNrsX/nUI/iGXONegttg0mJVnfrIkiV0cmTU6Oo2xw==} + engines: {node: '>=18.0.0'} peerDependencies: - react: ^16.8.0 || ^17 || ^18 + react: ^16.8.0 || ^17 || ^18 || ^19 react-icons@5.2.1: resolution: {integrity: sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==} @@ -10310,6 +10339,10 @@ packages: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} + react@19.0.0-rc-66855b96-20241106: + resolution: {integrity: sha512-klH7xkT71SxRCx4hb1hly5FJB21Hz0ACyxbXYAECEqssUjtJeFUAaI2U1DgJAzkGEnvEm3DkxuBchMC/9K4ipg==} + engines: {node: '>=0.10.0'} + react@19.0.0-rc-cae764ce-20241025: resolution: {integrity: sha512-5wV/3MJc6Ws4l4ZF95yaQKaMV8aWVlIBKOdPA4Kere7CfdJ0NMIuKt9j9v0U4ZTmCi4ubAdN+KL4gGdfTEIpuw==} engines: {node: '>=0.10.0'} @@ -10614,6 +10647,9 @@ packages: scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + scheduler@0.25.0-rc-66855b96-20241106: + resolution: {integrity: sha512-HQXp/Mnp/MMRSXMQF7urNFla+gmtXW/Gr1KliuR0iboTit4KvZRY8KYaq5ccCTAOJiUqQh2rE2F3wgUekmgdlA==} + scheduler@0.25.0-rc-cae764ce-20241025: resolution: {integrity: sha512-kiDqIcp0nrZ8RW65wMujBEs7eDNfd49hcfjDmscxWIsnDTz9NRQrTAChv/tYRYCUNk7qPM36SQOja2HcRuee0A==} @@ -13894,6 +13930,20 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@emotion/react@11.11.0(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.23.9 + '@emotion/babel-plugin': 11.11.0 + '@emotion/cache': 11.11.0 + '@emotion/serialize': 1.1.2 + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@19.0.0-rc-66855b96-20241106) + '@emotion/utils': 1.2.1 + '@emotion/weak-memoize': 0.3.1 + hoist-non-react-statics: 3.3.2 + react: 19.0.0-rc-66855b96-20241106 + optionalDependencies: + '@types/react': 18.3.3 + '@emotion/serialize@1.1.2': dependencies: '@emotion/hash': 0.9.1 @@ -13910,6 +13960,10 @@ snapshots: dependencies: react: 18.3.1 + '@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@19.0.0-rc-66855b96-20241106)': + dependencies: + react: 19.0.0-rc-66855b96-20241106 + '@emotion/utils@1.2.1': {} '@emotion/weak-memoize@0.3.1': {} @@ -14259,6 +14313,12 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@floating-ui/react-dom@2.1.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@floating-ui/dom': 1.6.1 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + '@floating-ui/react-dom@2.1.0(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025)': dependencies: '@floating-ui/dom': 1.6.1 @@ -14281,6 +14341,14 @@ snapshots: react-dom: 18.3.1(react@18.3.1) tabbable: 6.2.0 + '@floating-ui/react@0.26.16(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@floating-ui/react-dom': 2.1.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@floating-ui/utils': 0.2.1 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + tabbable: 6.2.0 + '@floating-ui/utils@0.2.1': {} '@formatjs/intl-localematcher@0.5.7': @@ -14302,13 +14370,30 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@headlessui/react@2.2.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@floating-ui/react': 0.26.16(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@react-aria/focus': 3.17.1(react@19.0.0-rc-66855b96-20241106) + '@react-aria/interactions': 3.21.3(react@19.0.0-rc-66855b96-20241106) + '@tanstack/react-virtual': 3.10.8(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + '@heroicons/react@1.0.6(react@18.3.1)': dependencies: react: 18.3.1 - '@hookform/resolvers@3.3.4(react-hook-form@7.50.0(react@18.3.1))': + '@heroicons/react@1.0.6(react@19.0.0-rc-66855b96-20241106)': + dependencies: + react: 19.0.0-rc-66855b96-20241106 + + '@hookform/resolvers@3.3.4(react-hook-form@7.53.2(react@18.3.1))': dependencies: - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@18.3.1) + + '@hookform/resolvers@3.3.4(react-hook-form@7.53.2(react@19.0.0-rc-66855b96-20241106))': + dependencies: + react-hook-form: 7.53.2(react@19.0.0-rc-66855b96-20241106) '@humanwhocodes/config-array@0.11.14': dependencies: @@ -15078,7 +15163,7 @@ snapshots: '@codemirror/view': 6.26.3 '@floating-ui/react': 0.24.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@heroicons/react': 1.0.6(react@18.3.1) - '@hookform/resolvers': 3.3.4(react-hook-form@7.50.0(react@18.3.1)) + '@hookform/resolvers': 3.3.4(react-hook-form@7.53.2(react@18.3.1)) '@lezer/common': 1.2.3 '@quri/prettier-plugin-squiggle': 0.8.5 '@quri/squiggle-lang': 0.8.5 @@ -15095,7 +15180,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-error-boundary: 4.0.11(react@18.3.1) - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@18.3.1) react-markdown: 8.0.7(@types/react@18.3.3)(react@18.3.1) react-resizable: 3.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) vscode-uri: 3.0.8 @@ -15117,7 +15202,7 @@ snapshots: '@codemirror/view': 6.26.3 '@floating-ui/react': 0.26.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@heroicons/react': 1.0.6(react@18.3.1) - '@hookform/resolvers': 3.3.4(react-hook-form@7.50.0(react@18.3.1)) + '@hookform/resolvers': 3.3.4(react-hook-form@7.53.2(react@18.3.1)) '@lezer/common': 1.2.3 '@quri/prettier-plugin-squiggle': 0.8.6 '@quri/squiggle-lang': 0.8.6 @@ -15135,7 +15220,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-error-boundary: 4.0.11(react@18.3.1) - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@18.3.1) react-markdown: 9.0.1(@types/react@18.3.3)(react@18.3.1) react-resizable: 3.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) vscode-uri: 3.0.8 @@ -15157,7 +15242,7 @@ snapshots: '@codemirror/view': 6.26.3 '@floating-ui/react': 0.26.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@heroicons/react': 1.0.6(react@18.3.1) - '@hookform/resolvers': 3.3.4(react-hook-form@7.50.0(react@18.3.1)) + '@hookform/resolvers': 3.3.4(react-hook-form@7.53.2(react@18.3.1)) '@lezer/common': 1.2.3 '@quri/prettier-plugin-squiggle': 0.9.0 '@quri/squiggle-lang': 0.9.0 @@ -15175,7 +15260,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-draggable: 4.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@18.3.1) react-markdown: 9.0.1(@types/react@18.3.3)(react@18.3.1) zod: 3.23.8 transitivePeerDependencies: @@ -15195,7 +15280,7 @@ snapshots: '@codemirror/view': 6.26.3 '@floating-ui/react': 0.26.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@heroicons/react': 1.0.6(react@18.3.1) - '@hookform/resolvers': 3.3.4(react-hook-form@7.50.0(react@18.3.1)) + '@hookform/resolvers': 3.3.4(react-hook-form@7.53.2(react@18.3.1)) '@lezer/common': 1.2.3 '@quri/prettier-plugin-squiggle': 0.9.2 '@quri/squiggle-lang': 0.9.2 @@ -15213,7 +15298,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-draggable: 4.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@18.3.1) react-markdown: 9.0.1(@types/react@18.3.3)(react@18.3.1) remark-gfm: 4.0.0 shikiji: 0.9.15 @@ -15237,7 +15322,7 @@ snapshots: '@codemirror/view': 6.26.3 '@floating-ui/react': 0.26.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@heroicons/react': 1.0.6(react@18.3.1) - '@hookform/resolvers': 3.3.4(react-hook-form@7.50.0(react@18.3.1)) + '@hookform/resolvers': 3.3.4(react-hook-form@7.53.2(react@18.3.1)) '@lezer/common': 1.2.3 '@quri/prettier-plugin-squiggle': 0.9.3 '@quri/squiggle-lang': 0.9.3 @@ -15255,7 +15340,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-draggable: 4.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@18.3.1) react-markdown: 9.0.1(@types/react@18.3.3)(react@18.3.1) remark-gfm: 4.0.0 shikiji: 0.10.2 @@ -15279,7 +15364,7 @@ snapshots: '@codemirror/view': 6.26.3 '@floating-ui/react': 0.26.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@heroicons/react': 1.0.6(react@18.3.1) - '@hookform/resolvers': 3.3.4(react-hook-form@7.50.0(react@18.3.1)) + '@hookform/resolvers': 3.3.4(react-hook-form@7.53.2(react@18.3.1)) '@lezer/common': 1.2.3 '@quri/prettier-plugin-squiggle': 0.9.4 '@quri/squiggle-lang': 0.9.4 @@ -15295,7 +15380,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-draggable: 4.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@18.3.1) react-markdown: 9.0.1(@types/react@18.3.3)(react@18.3.1) remark-gfm: 4.0.0 shikiji: 0.10.2 @@ -15319,7 +15404,7 @@ snapshots: '@codemirror/view': 6.26.3 '@floating-ui/react': 0.26.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@heroicons/react': 1.0.6(react@18.3.1) - '@hookform/resolvers': 3.3.4(react-hook-form@7.50.0(react@18.3.1)) + '@hookform/resolvers': 3.3.4(react-hook-form@7.53.2(react@18.3.1)) '@lezer/common': 1.2.3 '@quri/prettier-plugin-squiggle': 0.9.5 '@quri/squiggle-lang': 0.9.5 @@ -15335,7 +15420,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-draggable: 4.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@18.3.1) react-markdown: 9.0.1(@types/react@18.3.3)(react@18.3.1) remark-gfm: 4.0.0 shikiji: 0.10.2 @@ -15429,7 +15514,7 @@ snapshots: react: 18.3.1 react-colorful: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-dom: 18.3.1(react@18.3.1) - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@18.3.1) react-select: 5.8.0(patch_hash=pok3nxq32ihaf3qpdecuz4j5ea)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-textarea-autosize: 8.5.2(@types/react@18.3.3)(react@18.3.1) react-use: 17.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -15445,7 +15530,7 @@ snapshots: react: 18.3.1 react-colorful: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-dom: 18.3.1(react@18.3.1) - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@18.3.1) react-select: 5.8.0(patch_hash=pok3nxq32ihaf3qpdecuz4j5ea)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-textarea-autosize: 8.5.3(@types/react@18.3.3)(react@18.3.1) react-use: 17.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -15461,7 +15546,7 @@ snapshots: react: 18.3.1 react-colorful: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-dom: 18.3.1(react@18.3.1) - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@18.3.1) react-select: 5.8.0(patch_hash=pok3nxq32ihaf3qpdecuz4j5ea)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-textarea-autosize: 8.5.3(@types/react@18.3.3)(react@18.3.1) react-use: 17.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -15477,7 +15562,7 @@ snapshots: react: 18.3.1 react-colorful: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-dom: 18.3.1(react@18.3.1) - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@18.3.1) react-select: 5.8.0(patch_hash=pok3nxq32ihaf3qpdecuz4j5ea)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-textarea-autosize: 8.5.3(@types/react@18.3.3)(react@18.3.1) react-use: 17.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -15493,7 +15578,7 @@ snapshots: react: 18.3.1 react-colorful: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-dom: 18.3.1(react@18.3.1) - react-hook-form: 7.50.0(react@18.3.1) + react-hook-form: 7.53.2(react@18.3.1) react-select: 5.8.0(patch_hash=pok3nxq32ihaf3qpdecuz4j5ea)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-textarea-autosize: 8.5.3(@types/react@18.3.3)(react@18.3.1) react-use: 17.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -15569,6 +15654,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + react: 19.0.0-rc-66855b96-20241106 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025)': dependencies: react: 19.0.0-rc-cae764ce-20241025 @@ -15582,6 +15674,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-context@1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + react: 19.0.0-rc-66855b96-20241106 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-context@1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025)': dependencies: react: 19.0.0-rc-cae764ce-20241025 @@ -15617,6 +15716,29 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-dialog@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.25.6 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-id': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + aria-hidden: 1.2.3 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + react-remove-scroll: 2.5.5(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-dialog@1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -15659,6 +15781,20 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-dismissable-layer@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -15679,6 +15815,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-focus-guards@1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + react: 19.0.0-rc-66855b96-20241106 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-focus-guards@1.1.1(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025)': dependencies: react: 19.0.0-rc-cae764ce-20241025 @@ -15697,6 +15840,18 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025) @@ -15716,6 +15871,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-id@1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-id@1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025) @@ -15796,6 +15959,16 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-portal@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-portal@1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025)': dependencies: '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025) @@ -15817,6 +15990,17 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-presence@1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-presence@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025) @@ -15837,6 +16021,16 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025)': dependencies: '@radix-ui/react-slot': 1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025) @@ -15888,6 +16082,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-slot@1.0.2(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.25.6 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-slot@1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025) @@ -15918,6 +16120,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + react: 19.0.0-rc-66855b96-20241106 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025)': dependencies: react: 19.0.0-rc-cae764ce-20241025 @@ -15932,6 +16141,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025)': dependencies: '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025) @@ -15947,6 +16164,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025)': dependencies: '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025) @@ -15961,6 +16186,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@babel/runtime': 7.26.0 + react: 19.0.0-rc-66855b96-20241106 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025)': dependencies: react: 19.0.0-rc-cae764ce-20241025 @@ -16007,6 +16239,15 @@ snapshots: clsx: 2.1.1 react: 18.3.1 + '@react-aria/focus@3.17.1(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@react-aria/interactions': 3.21.3(react@19.0.0-rc-66855b96-20241106) + '@react-aria/utils': 3.24.1(react@19.0.0-rc-66855b96-20241106) + '@react-types/shared': 3.23.1(react@19.0.0-rc-66855b96-20241106) + '@swc/helpers': 0.5.15 + clsx: 2.1.1 + react: 19.0.0-rc-66855b96-20241106 + '@react-aria/interactions@3.21.3(react@18.3.1)': dependencies: '@react-aria/ssr': 3.9.4(react@18.3.1) @@ -16015,11 +16256,24 @@ snapshots: '@swc/helpers': 0.5.15 react: 18.3.1 + '@react-aria/interactions@3.21.3(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@react-aria/ssr': 3.9.4(react@19.0.0-rc-66855b96-20241106) + '@react-aria/utils': 3.24.1(react@19.0.0-rc-66855b96-20241106) + '@react-types/shared': 3.23.1(react@19.0.0-rc-66855b96-20241106) + '@swc/helpers': 0.5.15 + react: 19.0.0-rc-66855b96-20241106 + '@react-aria/ssr@3.9.4(react@18.3.1)': dependencies: '@swc/helpers': 0.5.15 react: 18.3.1 + '@react-aria/ssr@3.9.4(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@swc/helpers': 0.5.15 + react: 19.0.0-rc-66855b96-20241106 + '@react-aria/utils@3.24.1(react@18.3.1)': dependencies: '@react-aria/ssr': 3.9.4(react@18.3.1) @@ -16029,6 +16283,15 @@ snapshots: clsx: 2.1.1 react: 18.3.1 + '@react-aria/utils@3.24.1(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@react-aria/ssr': 3.9.4(react@19.0.0-rc-66855b96-20241106) + '@react-stately/utils': 3.10.1(react@19.0.0-rc-66855b96-20241106) + '@react-types/shared': 3.23.1(react@19.0.0-rc-66855b96-20241106) + '@swc/helpers': 0.5.15 + clsx: 2.1.1 + react: 19.0.0-rc-66855b96-20241106 + '@react-hook/latest@1.0.3(react@18.3.1)': dependencies: react: 18.3.1 @@ -16055,33 +16318,42 @@ snapshots: '@swc/helpers': 0.5.15 react: 18.3.1 + '@react-stately/utils@3.10.1(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@swc/helpers': 0.5.15 + react: 19.0.0-rc-66855b96-20241106 + '@react-types/shared@3.23.1(react@18.3.1)': dependencies: react: 18.3.1 - '@reactflow/background@11.3.14(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-types/shared@3.23.1(react@19.0.0-rc-66855b96-20241106)': + dependencies: + react: 19.0.0-rc-66855b96-20241106 + + '@reactflow/background@11.3.14(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: - '@reactflow/core': 11.11.4(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/core': 11.11.4(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) classcat: 5.0.5 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.5(@types/react@18.3.3)(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + zustand: 4.5.5(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/controls@11.2.14(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reactflow/controls@11.2.14(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: - '@reactflow/core': 11.11.4(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/core': 11.11.4(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) classcat: 5.0.5 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.5(@types/react@18.3.3)(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + zustand: 4.5.5(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/core@11.11.4(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reactflow/core@11.11.4(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: '@types/d3': 7.4.3 '@types/d3-drag': 3.0.2 @@ -16091,48 +16363,48 @@ snapshots: d3-drag: 3.0.0 d3-selection: 3.0.0 d3-zoom: 3.0.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.5(@types/react@18.3.3)(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + zustand: 4.5.5(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/minimap@11.7.14(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reactflow/minimap@11.7.14(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: - '@reactflow/core': 11.11.4(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/core': 11.11.4(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@types/d3-selection': 3.0.4 '@types/d3-zoom': 3.0.2 classcat: 5.0.5 d3-selection: 3.0.0 d3-zoom: 3.0.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.5(@types/react@18.3.3)(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + zustand: 4.5.5(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/node-resizer@2.2.14(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reactflow/node-resizer@2.2.14(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: - '@reactflow/core': 11.11.4(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/core': 11.11.4(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) classcat: 5.0.5 d3-drag: 3.0.0 d3-selection: 3.0.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.5(@types/react@18.3.3)(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + zustand: 4.5.5(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/node-toolbar@1.3.14(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@reactflow/node-toolbar@1.3.14(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: - '@reactflow/core': 11.11.4(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@reactflow/core': 11.11.4(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) classcat: 5.0.5 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - zustand: 4.5.5(@types/react@18.3.3)(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + zustand: 4.5.5(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) transitivePeerDependencies: - '@types/react' - immer @@ -16264,9 +16536,9 @@ snapshots: memoizerific: 1.11.3 ts-dedent: 2.2.0 - '@storybook/addon-controls@8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/addon-controls@8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: - '@storybook/blocks': 8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/blocks': 8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) dequal: 2.0.3 lodash: 4.17.21 ts-dedent: 2.2.0 @@ -16307,11 +16579,11 @@ snapshots: - prettier - supports-color - '@storybook/addon-essentials@8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/addon-essentials@8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: '@storybook/addon-actions': 8.1.6 '@storybook/addon-backgrounds': 8.1.6 - '@storybook/addon-controls': 8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/addon-controls': 8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/addon-docs': 8.1.6(@types/react-dom@18.3.0)(prettier@3.3.3) '@storybook/addon-highlight': 8.1.6 '@storybook/addon-measure': 8.1.6 @@ -16319,7 +16591,7 @@ snapshots: '@storybook/addon-toolbars': 8.1.6 '@storybook/addon-viewport': 8.1.6 '@storybook/core-common': 8.1.6(prettier@3.3.3) - '@storybook/manager-api': 8.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/manager-api': 8.1.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/node-logger': 8.1.6 '@storybook/preview-api': 8.1.6 ts-dedent: 2.2.0 @@ -16351,21 +16623,21 @@ snapshots: - jest - vitest - '@storybook/addon-links@8.0.9(react@18.3.1)': + '@storybook/addon-links@8.0.9(react@19.0.0-rc-66855b96-20241106)': dependencies: '@storybook/csf': 0.1.6 '@storybook/global': 5.0.0 ts-dedent: 2.2.0 optionalDependencies: - react: 18.3.1 + react: 19.0.0-rc-66855b96-20241106 - '@storybook/addon-links@8.1.6(react@18.3.1)': + '@storybook/addon-links@8.1.6(react@19.0.0-rc-66855b96-20241106)': dependencies: '@storybook/csf': 0.1.8 '@storybook/global': 5.0.0 ts-dedent: 2.2.0 optionalDependencies: - react: 18.3.1 + react: 19.0.0-rc-66855b96-20241106 '@storybook/addon-measure@8.1.6': dependencies: @@ -16383,35 +16655,35 @@ snapshots: dependencies: memoizerific: 1.11.3 - '@storybook/blocks@8.0.9(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/blocks@8.0.9(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: '@storybook/channels': 8.0.9 '@storybook/client-logger': 8.0.9 - '@storybook/components': 8.0.9(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/components': 8.0.9(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/core-events': 8.0.9 '@storybook/csf': 0.1.6 '@storybook/docs-tools': 8.0.9 '@storybook/global': 5.0.0 - '@storybook/icons': 1.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/manager-api': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/icons': 1.2.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@storybook/manager-api': 8.0.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/preview-api': 8.0.9 - '@storybook/theming': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/theming': 8.0.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/types': 8.0.9 '@types/lodash': 4.17.13 color-convert: 2.0.1 dequal: 2.0.3 lodash: 4.17.21 - markdown-to-jsx: 7.3.2(react@18.3.1) + markdown-to-jsx: 7.3.2(react@19.0.0-rc-66855b96-20241106) memoizerific: 1.11.3 polished: 4.3.1 - react-colorful: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-colorful: 5.6.1(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) telejson: 7.2.0 tocbot: 4.27.18 ts-dedent: 2.2.0 util-deprecate: 1.0.2 optionalDependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) transitivePeerDependencies: - '@types/react' - encoding @@ -16453,6 +16725,42 @@ snapshots: - prettier - supports-color + '@storybook/blocks@8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@storybook/channels': 8.1.6 + '@storybook/client-logger': 8.1.6 + '@storybook/components': 8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@storybook/core-events': 8.1.6 + '@storybook/csf': 0.1.8 + '@storybook/docs-tools': 8.1.6(prettier@3.3.3) + '@storybook/global': 5.0.0 + '@storybook/icons': 1.2.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@storybook/manager-api': 8.1.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@storybook/preview-api': 8.1.6 + '@storybook/theming': 8.1.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@storybook/types': 8.1.6 + '@types/lodash': 4.17.13 + color-convert: 2.0.1 + dequal: 2.0.3 + lodash: 4.17.21 + markdown-to-jsx: 7.3.2(react@19.0.0-rc-66855b96-20241106) + memoizerific: 1.11.3 + polished: 4.3.1 + react-colorful: 5.6.1(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + telejson: 7.2.0 + tocbot: 4.27.18 + ts-dedent: 2.2.0 + util-deprecate: 1.0.2 + optionalDependencies: + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - encoding + - prettier + - supports-color + '@storybook/builder-manager@8.1.6(prettier@3.3.3)': dependencies: '@fal-works/esbuild-plugin-global-externals': 2.1.2 @@ -16543,7 +16851,7 @@ snapshots: telejson: 7.2.0 tiny-invariant: 1.3.3 - '@storybook/cli@8.1.6(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/cli@8.1.6(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: '@babel/core': 7.26.0 '@babel/types': 7.24.5 @@ -16551,7 +16859,7 @@ snapshots: '@storybook/codemod': 8.1.6 '@storybook/core-common': 8.1.6(prettier@3.3.3) '@storybook/core-events': 8.1.6 - '@storybook/core-server': 8.1.6(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/core-server': 8.1.6(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/csf-tools': 8.1.6 '@storybook/node-logger': 8.1.6 '@storybook/telemetry': 8.1.6(prettier@3.3.3) @@ -16618,18 +16926,18 @@ snapshots: transitivePeerDependencies: - supports-color - '@storybook/components@8.0.9(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/components@8.0.9(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: - '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) '@storybook/client-logger': 8.0.9 '@storybook/csf': 0.1.6 '@storybook/global': 5.0.0 - '@storybook/icons': 1.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@storybook/theming': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/icons': 1.2.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@storybook/theming': 8.0.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/types': 8.0.9 memoizerific: 1.11.3 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) util-deprecate: 1.0.2 transitivePeerDependencies: - '@types/react' @@ -16652,6 +16960,24 @@ snapshots: - '@types/react' - '@types/react-dom' + '@storybook/components@8.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + '@storybook/client-logger': 8.1.6 + '@storybook/csf': 0.1.8 + '@storybook/global': 5.0.0 + '@storybook/icons': 1.2.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@storybook/theming': 8.1.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@storybook/types': 8.1.6 + memoizerific: 1.11.3 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + util-deprecate: 1.0.2 + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + '@storybook/core-common@8.0.9': dependencies: '@storybook/core-events': 8.0.9 @@ -16732,7 +17058,7 @@ snapshots: '@storybook/csf': 0.1.8 ts-dedent: 2.2.0 - '@storybook/core-server@8.1.6(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/core-server@8.1.6(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: '@aw-web-design/x-default-browser': 1.4.126 '@babel/core': 7.26.0 @@ -16747,7 +17073,7 @@ snapshots: '@storybook/docs-mdx': 3.1.0-next.0 '@storybook/global': 5.0.0 '@storybook/manager': 8.1.6 - '@storybook/manager-api': 8.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/manager-api': 8.1.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/node-logger': 8.1.6 '@storybook/preview-api': 8.1.6 '@storybook/telemetry': 8.1.6(prettier@3.3.3) @@ -16876,6 +17202,11 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@storybook/icons@1.2.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + '@storybook/instrumenter@8.1.6': dependencies: '@storybook/channels': 8.1.6 @@ -16886,16 +17217,16 @@ snapshots: '@vitest/utils': 1.5.3 util: 0.12.5 - '@storybook/manager-api@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/manager-api@8.0.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: '@storybook/channels': 8.0.9 '@storybook/client-logger': 8.0.9 '@storybook/core-events': 8.0.9 '@storybook/csf': 0.1.6 '@storybook/global': 5.0.0 - '@storybook/icons': 1.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/icons': 1.2.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/router': 8.0.9 - '@storybook/theming': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/theming': 8.0.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/types': 8.0.9 dequal: 2.0.3 lodash: 4.17.21 @@ -16928,6 +17259,27 @@ snapshots: - react - react-dom + '@storybook/manager-api@8.1.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@storybook/channels': 8.1.6 + '@storybook/client-logger': 8.1.6 + '@storybook/core-events': 8.1.6 + '@storybook/csf': 0.1.8 + '@storybook/global': 5.0.0 + '@storybook/icons': 1.2.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@storybook/router': 8.1.6 + '@storybook/theming': 8.1.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@storybook/types': 8.1.6 + dequal: 2.0.3 + lodash: 4.17.21 + memoizerific: 1.11.3 + store2: 2.14.3 + telejson: 7.2.0 + ts-dedent: 2.2.0 + transitivePeerDependencies: + - react + - react-dom + '@storybook/manager@8.1.6': {} '@storybook/node-logger@8.0.9': {} @@ -16972,28 +17324,33 @@ snapshots: '@storybook/preview@8.1.6': {} - '@storybook/react-dom-shim@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/react-dom-shim@8.0.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) '@storybook/react-dom-shim@8.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/react-vite@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.17.2)(typescript@5.6.3)(vite@5.2.11(@types/node@22.9.0))': + '@storybook/react-dom-shim@8.1.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + + '@storybook/react-vite@8.0.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(rollup@4.17.2)(typescript@5.6.3)(vite@5.2.11(@types/node@22.9.0))': dependencies: '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0(typescript@5.6.3)(vite@5.2.11(@types/node@22.9.0)) '@rollup/pluginutils': 5.1.0(rollup@4.17.2) '@storybook/builder-vite': 8.0.9(typescript@5.6.3)(vite@5.2.11(@types/node@22.9.0)) '@storybook/node-logger': 8.0.9 - '@storybook/react': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + '@storybook/react': 8.0.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.6.3) find-up: 5.0.0 magic-string: 0.30.10 - react: 18.3.1 + react: 19.0.0-rc-66855b96-20241106 react-docgen: 7.0.3 - react-dom: 18.3.1(react@18.3.1) + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) resolve: 1.22.8 tsconfig-paths: 4.2.0 vite: 5.2.11(@types/node@22.9.0) @@ -17005,19 +17362,19 @@ snapshots: - typescript - vite-plugin-glimmerx - '@storybook/react-vite@8.1.6(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.17.2)(typescript@5.6.3)(vite@5.2.10(@types/node@20.12.7))': + '@storybook/react-vite@8.1.6(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(rollup@4.17.2)(typescript@5.6.3)(vite@5.2.10(@types/node@20.12.7))': dependencies: '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.1(typescript@5.6.3)(vite@5.2.10(@types/node@20.12.7)) '@rollup/pluginutils': 5.1.0(rollup@4.17.2) '@storybook/builder-vite': 8.1.6(prettier@3.3.3)(typescript@5.6.3)(vite@5.2.10(@types/node@20.12.7)) '@storybook/node-logger': 8.1.6 - '@storybook/react': 8.1.6(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3) + '@storybook/react': 8.1.6(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.6.3) '@storybook/types': 8.1.6 find-up: 5.0.0 magic-string: 0.30.10 - react: 18.3.1 + react: 19.0.0-rc-66855b96-20241106 react-docgen: 7.0.3 - react-dom: 18.3.1(react@18.3.1) + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) resolve: 1.22.8 tsconfig-paths: 4.2.0 vite: 5.2.10(@types/node@20.12.7) @@ -17030,13 +17387,13 @@ snapshots: - typescript - vite-plugin-glimmerx - '@storybook/react@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3)': + '@storybook/react@8.0.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.6.3)': dependencies: '@storybook/client-logger': 8.0.9 '@storybook/docs-tools': 8.0.9 '@storybook/global': 5.0.0 '@storybook/preview-api': 8.0.9 - '@storybook/react-dom-shim': 8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/react-dom-shim': 8.0.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/types': 8.0.9 '@types/escodegen': 0.0.6 '@types/estree': 0.0.51 @@ -17048,9 +17405,9 @@ snapshots: html-tags: 3.3.1 lodash: 4.17.21 prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-element-to-jsx-string: 15.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + react-element-to-jsx-string: 15.0.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) semver: 7.6.3 ts-dedent: 2.2.0 type-fest: 2.19.0 @@ -17061,13 +17418,13 @@ snapshots: - encoding - supports-color - '@storybook/react@8.1.6(prettier@3.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3)': + '@storybook/react@8.1.6(prettier@3.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.6.3)': dependencies: '@storybook/client-logger': 8.1.6 '@storybook/docs-tools': 8.1.6(prettier@3.3.3) '@storybook/global': 5.0.0 '@storybook/preview-api': 8.1.6 - '@storybook/react-dom-shim': 8.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/react-dom-shim': 8.1.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@storybook/types': 8.1.6 '@types/escodegen': 0.0.6 '@types/estree': 0.0.51 @@ -17079,9 +17436,9 @@ snapshots: html-tags: 3.3.1 lodash: 4.17.21 prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-element-to-jsx-string: 15.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + react-element-to-jsx-string: 15.0.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) semver: 7.6.0 ts-dedent: 2.2.0 type-fest: 2.19.0 @@ -17145,15 +17502,15 @@ snapshots: '@testing-library/user-event': 14.5.2(@testing-library/dom@9.3.4) ts-dedent: 2.2.0 - '@storybook/theming@8.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@storybook/theming@8.0.9(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: - '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.3.1) + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@19.0.0-rc-66855b96-20241106) '@storybook/client-logger': 8.0.9 '@storybook/global': 5.0.0 memoizerific: 1.11.3 optionalDependencies: - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) '@storybook/theming@8.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -17165,6 +17522,16 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@storybook/theming@8.1.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@19.0.0-rc-66855b96-20241106) + '@storybook/client-logger': 8.1.6 + '@storybook/global': 5.0.0 + memoizerific: 1.11.3 + optionalDependencies: + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + '@storybook/types@8.0.9': dependencies: '@storybook/channels': 8.0.9 @@ -17222,6 +17589,12 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@tanstack/react-virtual@3.10.8(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': + dependencies: + '@tanstack/virtual-core': 3.10.8 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + '@tanstack/virtual-core@3.10.8': {} '@testing-library/dom@10.4.0': @@ -17276,12 +17649,12 @@ snapshots: '@types/jest': 29.5.14 jest: 29.7.0(@types/node@22.9.0)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@22.9.0)(typescript@5.6.3)) - '@testing-library/react@16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@testing-library/react@16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: '@babel/runtime': 7.25.6 '@testing-library/dom': 10.4.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) optionalDependencies: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 @@ -17861,12 +18234,12 @@ snapshots: next: 15.0.2(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025) react: 19.0.0-rc-cae764ce-20241025 - '@vercel/analytics@1.3.1(next@15.0.3(@babel/core@7.26.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + '@vercel/analytics@1.3.1(next@15.0.3(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)': dependencies: server-only: 0.0.1 optionalDependencies: - next: 15.0.3(@babel/core@7.26.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react: 18.3.1 + next: 15.0.3(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 '@vitest/expect@1.3.1': dependencies: @@ -19846,7 +20219,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.7 eslint-import-resolver-typescript: 3.5.3(eslint-plugin-import@2.28.1(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.28.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.5.3(eslint-plugin-import@2.28.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.28.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.5.3)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.7.1(eslint@8.57.0) eslint-plugin-react: 7.33.2(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -19869,7 +20242,7 @@ snapshots: debug: 4.3.6 enhanced-resolve: 5.15.0 eslint: 8.57.0 - eslint-plugin-import: 2.28.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.5.3(eslint-plugin-import@2.28.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.28.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.5.3)(eslint@8.57.0) get-tsconfig: 4.7.5 globby: 13.1.3 is-core-module: 2.13.1 @@ -19889,7 +20262,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.28.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.5.3(eslint-plugin-import@2.28.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): + eslint-plugin-import@2.28.1(@typescript-eslint/parser@6.20.0(eslint@8.57.0)(typescript@5.6.3))(eslint-import-resolver-typescript@3.5.3)(eslint@8.57.0): dependencies: array-includes: 3.1.7 array.prototype.findlastindex: 1.2.3 @@ -20399,6 +20772,14 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + framer-motion@11.0.3(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): + dependencies: + tslib: 2.6.2 + optionalDependencies: + '@emotion/is-prop-valid': 0.8.8 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + fresh@0.5.2: {} fs-constants@1.0.0: {} @@ -22251,6 +22632,10 @@ snapshots: dependencies: react: 18.3.1 + markdown-to-jsx@7.3.2(react@19.0.0-rc-66855b96-20241106): + dependencies: + react: 19.0.0-rc-66855b96-20241106 + md5.js@1.3.5: dependencies: hash-base: 3.1.0 @@ -23084,6 +23469,19 @@ snapshots: stacktrace-js: 2.0.2 stylis: 4.3.0 + nano-css@5.6.1(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + css-tree: 1.1.3 + csstype: 3.1.3 + fastest-stable-stringify: 2.0.2 + inline-style-prefixer: 7.0.0 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + rtl-css-js: 1.16.1 + stacktrace-js: 2.0.2 + stylis: 4.3.0 + nanoid@3.3.7: {} napi-build-utils@1.0.2: @@ -23097,14 +23495,22 @@ snapshots: neo-async@2.6.2: {} - next-auth@5.0.0-beta.25(next@15.0.3(@babel/core@7.26.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nodemailer@6.9.13)(react@18.3.1): + next-auth@5.0.0-beta.25(next@15.0.3(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(nodemailer@6.9.13)(react@19.0.0-rc-66855b96-20241106): dependencies: '@auth/core': 0.37.2(nodemailer@6.9.13) - next: 15.0.3(@babel/core@7.26.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react: 18.3.1 + next: 15.0.3(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 optionalDependencies: nodemailer: 6.9.13 + next-safe-action@7.9.9(next@15.0.3(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(zod@3.23.8): + dependencies: + next: 15.0.3(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + optionalDependencies: + zod: 3.23.8 + next-themes@0.3.0(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025): dependencies: react: 19.0.0-rc-cae764ce-20241025 @@ -23120,7 +23526,7 @@ snapshots: postcss: 8.4.31 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.6(@babel/core@7.26.0)(babel-plugin-macros@3.1.0)(react@18.3.1) + styled-jsx: 5.1.6(react@18.3.1) optionalDependencies: '@next/swc-darwin-arm64': 15.0.0 '@next/swc-darwin-x64': 15.0.0 @@ -23161,7 +23567,7 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@15.0.3(@babel/core@7.26.0)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@15.0.3(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): dependencies: '@next/env': 15.0.3 '@swc/counter': 0.1.3 @@ -23169,9 +23575,9 @@ snapshots: busboy: 1.6.0 caniuse-lite: 1.0.30001680 postcss: 8.4.31 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.6(@babel/core@7.26.0)(babel-plugin-macros@3.1.0)(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + styled-jsx: 5.1.6(babel-plugin-macros@3.1.0)(react@19.0.0-rc-66855b96-20241106) optionalDependencies: '@next/swc-darwin-arm64': 15.0.3 '@next/swc-darwin-x64': 15.0.3 @@ -23917,6 +24323,11 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react-colorful@5.6.1(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): + dependencies: + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + react-docgen-typescript@2.2.2(typescript@5.6.3): dependencies: typescript: 5.6.3 @@ -23942,6 +24353,11 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106): + dependencies: + react: 19.0.0-rc-66855b96-20241106 + scheduler: 0.25.0-rc-66855b96-20241106 + react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025): dependencies: react: 19.0.0-rc-cae764ce-20241025 @@ -23954,12 +24370,19 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-element-to-jsx-string@15.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + react-draggable@4.4.6(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): + dependencies: + clsx: 1.2.1 + prop-types: 15.8.1 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + + react-element-to-jsx-string@15.0.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): dependencies: '@base2/pretty-print-object': 1.0.1 is-plain-object: 5.0.0 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) react-is: 18.1.0 react-error-boundary@4.0.11(react@18.3.1): @@ -23967,13 +24390,17 @@ snapshots: '@babel/runtime': 7.25.6 react: 18.3.1 - react-hook-form@7.50.0(react@18.3.1): + react-hook-form@7.53.2(react@18.3.1): dependencies: react: 18.3.1 - react-icons@5.2.1(react@18.3.1): + react-hook-form@7.53.2(react@19.0.0-rc-66855b96-20241106): dependencies: - react: 18.3.1 + react: 19.0.0-rc-66855b96-20241106 + + react-icons@5.2.1(react@19.0.0-rc-66855b96-20241106): + dependencies: + react: 19.0.0-rc-66855b96-20241106 react-icons@5.2.1(react@19.0.0-rc-cae764ce-20241025): dependencies: @@ -23987,9 +24414,9 @@ snapshots: react-is@18.2.0: {} - react-loading-skeleton@3.4.0(react@18.3.1): + react-loading-skeleton@3.4.0(react@19.0.0-rc-66855b96-20241106): dependencies: - react: 18.3.1 + react: 19.0.0-rc-66855b96-20241106 react-markdown@8.0.7(@types/react@18.3.3)(react@18.3.1): dependencies: @@ -24030,6 +24457,23 @@ snapshots: transitivePeerDependencies: - supports-color + react-markdown@9.0.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106): + dependencies: + '@types/hast': 3.0.4 + '@types/react': 18.3.3 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.2.0 + html-url-attributes: 3.0.0 + mdast-util-to-hast: 13.0.2 + react: 19.0.0-rc-66855b96-20241106 + remark-parse: 11.0.0 + remark-rehype: 11.0.0 + unified: 11.0.4 + unist-util-visit: 5.0.0 + vfile: 6.0.1 + transitivePeerDependencies: + - supports-color + react-medium-image-zoom@5.2.10(react-dom@19.0.0-rc-cae764ce-20241025(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025): dependencies: react: 19.0.0-rc-cae764ce-20241025 @@ -24049,6 +24493,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + react-remove-scroll-bar@2.3.6(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106): + dependencies: + react: 19.0.0-rc-66855b96-20241106 + react-style-singleton: 2.2.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.3 + react-remove-scroll-bar@2.3.6(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025): dependencies: react: 19.0.0-rc-cae764ce-20241025 @@ -24068,6 +24520,17 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + react-remove-scroll@2.5.5(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106): + dependencies: + react: 19.0.0-rc-66855b96-20241106 + react-remove-scroll-bar: 2.3.6(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + react-style-singleton: 2.2.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + tslib: 2.8.1 + use-callback-ref: 1.3.2(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + use-sidecar: 1.1.2(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + optionalDependencies: + '@types/react': 18.3.3 + react-remove-scroll@2.6.0(@types/react@18.3.3)(react@18.3.1): dependencies: react: 18.3.1 @@ -24114,6 +24577,22 @@ snapshots: transitivePeerDependencies: - '@types/react' + react-select@5.8.0(patch_hash=pok3nxq32ihaf3qpdecuz4j5ea)(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): + dependencies: + '@babel/runtime': 7.23.9 + '@emotion/cache': 11.11.0 + '@emotion/react': 11.11.0(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + '@floating-ui/dom': 1.6.1 + '@types/react-transition-group': 4.4.6 + memoize-one: 6.0.0 + prop-types: 15.8.1 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + react-transition-group: 4.4.5(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + use-isomorphic-layout-effect: 1.1.2(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + transitivePeerDependencies: + - '@types/react' + react-style-singleton@2.2.1(@types/react@18.3.3)(react@18.3.1): dependencies: get-nonce: 1.0.1 @@ -24123,6 +24602,15 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + react-style-singleton@2.2.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106): + dependencies: + get-nonce: 1.0.1 + invariant: 2.2.4 + react: 19.0.0-rc-66855b96-20241106 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.3 + react-style-singleton@2.2.1(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025): dependencies: get-nonce: 1.0.1 @@ -24150,12 +24638,12 @@ snapshots: transitivePeerDependencies: - '@types/react' - react-textarea-autosize@8.5.4(@types/react@18.3.3)(react@18.3.1): + react-textarea-autosize@8.5.4(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106): dependencies: '@babel/runtime': 7.25.6 - react: 18.3.1 - use-composed-ref: 1.3.0(react@18.3.1) - use-latest: 1.2.1(@types/react@18.3.3)(react@18.3.1) + react: 19.0.0-rc-66855b96-20241106 + use-composed-ref: 1.3.0(react@19.0.0-rc-66855b96-20241106) + use-latest: 1.2.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) transitivePeerDependencies: - '@types/react' @@ -24168,11 +24656,25 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react-transition-group@4.4.5(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): + dependencies: + '@babel/runtime': 7.23.9 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + react-universal-interface@0.6.2(react@18.3.1)(tslib@2.6.2): dependencies: react: 18.3.1 tslib: 2.6.2 + react-universal-interface@0.6.2(react@19.0.0-rc-66855b96-20241106)(tslib@2.6.2): + dependencies: + react: 19.0.0-rc-66855b96-20241106 + tslib: 2.6.2 + react-use@17.5.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@types/js-cookie': 2.2.7 @@ -24192,22 +24694,43 @@ snapshots: ts-easing: 0.2.0 tslib: 2.6.2 + react-use@17.5.0(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): + dependencies: + '@types/js-cookie': 2.2.7 + '@xobotyi/scrollbar-width': 1.9.5 + copy-to-clipboard: 3.3.3 + fast-deep-equal: 3.1.3 + fast-shallow-equal: 1.0.0 + js-cookie: 2.2.1 + nano-css: 5.6.1(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) + react-universal-interface: 0.6.2(react@19.0.0-rc-66855b96-20241106)(tslib@2.6.2) + resize-observer-polyfill: 1.5.1 + screenfull: 5.2.0 + set-harmonic-interval: 1.0.1 + throttle-debounce: 3.0.1 + ts-easing: 0.2.0 + tslib: 2.6.2 + react@18.3.1: dependencies: loose-envify: 1.4.0 + react@19.0.0-rc-66855b96-20241106: {} + react@19.0.0-rc-cae764ce-20241025: {} - reactflow@11.11.4(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + reactflow@11.11.4(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): dependencies: - '@reactflow/background': 11.3.14(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reactflow/controls': 11.2.14(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reactflow/core': 11.11.4(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reactflow/minimap': 11.7.14(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reactflow/node-resizer': 2.2.14(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@reactflow/node-toolbar': 1.3.14(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + '@reactflow/background': 11.3.14(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@reactflow/controls': 11.2.14(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@reactflow/core': 11.11.4(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@reactflow/minimap': 11.7.14(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@reactflow/node-resizer': 2.2.14(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + '@reactflow/node-toolbar': 1.3.14(@types/react@18.3.3)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) + react: 19.0.0-rc-66855b96-20241106 + react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) transitivePeerDependencies: - '@types/react' - immer @@ -24673,6 +25196,8 @@ snapshots: dependencies: loose-envify: 1.4.0 + scheduler@0.25.0-rc-66855b96-20241106: {} + scheduler@0.25.0-rc-cae764ce-20241025: {} screenfull@5.2.0: {} @@ -24977,9 +25502,9 @@ snapshots: store2@2.14.3: {} - storybook@8.1.6(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + storybook@8.1.6(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106): dependencies: - '@storybook/cli': 8.1.6(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/cli': 8.1.6(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) transitivePeerDependencies: - '@babel/preset-env' - bufferutil @@ -25115,14 +25640,19 @@ snapshots: dependencies: inline-style-parser: 0.1.1 - styled-jsx@5.1.6(@babel/core@7.26.0)(babel-plugin-macros@3.1.0)(react@18.3.1): + styled-jsx@5.1.6(babel-plugin-macros@3.1.0)(react@19.0.0-rc-66855b96-20241106): dependencies: client-only: 0.0.1 - react: 18.3.1 + react: 19.0.0-rc-66855b96-20241106 optionalDependencies: - '@babel/core': 7.26.0 babel-plugin-macros: 3.1.0 + styled-jsx@5.1.6(react@18.3.1): + dependencies: + client-only: 0.0.1 + react: 18.3.1 + optional: true + styled-jsx@5.1.6(react@19.0.0-rc-cae764ce-20241025): dependencies: client-only: 0.0.1 @@ -25804,6 +26334,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + use-callback-ref@1.3.2(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106): + dependencies: + react: 19.0.0-rc-66855b96-20241106 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.3 + use-callback-ref@1.3.2(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025): dependencies: react: 19.0.0-rc-cae764ce-20241025 @@ -25815,12 +26352,22 @@ snapshots: dependencies: react: 18.3.1 + use-composed-ref@1.3.0(react@19.0.0-rc-66855b96-20241106): + dependencies: + react: 19.0.0-rc-66855b96-20241106 + use-isomorphic-layout-effect@1.1.2(@types/react@18.3.3)(react@18.3.1): dependencies: react: 18.3.1 optionalDependencies: '@types/react': 18.3.3 + use-isomorphic-layout-effect@1.1.2(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106): + dependencies: + react: 19.0.0-rc-66855b96-20241106 + optionalDependencies: + '@types/react': 18.3.3 + use-latest@1.2.1(@types/react@18.3.3)(react@18.3.1): dependencies: react: 18.3.1 @@ -25828,6 +26375,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + use-latest@1.2.1(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106): + dependencies: + react: 19.0.0-rc-66855b96-20241106 + use-isomorphic-layout-effect: 1.1.2(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106) + optionalDependencies: + '@types/react': 18.3.3 + use-sidecar@1.1.2(@types/react@18.3.3)(react@18.3.1): dependencies: detect-node-es: 1.1.0 @@ -25836,6 +26390,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + use-sidecar@1.1.2(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106): + dependencies: + detect-node-es: 1.1.0 + react: 19.0.0-rc-66855b96-20241106 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.3 + use-sidecar@1.1.2(@types/react@18.3.3)(react@19.0.0-rc-cae764ce-20241025): dependencies: detect-node-es: 1.1.0 @@ -25844,9 +26406,9 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 - use-sync-external-store@1.2.2(react@18.3.1): + use-sync-external-store@1.2.2(react@19.0.0-rc-66855b96-20241106): dependencies: - react: 18.3.1 + react: 19.0.0-rc-66855b96-20241106 use-sync-external-store@1.2.2(react@19.0.0-rc-cae764ce-20241025): dependencies: @@ -26251,11 +26813,11 @@ snapshots: zod@3.23.8: {} - zustand@4.5.5(@types/react@18.3.3)(react@18.3.1): + zustand@4.5.5(@types/react@18.3.3)(react@19.0.0-rc-66855b96-20241106): dependencies: - use-sync-external-store: 1.2.2(react@18.3.1) + use-sync-external-store: 1.2.2(react@19.0.0-rc-66855b96-20241106) optionalDependencies: '@types/react': 18.3.3 - react: 18.3.1 + react: 19.0.0-rc-66855b96-20241106 zwitch@2.0.4: {} From 7a6ed361807e5dca9ae44c622b20e4901a413bd3 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Sat, 30 Nov 2024 17:51:50 -0300 Subject: [PATCH 54/68] trying next-safe-action --- packages/hub/package.json | 1 + packages/hub/src/app/new/model/NewModel.tsx | 71 +++++----- packages/hub/src/lib/server/utils.ts | 21 ++- .../src/models/actions/createModelAction.ts | 132 ++++++++++++++++++ .../createSquiggleSnippetModelAction.ts | 91 ------------ 5 files changed, 188 insertions(+), 128 deletions(-) create mode 100644 packages/hub/src/models/actions/createModelAction.ts delete mode 100644 packages/hub/src/models/actions/createSquiggleSnippetModelAction.ts diff --git a/packages/hub/package.json b/packages/hub/package.json index 22cd7b96fe..3159874aa4 100644 --- a/packages/hub/package.json +++ b/packages/hub/package.json @@ -41,6 +41,7 @@ "lodash": "^4.17.21", "next": "^15.0.3", "next-auth": "5.0.0-beta.25", + "next-safe-action": "^7.9.9", "nodemailer": "^6.9.13", "pako": "^2.1.0", "react": "19.0.0-rc-66855b96-20241106", diff --git a/packages/hub/src/app/new/model/NewModel.tsx b/packages/hub/src/app/new/model/NewModel.tsx index 7ec8e46c3c..b754fcee56 100644 --- a/packages/hub/src/app/new/model/NewModel.tsx +++ b/packages/hub/src/app/new/model/NewModel.tsx @@ -1,25 +1,14 @@ "use client"; -import { useRouter } from "next/navigation"; +import { useAction } from "next-safe-action/hooks"; import { FC, useState } from "react"; -import { FormProvider } from "react-hook-form"; +import { FormProvider, useForm } from "react-hook-form"; -import { generateSeed } from "@quri/squiggle-lang"; -import { Button, CheckboxFormField } from "@quri/ui"; -import { defaultSquiggleVersion } from "@quri/versioned-squiggle-components"; +import { Button, CheckboxFormField, useToast } from "@quri/ui"; import { SelectGroup, SelectGroupOption } from "@/components/SelectGroup"; import { H1 } from "@/components/ui/Headers"; import { SlugFormField } from "@/components/ui/SlugFormField"; -import { useServerActionForm } from "@/lib/hooks/useServerActionForm"; -import { modelRoute } from "@/lib/routes"; -import { createSquiggleSnippetModelAction } from "@/models/actions/createSquiggleSnippetModelAction"; - -const defaultCode = `/* -Describe your code here -*/ - -a = normal(2, 5) -`; +import { createModelAction } from "@/models/actions/createModelAction"; type FormShape = { slug: string | undefined; @@ -32,36 +21,42 @@ export const NewModel: FC<{ initialGroup: SelectGroupOption | null }> = ({ }) => { const [group] = useState(initialGroup); - const router = useRouter(); + const toast = useToast(); + + const { executeAsync, status } = useAction(createModelAction, { + onError: ({ error, ...rest }) => { + console.trace("onError", error, rest); + if (error.serverError) { + toast(error.serverError, "error"); + return; + } - const { form, onSubmit, inFlight } = useServerActionForm< - FormShape, - typeof createSquiggleSnippetModelAction - >({ + const slugError = error.validationErrors?.slug?._errors?.[0]; + if (slugError) { + form.setError("slug", { + message: slugError, + }); + } else { + toast("Internal error", "error"); + } + }, + }); + + const form = useForm({ mode: "onChange", defaultValues: { // don't pass `slug: ""` here, it will lead to form reset if a user started to type in a value before JS finished loading group, isPrivate: false, }, - blockOnSuccess: true, - action: createSquiggleSnippetModelAction, - formDataToVariables: (data) => ({ + }); + + const onSubmit = form.handleSubmit(async (data) => { + await executeAsync({ slug: data.slug ?? "", // shouldn't happen but satisfies Typescript groupSlug: data.group?.slug, isPrivate: data.isPrivate, - code: defaultCode, - version: defaultSquiggleVersion, - seed: generateSeed(), - }), - onCompleted: (result) => { - router.push( - modelRoute({ - owner: result.model.owner.slug, - slug: result.model.slug, - }) - ); - }, + }); }); return ( @@ -86,7 +81,11 @@ export const NewModel: FC<{ initialGroup: SelectGroupOption | null }> = ({
+ + ); +} diff --git a/packages/hub/src/models/actions/adminUpdateModelVersionAction.ts b/packages/hub/src/models/actions/adminUpdateModelVersionAction.ts index dec6aeb4e8..092022a844 100644 --- a/packages/hub/src/models/actions/adminUpdateModelVersionAction.ts +++ b/packages/hub/src/models/actions/adminUpdateModelVersionAction.ts @@ -2,16 +2,18 @@ import { z } from "zod"; import { prisma } from "@/lib/server/prisma"; -import { makeServerAction } from "@/lib/server/utils"; +import { actionClient } from "@/lib/server/utils"; import { checkRootUser, getSelf, getSessionOrRedirect } from "@/users/auth"; // Admin-only query for upgrading model versions -export const adminUpdateModelVersionAction = makeServerAction( - z.object({ - modelId: z.string(), - version: z.string(), - }), - async (input) => { +export const adminUpdateModelVersionAction = actionClient + .schema( + z.object({ + modelId: z.string(), + version: z.string(), + }) + ) + .action(async ({ parsedInput: input }) => { await checkRootUser(); const session = await getSessionOrRedirect(); @@ -86,5 +88,4 @@ export const adminUpdateModelVersionAction = makeServerAction( }); return { model }; - } -); + }); diff --git a/packages/hub/src/search/actions/adminRebuildSearchIndexAction.ts b/packages/hub/src/search/actions/adminRebuildSearchIndexAction.ts index 1a5952b845..c319420ae4 100644 --- a/packages/hub/src/search/actions/adminRebuildSearchIndexAction.ts +++ b/packages/hub/src/search/actions/adminRebuildSearchIndexAction.ts @@ -2,16 +2,16 @@ import { z } from "zod"; -import { makeServerAction } from "@/lib/server/utils"; +import { actionClient } from "@/lib/server/utils"; import { checkRootUser } from "@/users/auth"; import { rebuildSearchableTable } from "../helpers"; // Admin-only query for rebuilding the search index -export const adminRebuildSearchIndexAction = makeServerAction( - z.object({}), - async () => { +export const adminRebuildSearchIndexAction = actionClient + .schema(z.object({})) + .action(async () => { await checkRootUser(); await rebuildSearchableTable(); - } -); + return { ok: true }; + }); From cda85cf4c4e2a4f79067fec42b5e694160ee0a88 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 3 Dec 2024 13:34:23 -0300 Subject: [PATCH 57/68] more safe actions --- .../[slug]/EditSquiggleSnippetModel.tsx | 12 +- .../[owner]/[slug]/ModelSettingsButton.tsx | 2 +- .../models/[owner]/[slug]/MoveModelAction.tsx | 13 +- .../[owner]/[slug]/UpdateModelSlugAction.tsx | 9 +- .../components/ui/SafeActionModalAction.tsx | 102 ++++++++++++++++ .../hub/src/lib/hooks/useSafeActionForm.ts | 112 ++++++++++++++++++ .../hub/src/models/actions/moveModelAction.ts | 51 +++++--- .../models/actions/updateModelSlugAction.ts | 74 +++++++++--- .../updateSquiggleSnippetModelAction.ts | 59 ++++----- 9 files changed, 349 insertions(+), 85 deletions(-) create mode 100644 packages/hub/src/components/ui/SafeActionModalAction.tsx create mode 100644 packages/hub/src/lib/hooks/useSafeActionForm.ts diff --git a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx index 2d6bf6f8d4..268537fee5 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx @@ -40,7 +40,7 @@ import { ReactRoot } from "@/components/ReactRoot"; import { FormModal } from "@/components/ui/FormModal"; import { SAMPLE_COUNT_DEFAULT, XY_POINT_LENGTH_DEFAULT } from "@/lib/constants"; import { useAvailableHeight } from "@/lib/hooks/useAvailableHeight"; -import { useServerActionForm } from "@/lib/hooks/useServerActionForm"; +import { useSafeActionForm } from "@/lib/hooks/useSafeActionForm"; import { modelRoute, variableRoute } from "@/lib/routes"; import { updateSquiggleSnippetModelAction } from "@/models/actions/updateSquiggleSnippetModelAction"; import { ModelFullDTO } from "@/models/data/full"; @@ -97,6 +97,7 @@ const SaveDialog: FC<{ onSubmit: OnSubmit; close: () => void }> = ({ title="Save with comment" form={form} close={close} + inFlight={form.formState.isSubmitting} submitText="Save" > name="comment" label="Comment" /> @@ -165,23 +166,22 @@ export const EditSquiggleSnippetModel: FC = ({ }; }, [content, revision.relativeValuesExports]); - const { form, onSubmit, inFlight } = useServerActionForm< + const { form, onSubmit, inFlight } = useSafeActionForm< SquiggleSnippetFormShape, typeof updateSquiggleSnippetModelAction, { comment: string } >({ defaultValues: initialFormValues, - action: async (variables) => { - const result = await updateSquiggleSnippetModelAction(variables); + action: updateSquiggleSnippetModelAction, + onCompleted: () => { toast("Saved", "confirmation"); draftUtils.discard(draftLocator); - return result; }, formDataToVariables: (formData, extraData) => ({ content: { code: formData.code, version, - seed: seed, + seed, autorunMode: content.autorunMode, sampleCount: content.sampleCount, xyPointLength: content.xyPointLength, diff --git a/packages/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx b/packages/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx index 668fba6b04..f92f25e845 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/ModelSettingsButton.tsx @@ -18,7 +18,7 @@ export const ModelSettingsButton: FC<{ render={({ close }) => ( - + )} diff --git a/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx index fc1127aa4e..1dc99bd4dc 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx @@ -4,7 +4,7 @@ import { FC } from "react"; import { RightArrowIcon } from "@quri/ui"; import { SelectOwner, SelectOwnerOption } from "@/components/SelectOwner"; -import { ServerActionModalAction } from "@/components/ui/ServerActionModalAction"; +import { SafeActionModalAction } from "@/components/ui/SafeActionModalAction"; import { modelRoute } from "@/lib/routes"; import { moveModelAction } from "@/models/actions/moveModelAction"; import { ModelCardDTO } from "@/models/data/cards"; @@ -15,14 +15,13 @@ type FormShape = { owner: SelectOwnerOption }; type Props = { model: ModelCardDTO; - close(): void; }; -export const MoveModelAction: FC = ({ model, close }) => { +export const MoveModelAction: FC = ({ model }) => { const router = useRouter(); return ( - + title="Change Owner" modalTitle={`Change owner for ${model.owner.slug}/${model.slug}`} submitText="Save" @@ -31,9 +30,10 @@ export const MoveModelAction: FC = ({ model, close }) => { // so we have to explicitly recast owner: model.owner as SelectOwnerOption, }} + action={moveModelAction} formDataToVariables={(data) => ({ oldOwner: model.owner.slug, - newOwner: data.owner.slug, + owner: { slug: data.owner.slug }, slug: model.slug, })} onCompleted={({ model: newModel }) => { @@ -46,7 +46,6 @@ export const MoveModelAction: FC = ({ model, close }) => { ); }} icon={RightArrowIcon} - action={moveModelAction} initialFocus="owner" blockOnSuccess > @@ -58,6 +57,6 @@ export const MoveModelAction: FC = ({ model, close }) => { name="owner" label="New owner" myOnly />
)} - + ); }; diff --git a/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx index 3aa9222949..c3bb4692ec 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx @@ -3,7 +3,7 @@ import { FC } from "react"; import { EditIcon } from "@quri/ui"; -import { ServerActionModalAction } from "@/components/ui/ServerActionModalAction"; +import { SafeActionModalAction } from "@/components/ui/SafeActionModalAction"; import { SlugFormField } from "@/components/ui/SlugFormField"; import { modelRoute } from "@/lib/routes"; import { updateModelSlugAction } from "@/models/actions/updateModelSlugAction"; @@ -22,14 +22,15 @@ export const UpdateModelSlugAction: FC = ({ model, close }) => { const router = useRouter(); return ( - + title="Rename" icon={EditIcon} action={updateModelSlugAction} + defaultValues={{ slug: model.slug }} formDataToVariables={(data) => ({ owner: model.owner.slug, oldSlug: model.slug, - newSlug: data.slug, + slug: data.slug, })} onCompleted={({ model: newModel }) => { draftUtils.rename( @@ -52,6 +53,6 @@ export const UpdateModelSlugAction: FC = ({ model, close }) => { name="slug" label="New slug" />
)} - + ); }; diff --git a/packages/hub/src/components/ui/SafeActionModalAction.tsx b/packages/hub/src/components/ui/SafeActionModalAction.tsx new file mode 100644 index 0000000000..9eb76e9741 --- /dev/null +++ b/packages/hub/src/components/ui/SafeActionModalAction.tsx @@ -0,0 +1,102 @@ +import { HookSafeActionFn } from "next-safe-action/hooks"; +import { FC, PropsWithChildren, ReactNode } from "react"; +import { FieldPath, FieldValues } from "react-hook-form"; + +import { + DropdownMenuModalActionItem, + IconProps, + useCloseDropdown, +} from "@quri/ui"; + +import { FormModal } from "@/components/ui/FormModal"; +import { useSafeActionForm } from "@/lib/hooks/useSafeActionForm"; + +type CommonProps< + TFormShape extends FieldValues, + Action extends HookSafeActionFn, +> = Pick< + Parameters>[0], + | "formDataToVariables" + | "defaultValues" + | "action" + | "onCompleted" + | "blockOnSuccess" +> & { + initialFocus?: FieldPath; + submitText: string; +}; + +function SafeActionFormModal< + TFormShape extends FieldValues, + const Action extends HookSafeActionFn, +>({ + formDataToVariables, + initialFocus, + defaultValues, + submitText, + action, + onCompleted, + title, + children, +}: PropsWithChildren> & { + title: string; +}): ReactNode { + // Note that we use the same `close` that's responsible for closing the dropdown. + const closeDropdown = useCloseDropdown(); + + const { form, onSubmit, inFlight } = useSafeActionForm({ + mode: "onChange", + defaultValues, + action, + formDataToVariables, + async onCompleted(data) { + onCompleted?.(data); + closeDropdown(); + }, + }); + + return ( + + {children} + + ); +} + +export function SafeActionModalAction< + TFormShape extends FieldValues, + const Action extends HookSafeActionFn, +>({ + modalTitle, + title, + icon, + children, + ...modalProps +}: CommonProps & { + modalTitle: string; + title: string; + icon?: FC; + children: () => ReactNode; +}): ReactNode { + return ( + ( + + {...modalProps} + title={modalTitle} + > + {children()} + + )} + /> + ); +} diff --git a/packages/hub/src/lib/hooks/useSafeActionForm.ts b/packages/hub/src/lib/hooks/useSafeActionForm.ts new file mode 100644 index 0000000000..92b0f018d0 --- /dev/null +++ b/packages/hub/src/lib/hooks/useSafeActionForm.ts @@ -0,0 +1,112 @@ +import { + InferSafeActionFnInput, + InferSafeActionFnResult, +} from "next-safe-action"; +import { HookSafeActionFn, useAction } from "next-safe-action/hooks"; +import { BaseSyntheticEvent, useCallback } from "react"; +import { FieldValues, Path, useForm, UseFormProps } from "react-hook-form"; + +import { useToast } from "@quri/ui"; + +/** + * This hook ties together `useForm` and server actions. + * + * See also: + * - `` if your form is available through a Dropdown menu + * + * All generic type parameters to this function default to `never`, so you'll have to set them explicitly to pass type checks. + */ +export function useSafeActionForm< + FormShape extends FieldValues = never, + const Action extends HookSafeActionFn = never, + ExtraData extends Record = Record, + ActionInput = InferSafeActionFnInput["clientInput"], +>({ + defaultValues, + mode, + action, + onCompleted, + formDataToVariables, + blockOnSuccess, +}: { + // This is unfortunately not strictly type-safe: if you return extra variables that are not needed for mutation, TypeScript won't complain. + // See also: https://stackoverflow.com/questions/72111571/typescript-exact-return-type-of-function + // This could be solved by converting the return type to generic, but I expect that the lack of partial type parameters in TypeScript + // would get in the way, so I won't even try. + formDataToVariables: (data: FormShape, extraData?: ExtraData) => ActionInput; + action: Action; + onCompleted?: ( + result: NonNullable["data"]> + ) => void | Promise; + blockOnSuccess?: boolean; +} & Pick, "defaultValues" | "mode">) { + const form = useForm({ defaultValues, mode }); + + const toast = useToast(); + + // TODO - use https://github.com/next-safe-action/adapter-react-hook-form + + const { executeAsync, isPending, hasSucceeded } = useAction(action, { + onSuccess: ({ data }) => { + if (data) { + onCompleted?.(data); + } + }, + onError: ({ error }) => { + if (error.serverError) { + toast(String(error.serverError), "error"); + return; + } + + // validation errors? + if (error.validationErrors) { + // TODO - support top-level global validation error + + let hasErrors = false; + for (const [field, fieldErrors] of Object.entries( + error.validationErrors + )) { + if (!fieldErrors || typeof fieldErrors !== "object") { + continue; + } + + if ("_errors" in fieldErrors && Array.isArray(fieldErrors._errors)) { + const fieldError = fieldErrors._errors[0]; + if (fieldError) { + // TODO - check that the field exists in the form + form.setError(field as Path, { + message: String(fieldError), + }); + hasErrors = true; + } + } + } + + if (hasErrors) { + return; + } + } + + toast("Internal error", "error"); + }, + }); + + const onSubmit = useCallback( + (event?: BaseSyntheticEvent, extraData?: ExtraData) => + form.handleSubmit(async (formData) => { + const result = await executeAsync( + formDataToVariables(formData, extraData) + ); + if (result?.serverError || result?.validationErrors) { + throw new Error("Action failed"); + } + })(event), + [form, formDataToVariables, executeAsync] + ); + + return { + form, + onSubmit, + inFlight: blockOnSuccess ? hasSucceeded : isPending, + }; +} diff --git a/packages/hub/src/models/actions/moveModelAction.ts b/packages/hub/src/models/actions/moveModelAction.ts index 547c6e0e38..715845a153 100644 --- a/packages/hub/src/models/actions/moveModelAction.ts +++ b/packages/hub/src/models/actions/moveModelAction.ts @@ -1,21 +1,27 @@ "use server"; +import { returnValidationErrors } from "next-safe-action"; import { z } from "zod"; import { prisma } from "@/lib/server/prisma"; -import { makeServerAction } from "@/lib/server/utils"; +import { actionClient } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { getWriteableModel } from "@/models/utils"; import { getWriteableOwnerBySlug } from "@/owners/data/auth"; import { getSessionOrRedirect } from "@/users/auth"; -export const moveModelAction = makeServerAction( - z.object({ - oldOwner: zSlug, - newOwner: zSlug, +const schema = z.object({ + oldOwner: zSlug, + // intentionally nested, matches the form shape, so that we report the error correctly + owner: z.object({ slug: zSlug, }), - async (input) => { + slug: zSlug, +}); + +export const moveModelAction = actionClient + .schema(schema) + .action(async ({ parsedInput: input }) => { const session = await getSessionOrRedirect(); const model = await getWriteableModel({ @@ -23,17 +29,24 @@ export const moveModelAction = makeServerAction( slug: input.slug, }); - const newOwner = await getWriteableOwnerBySlug(session, input.newOwner); - - const newModel = await prisma.model.update({ - where: { id: model.id }, - data: { ownerId: newOwner.id }, - select: { - slug: true, - owner: true, - }, - }); + const newOwner = await getWriteableOwnerBySlug(session, input.owner.slug); - return { model: newModel }; - } -); + try { + const newModel = await prisma.model.update({ + where: { id: model.id }, + data: { ownerId: newOwner.id }, + select: { + slug: true, + owner: true, + }, + }); + return { model: newModel }; + } catch { + returnValidationErrors(schema, { + // `owner`, not `owner.slug` - the select name from the RHF point of view is just `owner` + owner: { + _errors: [`Model ${input.slug} already exists on the target account`], + }, + }); + } + }); diff --git a/packages/hub/src/models/actions/updateModelSlugAction.ts b/packages/hub/src/models/actions/updateModelSlugAction.ts index f3506200d6..fa84dba90d 100644 --- a/packages/hub/src/models/actions/updateModelSlugAction.ts +++ b/packages/hub/src/models/actions/updateModelSlugAction.ts @@ -1,33 +1,69 @@ "use server"; +import { returnValidationErrors } from "next-safe-action"; import { z } from "zod"; import { prisma } from "@/lib/server/prisma"; -import { makeServerAction } from "@/lib/server/utils"; +import { actionClient } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { getWriteableModel } from "@/models/utils"; -export const updateModelSlugAction = makeServerAction( - z.object({ - owner: zSlug, - oldSlug: zSlug, - newSlug: zSlug, - }), - async (input) => { +const schema = z.object({ + owner: zSlug, + oldSlug: zSlug, + slug: zSlug, +}); + +export const updateModelSlugAction = actionClient + .schema(schema) + .outputSchema( + z.object({ + model: z.object({ + slug: zSlug, + owner: z.object({ + slug: zSlug, + }), + }), + }) + ) + .action(async ({ parsedInput: input }) => { const model = await getWriteableModel({ owner: input.owner, slug: input.oldSlug, }); - const newModel = await prisma.model.update({ - where: { id: model.id }, - data: { slug: input.newSlug }, - select: { - slug: true, - owner: true, - }, - }); + if (model.slug === input.slug) { + // no need to do anything + return { + model: { + slug: model.slug, + owner: { + slug: input.owner, + }, + }, + }; + } + + try { + const newModel = await prisma.model.update({ + where: { id: model.id }, + data: { slug: input.slug }, + select: { + slug: true, + owner: { + select: { + slug: true, + }, + }, + }, + }); - return { model: newModel }; - } -); + return { model: newModel }; + } catch { + returnValidationErrors(schema, { + slug: { + _errors: [`Model ${input.slug} already exists`], + }, + }); + } + }); diff --git a/packages/hub/src/models/actions/updateSquiggleSnippetModelAction.ts b/packages/hub/src/models/actions/updateSquiggleSnippetModelAction.ts index 8e628a1639..a0ab692e74 100644 --- a/packages/hub/src/models/actions/updateSquiggleSnippetModelAction.ts +++ b/packages/hub/src/models/actions/updateSquiggleSnippetModelAction.ts @@ -7,36 +7,38 @@ import { squiggleVersions } from "@quri/versioned-squiggle-components"; import { modelRoute } from "@/lib/routes"; import { prisma } from "@/lib/server/prisma"; -import { makeServerAction } from "@/lib/server/utils"; +import { actionClient, ActionError } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { getSelf, getSessionOrRedirect } from "@/users/auth"; import { getWriteableModel } from "../utils"; -export const updateSquiggleSnippetModelAction = makeServerAction( - z.object({ - owner: zSlug, - slug: zSlug, - relativeValuesExports: z.array( - z.object({ - variableName: z.string(), - definition: z.object({ - owner: zSlug, - slug: zSlug, - }), - }) - ), - content: z.object({ - code: z.string(), - version: z.string(), - seed: z.string(), - autorunMode: z.boolean().nullable(), - sampleCount: z.number().nullable(), - xyPointLength: z.number().nullable(), - }), - comment: z.string().optional(), - }), - async (input) => { +export const updateSquiggleSnippetModelAction = actionClient + .schema( + z.object({ + owner: zSlug, + slug: zSlug, + relativeValuesExports: z.array( + z.object({ + variableName: z.string(), + definition: z.object({ + owner: zSlug, + slug: zSlug, + }), + }) + ), + content: z.object({ + code: z.string(), + version: z.string(), + seed: z.string(), + autorunMode: z.boolean().nullable(), + sampleCount: z.number().nullable(), + xyPointLength: z.number().nullable(), + }), + comment: z.string().optional(), + }) + ) + .action(async ({ parsedInput: input }) => { const session = await getSessionOrRedirect(); const existingModel = await getWriteableModel({ @@ -46,7 +48,7 @@ export const updateSquiggleSnippetModelAction = makeServerAction( const version = input.content.version; if (!(squiggleVersions as readonly string[]).includes(version)) { - throw new Error(`Unknown Squiggle version ${version}`); + throw new ActionError(`Unknown Squiggle version ${version}`); } const relativeValuesExports = input.relativeValuesExports ?? []; @@ -90,7 +92,7 @@ export const updateSquiggleSnippetModelAction = makeServerAction( ?.get(pair.definition.slug); if (!definition) { - throw new Error( + throw new ActionError( `Definition with owner ${pair.definition.owner}, slug ${pair.definition.slug} not found` ); } @@ -153,5 +155,4 @@ export const updateSquiggleSnippetModelAction = makeServerAction( revalidatePath(modelRoute({ owner: input.owner, slug: input.slug })); return { model }; - } -); + }); From d733b7b161d1ae3baa5fb2a8b6f6fffe0c969bfe Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 3 Dec 2024 15:39:39 -0300 Subject: [PATCH 58/68] convert the remaining actions to nsa --- .../upgrade-versions/UpgradeableModel.tsx | 8 +- .../invite-link/AcceptGroupInvitePage.tsx | 15 +- .../[slug]/members/AddUserToGroupAction.tsx | 11 +- .../[slug]/members/DeleteMembershipAction.tsx | 15 +- .../members/GroupReusableInviteSection.tsx | 16 +- .../members/SetMembershipRoleAction.tsx | 17 +- .../[owner]/[slug]/ModelPrivacyControls.tsx | 15 +- .../BuildRelativeValuesCacheAction.tsx | 11 +- .../ClearRelativeValuesCacheAction.tsx | 11 +- .../src/app/new/definition/NewDefinition.tsx | 49 +++--- packages/hub/src/app/new/group/NewGroup.tsx | 4 +- .../DeleteRelativeValuesDefinitionAction.tsx | 10 +- .../edit/EditRelativeValuesDefinition.tsx | 50 +++--- .../src/components/ui/SafeActionButton.tsx | 21 ++- .../ui/SafeActionDropdownAction.tsx | 77 +++++++++ .../acceptReusableGroupInviteTokenAction.ts | 23 +-- .../groups/actions/addUserToGroupAction.ts | 31 ++-- .../src/groups/actions/createGroupAction.ts | 55 +++--- .../createReusableGroupInviteTokenAction.ts | 23 +-- .../groups/actions/deleteMembershipAction.ts | 31 ++-- .../deleteReusableGroupInviteTokenAction.ts | 23 +-- .../actions/updateMembershipRoleAction.ts | 32 ++-- packages/hub/src/lib/server/utils.ts | 25 --- .../src/models/actions/loadModelCardAction.ts | 23 +-- .../src/models/actions/loadModelFullAction.ts | 23 +-- .../actions/updateModelPrivacyAction.ts | 21 +-- .../actions/buildRelativeValuesCacheAction.ts | 30 ++-- .../actions/clearRelativeValuesCacheAction.ts | 19 +-- .../createRelativeValuesDefinitionAction.ts | 156 +++++++++--------- .../deleteRelativeValuesDefinitionAction.tsx | 20 ++- .../updateRelativeValuesDefinitionAction.ts | 109 ++++++------ .../RelativeValuesDefinitionForm/index.tsx | 43 +++-- .../relative-values/values/ModelEvaluator.ts | 1 + .../src/squiggle/components/ImportTooltip.tsx | 9 +- 34 files changed, 575 insertions(+), 452 deletions(-) create mode 100644 packages/hub/src/components/ui/SafeActionDropdownAction.tsx diff --git a/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx b/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx index a5636d74b3..135472af08 100644 --- a/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx +++ b/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx @@ -72,7 +72,13 @@ export const UpgradeableModel: FC<{ loadModelFullAction({ owner: incompleteModel.owner.slug, slug: incompleteModel.slug, - }).then(setModel); + }).then((result) => { + if (result?.data) { + setModel(result.data); + } else { + setModel(null); + } + }); }, []); if (model === "loading") { diff --git a/packages/hub/src/app/groups/[slug]/invite-link/AcceptGroupInvitePage.tsx b/packages/hub/src/app/groups/[slug]/invite-link/AcceptGroupInvitePage.tsx index ac2656438e..a6e2943709 100644 --- a/packages/hub/src/app/groups/[slug]/invite-link/AcceptGroupInvitePage.tsx +++ b/packages/hub/src/app/groups/[slug]/invite-link/AcceptGroupInvitePage.tsx @@ -4,7 +4,7 @@ import { FC } from "react"; import { useToast } from "@quri/ui"; -import { ServerActionButton } from "@/components/ui/ServerActionButton"; +import { SafeActionButton } from "@/components/ui/SafeActionButton"; import { acceptReusableGroupInviteTokenAction } from "@/groups/actions/acceptReusableGroupInviteTokenAction"; import { GroupCardDTO } from "@/groups/data/groupCards"; import { groupRoute } from "@/lib/routes"; @@ -19,17 +19,18 @@ export const AcceptGroupInvitePage: FC<{ return (

{`You've been invited to join ${group.slug} group.`}

- { - await acceptReusableGroupInviteTokenAction({ - groupSlug: group.slug, - inviteToken, - }); + action={acceptReusableGroupInviteTokenAction} + onSuccess={() => { toast("Joined", "confirmation"); router.push(groupRoute({ slug: group.slug })); }} + input={{ + groupSlug: group.slug, + inviteToken, + }} />
); diff --git a/packages/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx b/packages/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx index f4975f4999..410eb86945 100644 --- a/packages/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx +++ b/packages/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx @@ -4,7 +4,7 @@ import { FC } from "react"; import { PlusIcon, SelectStringFormField } from "@quri/ui"; import { SelectUser, SelectUserOption } from "@/components/SelectUser"; -import { ServerActionModalAction } from "@/components/ui/ServerActionModalAction"; +import { SafeActionModalAction } from "@/components/ui/SafeActionModalAction"; import { addUserToGroupAction } from "@/groups/actions/addUserToGroupAction"; import { GroupMemberDTO } from "@/groups/data/members"; @@ -17,13 +17,12 @@ type FormShape = { user: SelectUserOption; role: MembershipRole }; export const AddUserToGroupAction: FC = ({ groupSlug, append }) => { return ( - + title="Add" icon={PlusIcon} - action={async (data) => { - const membership = await addUserToGroupAction(data); + action={addUserToGroupAction} + onCompleted={(membership) => { append(membership); - return membership; }} defaultValues={{ role: "Member" }} formDataToVariables={(data) => ({ @@ -45,6 +44,6 @@ export const AddUserToGroupAction: FC = ({ groupSlug, append }) => { />
)} - + ); }; diff --git a/packages/hub/src/app/groups/[slug]/members/DeleteMembershipAction.tsx b/packages/hub/src/app/groups/[slug]/members/DeleteMembershipAction.tsx index 4c5bcb7b31..0abcd5e623 100644 --- a/packages/hub/src/app/groups/[slug]/members/DeleteMembershipAction.tsx +++ b/packages/hub/src/app/groups/[slug]/members/DeleteMembershipAction.tsx @@ -2,7 +2,7 @@ import { FC } from "react"; import { TrashIcon } from "@quri/ui"; -import { ServerActionDropdownAction } from "@/components/ui/ServerActionDropdownAction"; +import { SafeActionDropdownAction } from "@/components/ui/SafeActionDropdownAction"; import { deleteMembershipAction } from "@/groups/actions/deleteMembershipAction"; import { GroupMemberDTO } from "@/groups/data/members"; @@ -18,16 +18,15 @@ export const DeleteMembershipAction: FC = ({ remove, }) => { return ( - { - await deleteMembershipAction({ - group: groupSlug, - username: membership.user.slug, - }); - remove(membership); + action={deleteMembershipAction} + input={{ + group: groupSlug, + username: membership.user.slug, }} + onSuccess={() => remove(membership)} /> ); }; diff --git a/packages/hub/src/app/groups/[slug]/members/GroupReusableInviteSection.tsx b/packages/hub/src/app/groups/[slug]/members/GroupReusableInviteSection.tsx index b9001e88f8..b5b516fc98 100644 --- a/packages/hub/src/app/groups/[slug]/members/GroupReusableInviteSection.tsx +++ b/packages/hub/src/app/groups/[slug]/members/GroupReusableInviteSection.tsx @@ -5,7 +5,7 @@ import Skeleton from "react-loading-skeleton"; import { ClipboardCopyIcon, TextTooltip, useToast } from "@quri/ui"; import { H2 } from "@/components/ui/Headers"; -import { ServerActionButton } from "@/components/ui/ServerActionButton"; +import { SafeActionButton } from "@/components/ui/SafeActionButton"; import { createReusableGroupInviteTokenAction } from "@/groups/actions/createReusableGroupInviteTokenAction"; import { deleteReusableGroupInviteTokenAction } from "@/groups/actions/deleteReusableGroupInviteTokenAction"; import { groupInviteLink } from "@/lib/routes"; @@ -72,19 +72,17 @@ export const GroupReusableInviteSection: FC = ({ ) ) : null}
- - createReusableGroupInviteTokenAction({ slug: groupSlug }) - } + {reusableInviteToken ? ( - - deleteReusableGroupInviteTokenAction({ slug: groupSlug }) - } + ) : null} diff --git a/packages/hub/src/app/groups/[slug]/members/SetMembershipRoleAction.tsx b/packages/hub/src/app/groups/[slug]/members/SetMembershipRoleAction.tsx index cad0a7c669..518463021c 100644 --- a/packages/hub/src/app/groups/[slug]/members/SetMembershipRoleAction.tsx +++ b/packages/hub/src/app/groups/[slug]/members/SetMembershipRoleAction.tsx @@ -1,7 +1,7 @@ import { MembershipRole } from "@prisma/client"; import { FC } from "react"; -import { ServerActionDropdownAction } from "@/components/ui/ServerActionDropdownAction"; +import { SafeActionDropdownAction } from "@/components/ui/SafeActionDropdownAction"; import { updateMembershipRoleAction } from "@/groups/actions/updateMembershipRoleAction"; import { GroupMemberDTO } from "@/groups/data/members"; @@ -19,13 +19,14 @@ export const SetMembershipRoleAction: FC = ({ update, }) => { return ( - { - const newMembership = await updateMembershipRoleAction({ - user: membership.user.slug, - group: groupSlug, - role, - }); + { update(newMembership); }} title={role} diff --git a/packages/hub/src/app/models/[owner]/[slug]/ModelPrivacyControls.tsx b/packages/hub/src/app/models/[owner]/[slug]/ModelPrivacyControls.tsx index db1d65b745..4f847b6832 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/ModelPrivacyControls.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/ModelPrivacyControls.tsx @@ -4,7 +4,7 @@ import { FC } from "react"; import { Dropdown, DropdownMenu, GlobeIcon, LockIcon } from "@quri/ui"; -import { ServerActionDropdownAction } from "@/components/ui/ServerActionDropdownAction"; +import { SafeActionDropdownAction } from "@/components/ui/SafeActionDropdownAction"; import { updateModelPrivacyAction } from "@/models/actions/updateModelPrivacyAction"; import { ModelCardDTO } from "@/models/data/cards"; @@ -16,13 +16,12 @@ const UpdatePrivacyAction: FC<{ model: ModelCardDTO; }> = ({ model }) => { return ( - { - await updateModelPrivacyAction({ - owner: model.owner.slug, - slug: model.slug, - isPrivate: !model.isPrivate, - }); + { - await buildRelativeValuesCacheAction({ - exportId: relativeValuesExport.id, - }); + action={buildRelativeValuesCacheAction} + input={{ exportId: relativeValuesExport.id }} + onSuccess={() => { toast("Cache filled", "confirmation"); }} invariant={1} // close is controlled by the parent diff --git a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/ClearRelativeValuesCacheAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/ClearRelativeValuesCacheAction.tsx index 5748ad38a5..3e83870a78 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/ClearRelativeValuesCacheAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/relative-values/[variableName]/CacheMenu/ClearRelativeValuesCacheAction.tsx @@ -3,7 +3,7 @@ import { FC } from "react"; import { TrashIcon, useToast } from "@quri/ui"; -import { ServerActionDropdownAction } from "@/components/ui/ServerActionDropdownAction"; +import { SafeActionDropdownAction } from "@/components/ui/SafeActionDropdownAction"; import { clearRelativeValuesCacheAction } from "@/relative-values/actions/clearRelativeValuesCacheAction"; import { RelativeValuesExportFullDTO } from "@/relative-values/data/fullExport"; @@ -14,13 +14,12 @@ export const ClearRelativeValuesCacheAction: FC<{ const toast = useToast(); return ( - { - await clearRelativeValuesCacheAction({ - exportId: relativeValuesExport.id, - }); + action={clearRelativeValuesCacheAction} + input={{ exportId: relativeValuesExport.id }} + onSuccess={() => { toast("Cache cleared", "confirmation"); }} invariant={1} // close is controlled by the parent diff --git a/packages/hub/src/app/new/definition/NewDefinition.tsx b/packages/hub/src/app/new/definition/NewDefinition.tsx index 2dda823d86..2900d24b1f 100644 --- a/packages/hub/src/app/new/definition/NewDefinition.tsx +++ b/packages/hub/src/app/new/definition/NewDefinition.tsx @@ -7,38 +7,37 @@ import { H1 } from "@/components/ui/Headers"; import { relativeValuesRoute } from "@/lib/routes"; import { createRelativeValuesDefinitionAction } from "@/relative-values/actions/createRelativeValuesDefinitionAction"; import { RelativeValuesDefinitionForm } from "@/relative-values/components/RelativeValuesDefinitionForm"; -import { FormShape } from "@/relative-values/components/RelativeValuesDefinitionForm/FormShape"; export const NewDefinition: FC = () => { const router = useRouter(); - const save = async (data: FormShape) => { - const result = await createRelativeValuesDefinitionAction({ - slug: data.slug, - title: data.title, - items: data.items.map((item) => ({ - ...item, - clusterId: item.clusterId ?? undefined, - })), - clusters: data.clusters.map((cluster) => ({ - ...cluster, - recommendedUnit: cluster.recommendedUnit ?? undefined, - })), - recommendedUnit: data.recommendedUnit ?? undefined, - }); - router.push( - relativeValuesRoute({ - owner: result.owner, - slug: result.slug, - }) - ); - // confirmation: "Definition created", - }; - return (

New Relative Values definition

- + ({ + slug: data.slug, + title: data.title, + items: data.items.map((item) => ({ + ...item, + clusterId: item.clusterId ?? undefined, + })), + clusters: data.clusters.map((cluster) => ({ + ...cluster, + recommendedUnit: cluster.recommendedUnit ?? undefined, + })), + recommendedUnit: data.recommendedUnit ?? undefined, + })} + onCompleted={(data) => { + router.push( + relativeValuesRoute({ + owner: data.owner, + slug: data.slug, + }) + ); + }} + />
); }; diff --git a/packages/hub/src/app/new/group/NewGroup.tsx b/packages/hub/src/app/new/group/NewGroup.tsx index 03fda16f2f..6e28babaf7 100644 --- a/packages/hub/src/app/new/group/NewGroup.tsx +++ b/packages/hub/src/app/new/group/NewGroup.tsx @@ -8,7 +8,7 @@ import { Button } from "@quri/ui"; import { H1 } from "@/components/ui/Headers"; import { SlugFormField } from "@/components/ui/SlugFormField"; import { createGroupAction } from "@/groups/actions/createGroupAction"; -import { useServerActionForm } from "@/lib/hooks/useServerActionForm"; +import { useSafeActionForm } from "@/lib/hooks/useSafeActionForm"; import { groupRoute } from "@/lib/routes"; export const NewGroup: FC = () => { @@ -18,7 +18,7 @@ export const NewGroup: FC = () => { slug: string | undefined; }; - const { form, onSubmit, inFlight } = useServerActionForm< + const { form, onSubmit, inFlight } = useSafeActionForm< FormShape, typeof createGroupAction >({ diff --git a/packages/hub/src/app/relative-values/[owner]/[slug]/DeleteRelativeValuesDefinitionAction.tsx b/packages/hub/src/app/relative-values/[owner]/[slug]/DeleteRelativeValuesDefinitionAction.tsx index 7dd94bf231..f7b6e3672f 100644 --- a/packages/hub/src/app/relative-values/[owner]/[slug]/DeleteRelativeValuesDefinitionAction.tsx +++ b/packages/hub/src/app/relative-values/[owner]/[slug]/DeleteRelativeValuesDefinitionAction.tsx @@ -1,8 +1,9 @@ import { useRouter } from "next/navigation"; import { FC } from "react"; -import { DropdownMenuAsyncActionItem, TrashIcon, useToast } from "@quri/ui"; +import { TrashIcon, useToast } from "@quri/ui"; +import { SafeActionDropdownAction } from "@/components/ui/SafeActionDropdownAction"; import { deleteRelativeValuesDefinitionAction } from "@/relative-values/actions/deleteRelativeValuesDefinitionAction"; type Props = { @@ -16,10 +17,11 @@ export const DeleteDefinitionAction: FC = ({ owner, slug }) => { const toast = useToast(); return ( - { - await deleteRelativeValuesDefinitionAction({ owner, slug }); + action={deleteRelativeValuesDefinitionAction} + input={{ owner, slug }} + onSuccess={() => { toast("Definition deleted", "confirmation"); router.push("/"); }} diff --git a/packages/hub/src/app/relative-values/[owner]/[slug]/edit/EditRelativeValuesDefinition.tsx b/packages/hub/src/app/relative-values/[owner]/[slug]/edit/EditRelativeValuesDefinition.tsx index 60761875f6..d736b31d51 100644 --- a/packages/hub/src/app/relative-values/[owner]/[slug]/edit/EditRelativeValuesDefinition.tsx +++ b/packages/hub/src/app/relative-values/[owner]/[slug]/edit/EditRelativeValuesDefinition.tsx @@ -5,7 +5,6 @@ import { FC } from "react"; import { relativeValuesRoute } from "@/lib/routes"; import { updateRelativeValuesDefinitionAction } from "@/relative-values/actions/updateRelativeValuesDefinitionAction"; import { RelativeValuesDefinitionForm } from "@/relative-values/components/RelativeValuesDefinitionForm"; -import { FormShape } from "@/relative-values/components/RelativeValuesDefinitionForm/FormShape"; import { RelativeValuesDefinitionFullDTO } from "@/relative-values/data/full"; export const EditRelativeValuesDefinition: FC<{ @@ -15,31 +14,9 @@ export const EditRelativeValuesDefinition: FC<{ const revision = definition.currentRevision; - const save = async (data: FormShape) => { - await updateRelativeValuesDefinitionAction({ - slug: definition.slug, - owner: definition.owner.slug, - title: data.title, - items: data.items.map((item) => ({ - ...item, - clusterId: item.clusterId ?? undefined, - })), - clusters: data.clusters.map((cluster) => ({ - ...cluster, - recommendedUnit: cluster.recommendedUnit || undefined, - })), - recommendedUnit: data.recommendedUnit || undefined, - }); - router.push( - relativeValuesRoute({ - owner: definition.owner.slug, - slug: definition.slug, - }) - ); - }; - return ( ({ + slug: definition.slug, + owner: definition.owner.slug, + title: data.title, + items: data.items.map((item) => ({ + ...item, + clusterId: item.clusterId ?? undefined, + })), + clusters: data.clusters.map((cluster) => ({ + ...cluster, + recommendedUnit: cluster.recommendedUnit || undefined, + })), + recommendedUnit: data.recommendedUnit || undefined, + })} + onCompleted={(data) => { + router.push( + relativeValuesRoute({ + owner: data.owner, + slug: data.slug, + }) + ); + }} /> ); }; diff --git a/packages/hub/src/components/ui/SafeActionButton.tsx b/packages/hub/src/components/ui/SafeActionButton.tsx index 5db1e9dd33..309cd5c933 100644 --- a/packages/hub/src/components/ui/SafeActionButton.tsx +++ b/packages/hub/src/components/ui/SafeActionButton.tsx @@ -1,12 +1,15 @@ "use client"; -import { InferSafeActionFnInput } from "next-safe-action"; +import { + InferSafeActionFnInput, + InferSafeActionFnResult, +} from "next-safe-action"; import { HookSafeActionFn, useAction } from "next-safe-action/hooks"; import { ReactNode } from "react"; import { Button, useToast } from "@quri/ui"; export function SafeActionButton< - const T extends HookSafeActionFn, + const Action extends HookSafeActionFn, >({ action, input, @@ -17,9 +20,11 @@ export function SafeActionButton< theme, size, }: { - action: T; + action: Action; input: InferSafeActionFnInput["clientInput"]; - onSuccess?: () => void; + onSuccess?: ( + data: NonNullable["data"]> + ) => void; title: string; confirmation?: string; } & Pick[0], "theme" | "size">): ReactNode { @@ -27,10 +32,12 @@ export function SafeActionButton< const { execute, isPending } = useAction(action, { onSuccess: ({ data }) => { - if (data && confirmation) { - toast(confirmation, "confirmation"); + if (data) { + if (confirmation) { + toast(confirmation, "confirmation"); + } + onSuccess?.(data); } - onSuccess?.(); }, onError: ({ error }) => { toast( diff --git a/packages/hub/src/components/ui/SafeActionDropdownAction.tsx b/packages/hub/src/components/ui/SafeActionDropdownAction.tsx new file mode 100644 index 0000000000..0f2a4ddb1e --- /dev/null +++ b/packages/hub/src/components/ui/SafeActionDropdownAction.tsx @@ -0,0 +1,77 @@ +import { + InferSafeActionFnInput, + InferSafeActionFnResult, +} from "next-safe-action"; +import { HookSafeActionFn, useAction } from "next-safe-action/hooks"; +import { FC } from "react"; + +import { + DropdownMenuActionItem, + IconProps, + useCloseDropdown, + useToast, +} from "@quri/ui"; + +import { useCloseDropdownOnInvariantChange } from "./CloseDropdownOnInvariantChange"; + +export function SafeActionDropdownAction< + const T extends HookSafeActionFn, +>({ + title, + icon, + action, + input, + invariant, + onSuccess, +}: { + title: string; + icon?: FC; + action: T; + input: InferSafeActionFnInput["clientInput"]; + onSuccess?: ( + data: NonNullable["data"]> + ) => void; + // If set, the dropdown will close only after the invariant changes. + // This is useful, because server action returns before it sends back the revalidated UI. + // Re-rendering the new UI might take a while (it's async), so we don't want to close the dropdown immediately. + // This is an ugly workaround; see also: https://github.com/vercel/next.js/discussions/53206 + // Discussion in QURI Slack: https://quri.slack.com/archives/C059EEU0HMM/p1732810277978719 + // + // Also note that in some cases even `invariant` is not enough. Consider the scenario where the list of items in the dropdown is based on the component props. + // In this case, this action would be unmounted before it would get the chance to close the dropdown. + // In that scenario you might prefer to use `` instead. + // (The example of this is ``.) + invariant?: unknown; +}) { + const close = useCloseDropdown(); + const toast = useToast(); + + const { execute, isPending } = useAction(action, { + onSuccess: ({ data }) => { + if (data && invariant === undefined) { + onSuccess?.(data); + // if there's no invariant, close the dropdown immediately + if (invariant === undefined) { + close(); + } + } + }, + onError: ({ error }) => { + toast( + error.serverError ? String(error.serverError) : "Internal error", + "error" + ); + }, + }); + + useCloseDropdownOnInvariantChange(invariant); + + return ( + execute(input)} + /> + ); +} diff --git a/packages/hub/src/groups/actions/acceptReusableGroupInviteTokenAction.ts b/packages/hub/src/groups/actions/acceptReusableGroupInviteTokenAction.ts index c51ba2b590..ea94c6399a 100644 --- a/packages/hub/src/groups/actions/acceptReusableGroupInviteTokenAction.ts +++ b/packages/hub/src/groups/actions/acceptReusableGroupInviteTokenAction.ts @@ -6,22 +6,24 @@ import { z } from "zod"; import { getMyMembership } from "@/groups/helpers"; import { groupMembersRoute } from "@/lib/routes"; import { prisma } from "@/lib/server/prisma"; -import { makeServerAction } from "@/lib/server/utils"; +import { actionClient, ActionError } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { getSessionOrRedirect } from "@/users/auth"; import { validateReusableGroupInviteToken } from "../data/helpers"; -export const acceptReusableGroupInviteTokenAction = makeServerAction( - z.object({ - groupSlug: zSlug, - inviteToken: z.string(), - }), - async (input) => { +export const acceptReusableGroupInviteTokenAction = actionClient + .schema( + z.object({ + groupSlug: zSlug, + inviteToken: z.string(), + }) + ) + .action(async ({ parsedInput: input }) => { const session = await getSessionOrRedirect(); if (!(await validateReusableGroupInviteToken(input))) { - throw new Error("Invalid token"); + throw new ActionError("Invalid token"); } const group = await prisma.group.findFirstOrThrow({ @@ -37,7 +39,7 @@ export const acceptReusableGroupInviteTokenAction = makeServerAction( groupSlug: input.groupSlug, }); if (myMembership) { - throw new Error("You're already a member of this group"); + throw new ActionError("You're already a member of this group"); } await prisma.userGroupMembership.create({ @@ -57,5 +59,4 @@ export const acceptReusableGroupInviteTokenAction = makeServerAction( }); revalidatePath(groupMembersRoute({ slug: input.groupSlug })); - } -); + }); diff --git a/packages/hub/src/groups/actions/addUserToGroupAction.ts b/packages/hub/src/groups/actions/addUserToGroupAction.ts index 4bb1031094..b5a0c6381d 100644 --- a/packages/hub/src/groups/actions/addUserToGroupAction.ts +++ b/packages/hub/src/groups/actions/addUserToGroupAction.ts @@ -4,7 +4,7 @@ import { MembershipRole } from "@prisma/client"; import { z } from "zod"; import { prisma } from "@/lib/server/prisma"; -import { makeServerAction } from "@/lib/server/utils"; +import { actionClient, ActionError } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { getSessionOrRedirect } from "@/users/auth"; @@ -14,13 +14,17 @@ import { membershipToDTO, } from "../data/members"; -export const addUserToGroupAction = makeServerAction( - z.object({ - group: zSlug, - username: zSlug, - role: z.enum(Object.keys(MembershipRole) as [keyof typeof MembershipRole]), - }), - async (input): Promise => { +export const addUserToGroupAction = actionClient + .schema( + z.object({ + group: zSlug, + username: zSlug, + role: z.enum( + Object.keys(MembershipRole) as [keyof typeof MembershipRole] + ), + }) + ) + .action(async ({ parsedInput: input }): Promise => { const session = await getSessionOrRedirect(); const membership = await prisma.$transaction(async (tx) => { @@ -30,7 +34,7 @@ export const addUserToGroupAction = makeServerAction( }, }); if (!groupOwner) { - throw new Error(`Group ${input.group} not found`); + throw new ActionError(`Group ${input.group} not found`); } const requestedUser = await tx.user.findFirst({ @@ -42,7 +46,7 @@ export const addUserToGroupAction = makeServerAction( }); if (!requestedUser) { - throw new Error(`User ${input.username} not found`); + throw new ActionError(`User ${input.username} not found`); } // We perform all checks one by one because that allows more precise error reporting. @@ -59,7 +63,7 @@ export const addUserToGroupAction = makeServerAction( }, }); if (!isAdmin) { - throw new Error(`You're not an admin of ${input.group} group`); + throw new ActionError(`You're not an admin of ${input.group} group`); } const alreadyAMember = await tx.group.count({ @@ -71,7 +75,7 @@ export const addUserToGroupAction = makeServerAction( }, }); if (alreadyAMember) { - throw new Error( + throw new ActionError( `${input.username} is already a member of ${input.group}` ); } @@ -103,5 +107,4 @@ export const addUserToGroupAction = makeServerAction( }); return membershipToDTO(membership); - } -); + }); diff --git a/packages/hub/src/groups/actions/createGroupAction.ts b/packages/hub/src/groups/actions/createGroupAction.ts index 66326c0073..2f06da9ed7 100644 --- a/packages/hub/src/groups/actions/createGroupAction.ts +++ b/packages/hub/src/groups/actions/createGroupAction.ts @@ -1,45 +1,50 @@ "use server"; +import { returnValidationErrors } from "next-safe-action"; import { z } from "zod"; import { prisma } from "@/lib/server/prisma"; -import { makeServerAction, rethrowOnConstraint } from "@/lib/server/utils"; +import { actionClient } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { indexGroupId } from "@/search/helpers"; import { getSessionOrRedirect } from "@/users/auth"; -export const createGroupAction = makeServerAction( - z.object({ - slug: zSlug, - }), - async (input): Promise<{ slug: string }> => { +const schema = z.object({ + slug: zSlug, +}); + +export const createGroupAction = actionClient + .schema(schema) + .action(async ({ parsedInput: input }): Promise<{ slug: string }> => { const session = await getSessionOrRedirect(); const user = await prisma.user.findUniqueOrThrow({ where: { email: session.user.email }, }); - const group = await rethrowOnConstraint( - () => - prisma.group.create({ - data: { - asOwner: { - create: { - slug: input.slug, - }, - }, - memberships: { - create: [{ userId: user.id, role: "Admin" }], + let group: { id: string }; + try { + group = await prisma.group.create({ + data: { + asOwner: { + create: { + slug: input.slug, }, }, - }), - { - target: ["slug"], - error: `The group ${input.slug} already exists`, - } - ); + memberships: { + create: [{ userId: user.id, role: "Admin" }], + }, + }, + select: { id: true }, + }); + } catch { + returnValidationErrors(schema, { + slug: { + _errors: [`Group ${input.slug} already exists`], + }, + }); + } await indexGroupId(group.id); return { slug: input.slug }; - } -); + }); diff --git a/packages/hub/src/groups/actions/createReusableGroupInviteTokenAction.ts b/packages/hub/src/groups/actions/createReusableGroupInviteTokenAction.ts index 5cc8855785..1b7025163b 100644 --- a/packages/hub/src/groups/actions/createReusableGroupInviteTokenAction.ts +++ b/packages/hub/src/groups/actions/createReusableGroupInviteTokenAction.ts @@ -5,7 +5,7 @@ import { z } from "zod"; import { groupMembersRoute } from "@/lib/routes"; import { prisma } from "@/lib/server/prisma"; -import { makeServerAction } from "@/lib/server/utils"; +import { actionClient, ActionError } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { loadMyMembership } from "../data/members"; @@ -17,17 +17,21 @@ import { loadMyMembership } from "../data/members"; * You must be an admin of the group to call this mutation. Previous invite * token, if it existed, will stop working. */ -export const createReusableGroupInviteTokenAction = makeServerAction( - z.object({ - slug: zSlug, - }), - async (input): Promise => { +export const createReusableGroupInviteTokenAction = actionClient + .schema( + z.object({ + slug: zSlug, + }) + ) + .action(async ({ parsedInput: input }): Promise => { const myMembership = await loadMyMembership({ groupSlug: input.slug }); if (!myMembership) { - throw new Error("Not a member of this group"); + throw new ActionError("You're not a member of this group"); } if (myMembership.role !== "Admin") { - throw new Error("Only group admins can delete reusable invite tokens"); + throw new ActionError( + "Only group admins can create reusable invite tokens" + ); } const group = await prisma.group.findFirstOrThrow({ @@ -52,5 +56,4 @@ export const createReusableGroupInviteTokenAction = makeServerAction( }); revalidatePath(groupMembersRoute({ slug: input.slug })); - } -); + }); diff --git a/packages/hub/src/groups/actions/deleteMembershipAction.ts b/packages/hub/src/groups/actions/deleteMembershipAction.ts index 87c01f8fd6..6a65fa4e28 100644 --- a/packages/hub/src/groups/actions/deleteMembershipAction.ts +++ b/packages/hub/src/groups/actions/deleteMembershipAction.ts @@ -6,18 +6,20 @@ import { z } from "zod"; import { getMembership, getMyMembership } from "@/groups/helpers"; import { groupMembersRoute } from "@/lib/routes"; import { prisma } from "@/lib/server/prisma"; -import { makeServerAction } from "@/lib/server/utils"; +import { actionClient, ActionError } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { getSessionOrRedirect } from "@/users/auth"; import { groupHasAdminsBesidesUser } from "../data/helpers"; -export const deleteMembershipAction = makeServerAction( - z.object({ - group: zSlug, - username: zSlug, - }), - async (input) => { +export const deleteMembershipAction = actionClient + .schema( + z.object({ + group: zSlug, + username: zSlug, + }) + ) + .action(async ({ parsedInput: input }): Promise<"ok"> => { const session = await getSessionOrRedirect(); // somewhat repetitive compared to `updateMembershipRole`, but with slightly different error messages @@ -26,14 +28,14 @@ export const deleteMembershipAction = makeServerAction( }); if (!myMembership) { - throw new Error("You're not a member of this group"); + throw new ActionError("You're not a member of this group"); } if ( input.username !== session.user.username && myMembership.role !== "Admin" ) { - throw new Error("Only admins can delete other members"); + throw new ActionError("Only admins can delete other members"); } const membershipToDelete = await getMembership({ @@ -42,7 +44,9 @@ export const deleteMembershipAction = makeServerAction( }); if (!membershipToDelete) { - throw new Error(`${input.username} is not a member of ${input.group}`); + throw new ActionError( + `${input.username} is not a member of ${input.group}` + ); } if ( @@ -51,7 +55,7 @@ export const deleteMembershipAction = makeServerAction( userSlug: input.username, })) ) { - throw new Error( + throw new ActionError( `Can't delete, ${input.username} is the last admin of ${input.group}` ); } @@ -63,5 +67,6 @@ export const deleteMembershipAction = makeServerAction( }); revalidatePath(groupMembersRoute({ slug: input.group })); - } -); + + return "ok"; + }); diff --git a/packages/hub/src/groups/actions/deleteReusableGroupInviteTokenAction.ts b/packages/hub/src/groups/actions/deleteReusableGroupInviteTokenAction.ts index 3eb3819b8b..127dd879a3 100644 --- a/packages/hub/src/groups/actions/deleteReusableGroupInviteTokenAction.ts +++ b/packages/hub/src/groups/actions/deleteReusableGroupInviteTokenAction.ts @@ -4,23 +4,27 @@ import { z } from "zod"; import { groupMembersRoute } from "@/lib/routes"; import { prisma } from "@/lib/server/prisma"; -import { makeServerAction } from "@/lib/server/utils"; +import { actionClient, ActionError } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { loadMyMembership } from "../data/members"; // Disable a reusable invite token for a group. -export const deleteReusableGroupInviteTokenAction = makeServerAction( - z.object({ - slug: zSlug, - }), - async (input): Promise => { +export const deleteReusableGroupInviteTokenAction = actionClient + .schema( + z.object({ + slug: zSlug, + }) + ) + .action(async ({ parsedInput: input }): Promise => { const myMembership = await loadMyMembership({ groupSlug: input.slug }); if (!myMembership) { - throw new Error("Not a member of this group"); + throw new ActionError("Not a member of this group"); } if (myMembership.role !== "Admin") { - throw new Error("Only group admins can delete reusable invite tokens"); + throw new ActionError( + "Only group admins can delete reusable invite tokens" + ); } const group = await prisma.group.findFirstOrThrow({ @@ -37,5 +41,4 @@ export const deleteReusableGroupInviteTokenAction = makeServerAction( }); revalidatePath(groupMembersRoute({ slug: input.slug })); - } -); + }); diff --git a/packages/hub/src/groups/actions/updateMembershipRoleAction.ts b/packages/hub/src/groups/actions/updateMembershipRoleAction.ts index d094e4adb1..90fc178d1c 100644 --- a/packages/hub/src/groups/actions/updateMembershipRoleAction.ts +++ b/packages/hub/src/groups/actions/updateMembershipRoleAction.ts @@ -4,26 +4,29 @@ import { MembershipRole } from "@prisma/client"; import { z } from "zod"; import { prisma } from "@/lib/server/prisma"; -import { makeServerAction } from "@/lib/server/utils"; +import { actionClient, ActionError } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { getSessionOrRedirect } from "@/users/auth"; import { groupHasAdminsBesidesUser } from "../data/helpers"; import { - GroupMemberDTO, loadMembership, loadMyMembership, membershipSelect, membershipToDTO, } from "../data/members"; -export const updateMembershipRoleAction = makeServerAction( - z.object({ - group: zSlug, - user: zSlug, - role: z.enum(Object.keys(MembershipRole) as [keyof typeof MembershipRole]), - }), - async (input): Promise => { +export const updateMembershipRoleAction = actionClient + .schema( + z.object({ + group: zSlug, + user: zSlug, + role: z.enum( + Object.keys(MembershipRole) as [keyof typeof MembershipRole] + ), + }) + ) + .action(async ({ parsedInput: input }) => { const session = await getSessionOrRedirect(); // somewhat repetitive compared to `deleteMembership`, but with slightly different error messages @@ -32,11 +35,11 @@ export const updateMembershipRoleAction = makeServerAction( }); if (!myMembership) { - throw new Error("You're not a member of this group"); + throw new ActionError("You're not a member of this group"); } if (input.user !== session.user.username && myMembership.role !== "Admin") { - throw new Error("Only admins can update other members roles"); + throw new ActionError("Only admins can update other members roles"); } const membershipToUpdate = await loadMembership({ @@ -45,7 +48,7 @@ export const updateMembershipRoleAction = makeServerAction( }); if (!membershipToUpdate) { - throw new Error(`${input.user} is not a member of ${input.group}`); + throw new ActionError(`${input.user} is not a member of ${input.group}`); } if (membershipToUpdate.role === input.role) { @@ -58,7 +61,7 @@ export const updateMembershipRoleAction = makeServerAction( userSlug: input.user, })) ) { - throw new Error( + throw new ActionError( `Can't change the role, ${input.user} is the last admin of ${input.group}` ); } @@ -70,5 +73,4 @@ export const updateMembershipRoleAction = makeServerAction( }); return membershipToDTO(updatedMembership); - } -); + }); diff --git a/packages/hub/src/lib/server/utils.ts b/packages/hub/src/lib/server/utils.ts index 47c5dbb68b..9dd11754d4 100644 --- a/packages/hub/src/lib/server/utils.ts +++ b/packages/hub/src/lib/server/utils.ts @@ -3,31 +3,6 @@ import { createSafeActionClient, DEFAULT_SERVER_ERROR_MESSAGE, } from "next-safe-action"; -import { z } from "zod"; - -export type DeepReadonly = T extends (infer R)[] - ? DeepReadonlyArray - : T extends object - ? DeepReadonlyObject - : T; - -interface DeepReadonlyArray extends ReadonlyArray> {} - -type DeepReadonlyObject = { - readonly [P in keyof T]: DeepReadonly; -}; - -export function makeServerAction( - schema: z.ZodType, - handler: (input: T) => Promise -) { - return async ( - data: DeepReadonly // data type is unknown/unsafe, but we will validate it immediately - ) => { - const input = schema.parse(data); - return handler(input); - }; -} export class ActionError extends Error {} diff --git a/packages/hub/src/models/actions/loadModelCardAction.ts b/packages/hub/src/models/actions/loadModelCardAction.ts index fad32f5922..c88d9a9498 100644 --- a/packages/hub/src/models/actions/loadModelCardAction.ts +++ b/packages/hub/src/models/actions/loadModelCardAction.ts @@ -1,19 +1,22 @@ "use server"; import { z } from "zod"; -import { makeServerAction } from "@/lib/server/utils"; +import { actionClient } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { loadModelCard, ModelCardDTO } from "../data/cards"; // Data-fetching action, used in ImportTooltip. // Don't use this for loading models; server actions are discouraged for data fetching. -export const loadModelCardAction = makeServerAction( - z.object({ - owner: zSlug, - slug: zSlug, - }), - async ({ owner, slug }): Promise => { - return loadModelCard({ owner, slug }); - } -); +export const loadModelCardAction = actionClient + .schema( + z.object({ + owner: zSlug, + slug: zSlug, + }) + ) + .action( + async ({ parsedInput: { owner, slug } }): Promise => { + return loadModelCard({ owner, slug }); + } + ); diff --git a/packages/hub/src/models/actions/loadModelFullAction.ts b/packages/hub/src/models/actions/loadModelFullAction.ts index d476bae0ba..a5509288ed 100644 --- a/packages/hub/src/models/actions/loadModelFullAction.ts +++ b/packages/hub/src/models/actions/loadModelFullAction.ts @@ -1,19 +1,22 @@ "use server"; import { z } from "zod"; -import { makeServerAction } from "@/lib/server/utils"; +import { actionClient } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { loadModelFull, ModelFullDTO } from "../data/full"; // Data-fetching action, used in /admin/upgrade-versions. // Don't use this for loading models; server actions are discouraged for data fetching. -export const loadModelFullAction = makeServerAction( - z.object({ - owner: zSlug, - slug: zSlug, - }), - async ({ owner, slug }): Promise => { - return loadModelFull({ owner, slug }); - } -); +export const loadModelFullAction = actionClient + .schema( + z.object({ + owner: zSlug, + slug: zSlug, + }) + ) + .action( + async ({ parsedInput: { owner, slug } }): Promise => { + return loadModelFull({ owner, slug }); + } + ); diff --git a/packages/hub/src/models/actions/updateModelPrivacyAction.ts b/packages/hub/src/models/actions/updateModelPrivacyAction.ts index fc58102c63..62c33efd3c 100644 --- a/packages/hub/src/models/actions/updateModelPrivacyAction.ts +++ b/packages/hub/src/models/actions/updateModelPrivacyAction.ts @@ -5,17 +5,19 @@ import { z } from "zod"; import { modelRoute } from "@/lib/routes"; import { prisma } from "@/lib/server/prisma"; -import { makeServerAction } from "@/lib/server/utils"; +import { actionClient } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { getWriteableModel } from "@/models/utils"; -export const updateModelPrivacyAction = makeServerAction( - z.object({ - owner: zSlug, - slug: zSlug, - isPrivate: z.boolean(), - }), - async (input) => { +export const updateModelPrivacyAction = actionClient + .schema( + z.object({ + owner: zSlug, + slug: zSlug, + isPrivate: z.boolean(), + }) + ) + .action(async ({ parsedInput: input }) => { const model = await getWriteableModel({ slug: input.slug, owner: input.owner, @@ -29,5 +31,4 @@ export const updateModelPrivacyAction = makeServerAction( revalidatePath(modelRoute({ owner: input.owner, slug: input.slug })); return { isPrivate: newModel.isPrivate }; - } -); + }); diff --git a/packages/hub/src/relative-values/actions/buildRelativeValuesCacheAction.ts b/packages/hub/src/relative-values/actions/buildRelativeValuesCacheAction.ts index c629f46bdf..ed49589e1b 100644 --- a/packages/hub/src/relative-values/actions/buildRelativeValuesCacheAction.ts +++ b/packages/hub/src/relative-values/actions/buildRelativeValuesCacheAction.ts @@ -4,20 +4,20 @@ import { z } from "zod"; import { modelForRelativeValuesExportRoute } from "@/lib/routes"; import { prisma } from "@/lib/server/prisma"; -import { makeServerAction } from "@/lib/server/utils"; +import { actionClient, ActionError } from "@/lib/server/utils"; import { cartesianProduct } from "@/relative-values/lib/utils"; import { relativeValuesItemsSchema } from "@/relative-values/types"; import { ModelEvaluator } from "@/relative-values/values/ModelEvaluator"; import { getRelativeValuesExportForWriteableModel } from "../utils"; -export const buildRelativeValuesCacheAction = makeServerAction( - z.object({ - exportId: z.string(), - }), - async (input): Promise => { - const exportId = input.exportId; - +export const buildRelativeValuesCacheAction = actionClient + .schema( + z.object({ + exportId: z.string(), + }) + ) + .action(async ({ parsedInput: { exportId } }): Promise => { const relativeValuesExport = await getRelativeValuesExportForWriteableModel( { exportId, @@ -27,12 +27,12 @@ export const buildRelativeValuesCacheAction = makeServerAction( const { modelRevision } = relativeValuesExport; if (modelRevision.contentType !== "SquiggleSnippet") { - throw new Error("Unsupported model revision content type"); + throw new ActionError("Unsupported model revision content type"); } const squiggleSnippet = modelRevision.squiggleSnippet; if (!squiggleSnippet) { - throw new Error("Model content not found"); + throw new ActionError("Model content not found"); } const evaluatorResult = await ModelEvaluator.create( @@ -40,7 +40,7 @@ export const buildRelativeValuesCacheAction = makeServerAction( relativeValuesExport.variableName ); if (!evaluatorResult.ok) { - throw new Error( + throw new ActionError( `Failed to create evaluator: ${evaluatorResult.value.toString()}` ); } @@ -48,7 +48,7 @@ export const buildRelativeValuesCacheAction = makeServerAction( const definitionRevision = relativeValuesExport.definition.currentRevision; if (!definitionRevision) { - throw new Error("Definition revision not found"); + throw new ActionError("Definition revision not found"); } const items = relativeValuesItemsSchema.parse(definitionRevision.items); @@ -95,9 +95,6 @@ export const buildRelativeValuesCacheAction = makeServerAction( }, }); - // sleep - await new Promise((resolve) => setTimeout(resolve, 1000)); - revalidatePath( modelForRelativeValuesExportRoute({ owner: relativeValuesExport.modelRevision.model.owner.slug, @@ -105,5 +102,4 @@ export const buildRelativeValuesCacheAction = makeServerAction( variableName: relativeValuesExport.variableName, }) ); - } -); + }); diff --git a/packages/hub/src/relative-values/actions/clearRelativeValuesCacheAction.ts b/packages/hub/src/relative-values/actions/clearRelativeValuesCacheAction.ts index 4a858bd66c..185a5633e9 100644 --- a/packages/hub/src/relative-values/actions/clearRelativeValuesCacheAction.ts +++ b/packages/hub/src/relative-values/actions/clearRelativeValuesCacheAction.ts @@ -4,16 +4,16 @@ import { z } from "zod"; import { modelForRelativeValuesExportRoute } from "@/lib/routes"; import { prisma } from "@/lib/server/prisma"; -import { makeServerAction } from "@/lib/server/utils"; +import { actionClient } from "@/lib/server/utils"; import { getRelativeValuesExportForWriteableModel } from "@/relative-values/utils"; -export const clearRelativeValuesCacheAction = makeServerAction( - z.object({ - exportId: z.string(), - }), - async (input): Promise => { - const exportId = input.exportId; - +export const clearRelativeValuesCacheAction = actionClient + .schema( + z.object({ + exportId: z.string(), + }) + ) + .action(async ({ parsedInput: { exportId } }): Promise => { await getRelativeValuesExportForWriteableModel({ exportId, }); @@ -49,5 +49,4 @@ export const clearRelativeValuesCacheAction = makeServerAction( variableName: relativeValuesExport.variableName, }) ); - } -); + }); diff --git a/packages/hub/src/relative-values/actions/createRelativeValuesDefinitionAction.ts b/packages/hub/src/relative-values/actions/createRelativeValuesDefinitionAction.ts index b9a65c7fcb..84e9f206b1 100644 --- a/packages/hub/src/relative-values/actions/createRelativeValuesDefinitionAction.ts +++ b/packages/hub/src/relative-values/actions/createRelativeValuesDefinitionAction.ts @@ -1,97 +1,103 @@ "use server"; import { prisma } from "@/lib/server/prisma"; -import { makeServerAction, rethrowOnConstraint } from "@/lib/server/utils"; +import { + actionClient, + ActionError, + rethrowOnConstraint, +} from "@/lib/server/utils"; import { getWriteableOwnerBySlug } from "@/owners/data/auth"; import { indexDefinitionId } from "@/search/helpers"; import { getSessionOrRedirect } from "@/users/auth"; import { inputSchema, validateRelativeValuesDefinition } from "./common"; -export const createRelativeValuesDefinitionAction = makeServerAction( - inputSchema, - async ( - input - ): Promise<{ - owner: string; - slug: string; - }> => { - const session = await getSessionOrRedirect(); - const ownerSlug = input.owner ?? session.user.username; - if (!ownerSlug) { - throw new Error("Owner slug or username is required"); - } - const owner = await getWriteableOwnerBySlug(session, ownerSlug); +export const createRelativeValuesDefinitionAction = actionClient + .schema(inputSchema) + .action( + async ({ + parsedInput: input, + }): Promise<{ + owner: string; + slug: string; + }> => { + const session = await getSessionOrRedirect(); + const ownerSlug = input.owner ?? session.user.username; + if (!ownerSlug) { + // shouldn't happen unless this is an username-less user + throw new ActionError("Owner slug or username is required"); + } + const owner = await getWriteableOwnerBySlug(session, ownerSlug); - validateRelativeValuesDefinition({ - items: input.items, - clusters: input.clusters, - recommendedUnit: input.recommendedUnit, - }); + validateRelativeValuesDefinition({ + items: input.items, + clusters: input.clusters, + recommendedUnit: input.recommendedUnit, + }); - const definition = await prisma.$transaction(async (tx) => { - const definition = await rethrowOnConstraint( - () => - tx.relativeValuesDefinition.create({ - data: { - ownerId: owner.id, - slug: input.slug, - revisions: { - create: { - title: input.title, - items: input.items, - clusters: input.clusters, - recommendedUnit: input.recommendedUnit, + const definition = await prisma.$transaction(async (tx) => { + const definition = await rethrowOnConstraint( + () => + tx.relativeValuesDefinition.create({ + data: { + ownerId: owner.id, + slug: input.slug, + revisions: { + create: { + title: input.title, + items: input.items, + clusters: input.clusters, + recommendedUnit: input.recommendedUnit, + }, }, }, - }, - select: { - id: true, - slug: true, - owner: { - select: { - slug: true, + select: { + id: true, + slug: true, + owner: { + select: { + slug: true, + }, }, }, - }, - }), - { - target: ["slug", "ownerId"], - error: `The definition ${input.slug} already exists on this account`, - } - ); + }), + { + target: ["slug", "ownerId"], + error: `The definition ${input.slug} already exists on this account`, + } + ); - const revision = await tx.relativeValuesDefinitionRevision.create({ - data: { - title: input.title, - items: input.items, - clusters: input.clusters, - recommendedUnit: input.recommendedUnit, - definition: { - connect: { - id: definition.id, + const revision = await tx.relativeValuesDefinitionRevision.create({ + data: { + title: input.title, + items: input.items, + clusters: input.clusters, + recommendedUnit: input.recommendedUnit, + definition: { + connect: { + id: definition.id, + }, }, }, - }, - }); + }); - await tx.relativeValuesDefinition.update({ - where: { - id: definition.id, - }, - data: { - currentRevisionId: revision.id, - }, - }); + await tx.relativeValuesDefinition.update({ + where: { + id: definition.id, + }, + data: { + currentRevisionId: revision.id, + }, + }); - return definition; - }); + return definition; + }); - await indexDefinitionId(definition.id); + await indexDefinitionId(definition.id); - return { - owner: definition.owner.slug, - slug: definition.slug, - }; - } -); + return { + owner: definition.owner.slug, + slug: definition.slug, + }; + } + ); diff --git a/packages/hub/src/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx b/packages/hub/src/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx index 037c0f4c83..4c8ac5d78e 100644 --- a/packages/hub/src/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx +++ b/packages/hub/src/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx @@ -2,17 +2,19 @@ import { z } from "zod"; import { prisma } from "@/lib/server/prisma"; -import { makeServerAction } from "@/lib/server/utils"; +import { actionClient } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { getWriteableOwnerBySlug } from "@/owners/data/auth"; import { getSessionOrRedirect } from "@/users/auth"; -export const deleteRelativeValuesDefinitionAction = makeServerAction( - z.object({ - owner: zSlug, - slug: zSlug, - }), - async (input) => { +export const deleteRelativeValuesDefinitionAction = actionClient + .schema( + z.object({ + owner: zSlug, + slug: zSlug, + }) + ) + .action(async ({ parsedInput: input }): Promise<"ok"> => { const session = await getSessionOrRedirect(); const owner = await getWriteableOwnerBySlug(session, input.owner); @@ -25,5 +27,5 @@ export const deleteRelativeValuesDefinitionAction = makeServerAction( }, }, }); - } -); + return "ok"; + }); diff --git a/packages/hub/src/relative-values/actions/updateRelativeValuesDefinitionAction.ts b/packages/hub/src/relative-values/actions/updateRelativeValuesDefinitionAction.ts index f62013f273..212ffd62c1 100644 --- a/packages/hub/src/relative-values/actions/updateRelativeValuesDefinitionAction.ts +++ b/packages/hub/src/relative-values/actions/updateRelativeValuesDefinitionAction.ts @@ -1,72 +1,75 @@ "use server"; import { prisma } from "@/lib/server/prisma"; -import { makeServerAction } from "@/lib/server/utils"; +import { actionClient } from "@/lib/server/utils"; import { getWriteableOwnerBySlug } from "@/owners/data/auth"; import { getSessionOrRedirect } from "@/users/auth"; import { inputSchema, validateRelativeValuesDefinition } from "./common"; -export const updateRelativeValuesDefinitionAction = makeServerAction( - inputSchema, - async (input): Promise<{ owner: string; slug: string }> => { - const session = await getSessionOrRedirect(); - const ownerSlug = input.owner ?? session.user.username; - if (!ownerSlug) { - throw new Error("Owner slug or username is required"); - } - const owner = await getWriteableOwnerBySlug(session, ownerSlug); +export const updateRelativeValuesDefinitionAction = actionClient + .schema(inputSchema) + .action( + async ({ + parsedInput: input, + }): Promise<{ owner: string; slug: string }> => { + const session = await getSessionOrRedirect(); + const ownerSlug = input.owner ?? session.user.username; + if (!ownerSlug) { + throw new Error("Owner slug or username is required"); + } + const owner = await getWriteableOwnerBySlug(session, ownerSlug); - validateRelativeValuesDefinition({ - items: input.items, - clusters: input.clusters, - recommendedUnit: input.recommendedUnit, - }); + validateRelativeValuesDefinition({ + items: input.items, + clusters: input.clusters, + recommendedUnit: input.recommendedUnit, + }); - const definition = await prisma.$transaction(async (tx) => { - const revision = await tx.relativeValuesDefinitionRevision.create({ - data: { - title: input.title, - items: input.items, - clusters: input.clusters, - recommendedUnit: input.recommendedUnit, - definition: { - connect: { - slug_ownerId: { - slug: input.slug, - ownerId: owner.id, + const definition = await prisma.$transaction(async (tx) => { + const revision = await tx.relativeValuesDefinitionRevision.create({ + data: { + title: input.title, + items: input.items, + clusters: input.clusters, + recommendedUnit: input.recommendedUnit, + definition: { + connect: { + slug_ownerId: { + slug: input.slug, + ownerId: owner.id, + }, }, }, }, - }, - include: { - definition: { - select: { - id: true, + include: { + definition: { + select: { + id: true, + }, }, }, - }, - }); + }); - const definition = await tx.relativeValuesDefinition.update({ - where: { - id: revision.definition.id, - }, - data: { - currentRevisionId: revision.id, - }, - select: { - owner: { - select: { - slug: true, + const definition = await tx.relativeValuesDefinition.update({ + where: { + id: revision.definition.id, + }, + data: { + currentRevisionId: revision.id, + }, + select: { + owner: { + select: { + slug: true, + }, }, + slug: true, }, - slug: true, - }, - }); + }); - return definition; - }); + return definition; + }); - return { owner: definition.owner.slug, slug: definition.slug }; - } -); + return { owner: definition.owner.slug, slug: definition.slug }; + } + ); diff --git a/packages/hub/src/relative-values/components/RelativeValuesDefinitionForm/index.tsx b/packages/hub/src/relative-values/components/RelativeValuesDefinitionForm/index.tsx index 2974694018..5c5dfdd6ff 100644 --- a/packages/hub/src/relative-values/components/RelativeValuesDefinitionForm/index.tsx +++ b/packages/hub/src/relative-values/components/RelativeValuesDefinitionForm/index.tsx @@ -1,9 +1,14 @@ -import { FC } from "react"; -import { FormProvider, useForm } from "react-hook-form"; +import { + InferSafeActionFnInput, + InferSafeActionFnResult, +} from "next-safe-action"; +import { HookSafeActionFn } from "next-safe-action/hooks"; +import { FormProvider } from "react-hook-form"; import { Button, StyledTab, TextFormField } from "@quri/ui"; import { SlugFormField } from "@/components/ui/SlugFormField"; +import { useSafeActionForm } from "@/lib/hooks/useSafeActionForm"; import { FormShape } from "./FormShape"; import { FormSectionHeader, HTMLForm } from "./HTMLForm"; @@ -15,15 +20,31 @@ type Props = { save: (data: FormShape) => Promise; }; -export const RelativeValuesDefinitionForm: FC = ({ +export function RelativeValuesDefinitionForm< + Action extends HookSafeActionFn, +>({ defaultValues, withoutSlug, - save, -}) => { - const form = useForm({ defaultValues }); - - const onSubmit = form.handleSubmit(async (data) => { - await save(data); + action, + formDataToInput, + onCompleted, +}: { + defaultValues?: FormShape; + withoutSlug?: boolean; + action: Action; + formDataToInput: ( + data: FormShape + ) => InferSafeActionFnInput["clientInput"]; + onCompleted?: ( + data: NonNullable["data"]> + ) => void; +}) { + const { form, onSubmit, inFlight } = useSafeActionForm({ + mode: "onChange", + defaultValues, + action, + formDataToVariables: formDataToInput, + onCompleted, }); return ( @@ -63,11 +84,11 @@ export const RelativeValuesDefinitionForm: FC = ({
-
); -}; +} diff --git a/packages/hub/src/relative-values/values/ModelEvaluator.ts b/packages/hub/src/relative-values/values/ModelEvaluator.ts index 97cd4e857a..6b427dca7b 100644 --- a/packages/hub/src/relative-values/values/ModelEvaluator.ts +++ b/packages/hub/src/relative-values/values/ModelEvaluator.ts @@ -104,6 +104,7 @@ export class ModelEvaluator { cache?: RelativeValuesExportFullDTO["cache"] ): Promise> { // TODO - versioned-components + // TODO - support hub imports const project = new SqProject({ linker: makeSelfContainedLinker({ wrapper: ` diff --git a/packages/hub/src/squiggle/components/ImportTooltip.tsx b/packages/hub/src/squiggle/components/ImportTooltip.tsx index df26b0dfa0..76e3587b1e 100644 --- a/packages/hub/src/squiggle/components/ImportTooltip.tsx +++ b/packages/hub/src/squiggle/components/ImportTooltip.tsx @@ -22,7 +22,14 @@ export const ImportTooltip: FC = ({ importId }) => { useEffect(() => { // TODO - this is done with a server action, so it's not cached. // A route would be better. - loadModelCardAction({ owner, slug }).then(setModel); + loadModelCardAction({ owner, slug }).then((result) => { + if (result?.data) { + setModel(result.data); + } else { + // TODO - handle errors + setModel(null); + } + }); }, [owner, slug]); return ( From ab7fcc4af6f309450862e97693df9fcc817d24cb Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 3 Dec 2024 15:57:39 -0300 Subject: [PATCH 59/68] failValidationOnConstraint is back --- .../acceptReusableGroupInviteTokenAction.ts | 2 +- .../groups/actions/addUserToGroupAction.ts | 2 +- .../src/groups/actions/createGroupAction.ts | 48 +++++++------ .../createReusableGroupInviteTokenAction.ts | 2 +- .../groups/actions/deleteMembershipAction.ts | 2 +- .../deleteReusableGroupInviteTokenAction.ts | 2 +- .../actions/updateMembershipRoleAction.ts | 2 +- packages/hub/src/lib/server/actionClient.ts | 68 +++++++++++++++++++ packages/hub/src/lib/server/utils.ts | 46 ------------- .../actions/adminUpdateModelVersionAction.ts | 2 +- .../src/models/actions/createModelAction.ts | 49 +++++++------ .../src/models/actions/deleteModelAction.ts | 2 +- .../src/models/actions/loadModelCardAction.ts | 2 +- .../src/models/actions/loadModelFullAction.ts | 2 +- .../hub/src/models/actions/moveModelAction.ts | 49 +++++++------ .../actions/updateModelPrivacyAction.ts | 2 +- .../models/actions/updateModelSlugAction.ts | 49 +++++++------ .../updateSquiggleSnippetModelAction.ts | 2 +- packages/hub/src/models/utils.ts | 2 +- .../actions/buildRelativeValuesCacheAction.ts | 2 +- .../actions/clearRelativeValuesCacheAction.ts | 2 +- .../createRelativeValuesDefinitionAction.ts | 18 +++-- .../deleteRelativeValuesDefinitionAction.tsx | 2 +- .../updateRelativeValuesDefinitionAction.ts | 2 +- .../actions/adminRebuildSearchIndexAction.ts | 2 +- 25 files changed, 210 insertions(+), 153 deletions(-) create mode 100644 packages/hub/src/lib/server/actionClient.ts delete mode 100644 packages/hub/src/lib/server/utils.ts diff --git a/packages/hub/src/groups/actions/acceptReusableGroupInviteTokenAction.ts b/packages/hub/src/groups/actions/acceptReusableGroupInviteTokenAction.ts index ea94c6399a..7a0b223ee7 100644 --- a/packages/hub/src/groups/actions/acceptReusableGroupInviteTokenAction.ts +++ b/packages/hub/src/groups/actions/acceptReusableGroupInviteTokenAction.ts @@ -5,8 +5,8 @@ import { z } from "zod"; import { getMyMembership } from "@/groups/helpers"; import { groupMembersRoute } from "@/lib/routes"; +import { actionClient, ActionError } from "@/lib/server/actionClient"; import { prisma } from "@/lib/server/prisma"; -import { actionClient, ActionError } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { getSessionOrRedirect } from "@/users/auth"; diff --git a/packages/hub/src/groups/actions/addUserToGroupAction.ts b/packages/hub/src/groups/actions/addUserToGroupAction.ts index b5a0c6381d..0f46b3707b 100644 --- a/packages/hub/src/groups/actions/addUserToGroupAction.ts +++ b/packages/hub/src/groups/actions/addUserToGroupAction.ts @@ -3,8 +3,8 @@ import { MembershipRole } from "@prisma/client"; import { z } from "zod"; +import { actionClient, ActionError } from "@/lib/server/actionClient"; import { prisma } from "@/lib/server/prisma"; -import { actionClient, ActionError } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { getSessionOrRedirect } from "@/users/auth"; diff --git a/packages/hub/src/groups/actions/createGroupAction.ts b/packages/hub/src/groups/actions/createGroupAction.ts index 2f06da9ed7..fe054162c7 100644 --- a/packages/hub/src/groups/actions/createGroupAction.ts +++ b/packages/hub/src/groups/actions/createGroupAction.ts @@ -1,9 +1,11 @@ "use server"; -import { returnValidationErrors } from "next-safe-action"; import { z } from "zod"; +import { + actionClient, + failValidationOnConstraint, +} from "@/lib/server/actionClient"; import { prisma } from "@/lib/server/prisma"; -import { actionClient } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { indexGroupId } from "@/search/helpers"; import { getSessionOrRedirect } from "@/users/auth"; @@ -21,28 +23,32 @@ export const createGroupAction = actionClient where: { email: session.user.email }, }); - let group: { id: string }; - try { - group = await prisma.group.create({ - data: { - asOwner: { - create: { - slug: input.slug, + const group = await failValidationOnConstraint( + () => + prisma.group.create({ + data: { + asOwner: { + create: { + slug: input.slug, + }, + }, + memberships: { + create: [{ userId: user.id, role: "Admin" }], }, }, - memberships: { - create: [{ userId: user.id, role: "Admin" }], + select: { id: true }, + }), + { + schema, + handlers: [ + { + constraint: ["slug"], + input: "slug", + error: `Group ${input.slug} already exists`, }, - }, - select: { id: true }, - }); - } catch { - returnValidationErrors(schema, { - slug: { - _errors: [`Group ${input.slug} already exists`], - }, - }); - } + ], + } + ); await indexGroupId(group.id); diff --git a/packages/hub/src/groups/actions/createReusableGroupInviteTokenAction.ts b/packages/hub/src/groups/actions/createReusableGroupInviteTokenAction.ts index 1b7025163b..65428573a9 100644 --- a/packages/hub/src/groups/actions/createReusableGroupInviteTokenAction.ts +++ b/packages/hub/src/groups/actions/createReusableGroupInviteTokenAction.ts @@ -4,8 +4,8 @@ import { revalidatePath } from "next/cache"; import { z } from "zod"; import { groupMembersRoute } from "@/lib/routes"; +import { actionClient, ActionError } from "@/lib/server/actionClient"; import { prisma } from "@/lib/server/prisma"; -import { actionClient, ActionError } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { loadMyMembership } from "../data/members"; diff --git a/packages/hub/src/groups/actions/deleteMembershipAction.ts b/packages/hub/src/groups/actions/deleteMembershipAction.ts index 6a65fa4e28..4e08f781d8 100644 --- a/packages/hub/src/groups/actions/deleteMembershipAction.ts +++ b/packages/hub/src/groups/actions/deleteMembershipAction.ts @@ -5,8 +5,8 @@ import { z } from "zod"; import { getMembership, getMyMembership } from "@/groups/helpers"; import { groupMembersRoute } from "@/lib/routes"; +import { actionClient, ActionError } from "@/lib/server/actionClient"; import { prisma } from "@/lib/server/prisma"; -import { actionClient, ActionError } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { getSessionOrRedirect } from "@/users/auth"; diff --git a/packages/hub/src/groups/actions/deleteReusableGroupInviteTokenAction.ts b/packages/hub/src/groups/actions/deleteReusableGroupInviteTokenAction.ts index 127dd879a3..550f0a7b3e 100644 --- a/packages/hub/src/groups/actions/deleteReusableGroupInviteTokenAction.ts +++ b/packages/hub/src/groups/actions/deleteReusableGroupInviteTokenAction.ts @@ -3,8 +3,8 @@ import { revalidatePath } from "next/cache"; import { z } from "zod"; import { groupMembersRoute } from "@/lib/routes"; +import { actionClient, ActionError } from "@/lib/server/actionClient"; import { prisma } from "@/lib/server/prisma"; -import { actionClient, ActionError } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { loadMyMembership } from "../data/members"; diff --git a/packages/hub/src/groups/actions/updateMembershipRoleAction.ts b/packages/hub/src/groups/actions/updateMembershipRoleAction.ts index 90fc178d1c..7b020682ed 100644 --- a/packages/hub/src/groups/actions/updateMembershipRoleAction.ts +++ b/packages/hub/src/groups/actions/updateMembershipRoleAction.ts @@ -3,8 +3,8 @@ import { MembershipRole } from "@prisma/client"; import { z } from "zod"; +import { actionClient, ActionError } from "@/lib/server/actionClient"; import { prisma } from "@/lib/server/prisma"; -import { actionClient, ActionError } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { getSessionOrRedirect } from "@/users/auth"; diff --git a/packages/hub/src/lib/server/actionClient.ts b/packages/hub/src/lib/server/actionClient.ts new file mode 100644 index 0000000000..4a3106e92f --- /dev/null +++ b/packages/hub/src/lib/server/actionClient.ts @@ -0,0 +1,68 @@ +import { Prisma } from "@prisma/client"; +import { + createSafeActionClient, + DEFAULT_SERVER_ERROR_MESSAGE, + returnValidationErrors, + ValidationErrors, +} from "next-safe-action"; +import { z } from "zod"; + +export class ActionError extends Error {} + +export const actionClient = createSafeActionClient({ + handleServerError(e) { + console.error("Action error:", e.message); + + if (e instanceof ActionError) { + return e.message; + } + + return DEFAULT_SERVER_ERROR_MESSAGE; + }, +}); + +// Converts Prisma constraint error (usually happens on create operations) to next-safe-action validation error. +// This allows us to attach the error to the form field that caused the error. +// The constraint must be known - if the constraint happens on a field that wasn't provided in the configuration, it will be thrown as a generic server-side error (and hidden in production). +export async function failValidationOnConstraint( + cb: () => Promise, + // This configuration is a bit verbose because it's the most generic form: it supports multiple possible constraints, with multiple handlers. + // A complicated "create" operation could, theoretically, have multiple ways to fail uniqueness, and we'd need to report different fields for each. + { + schema, + handlers, + }: { + schema: S; + handlers: { + // Which field or a combination of fields caused the error. + // For example, if the error happens on a unique constraint on `slug` and `ownerId`, the constraint should be `["slug", "ownerId"]`. + constraint: string[]; + // Action's input field name that will be reported in the validation error. + // Must be a key on the action's input schema. + // Nested keys are not supported. + input: keyof z.input; + // Error message to report. + error: string; + }[]; + } +): Promise { + try { + return await cb(); + } catch (e) { + for (const handler of handlers) { + if ( + e instanceof Prisma.PrismaClientKnownRequestError && + e.code === "P2002" && + Array.isArray(e.meta?.["target"]) && + e.meta?.["target"].join(",") === handler.constraint.join(",") + ) { + returnValidationErrors(schema, { + [handler.input]: { + _errors: [handler.error], + }, + } as ValidationErrors>); + } + } + throw e; + } +} diff --git a/packages/hub/src/lib/server/utils.ts b/packages/hub/src/lib/server/utils.ts deleted file mode 100644 index 9dd11754d4..0000000000 --- a/packages/hub/src/lib/server/utils.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Prisma } from "@prisma/client"; -import { - createSafeActionClient, - DEFAULT_SERVER_ERROR_MESSAGE, -} from "next-safe-action"; - -export class ActionError extends Error {} - -export const actionClient = createSafeActionClient({ - handleServerError(e) { - console.error("Action error:", e.message); - - if (e instanceof ActionError) { - return e.message; - } - - return DEFAULT_SERVER_ERROR_MESSAGE; - }, -}); - -// Rethrows Prisma constraint error (usually happens on create operations) with a nicer error message. -export async function rethrowOnConstraint( - cb: () => Promise, - ...handlers: { - target: string[]; - error: string; - }[] -): Promise { - try { - return await cb(); - } catch (e) { - for (const handler of handlers) { - if ( - e instanceof Prisma.PrismaClientKnownRequestError && - e.code === "P2002" && - Array.isArray(e.meta?.["target"]) && - e.meta?.["target"].join(",") === handler.target.join(",") - ) { - // TODO - throw more specific error - // TODO - should this even be an ActionError? `handler.error` is still not very readable - throw new ActionError(handler.error); - } - } - throw e; - } -} diff --git a/packages/hub/src/models/actions/adminUpdateModelVersionAction.ts b/packages/hub/src/models/actions/adminUpdateModelVersionAction.ts index 092022a844..b961498f06 100644 --- a/packages/hub/src/models/actions/adminUpdateModelVersionAction.ts +++ b/packages/hub/src/models/actions/adminUpdateModelVersionAction.ts @@ -1,8 +1,8 @@ "use server"; import { z } from "zod"; +import { actionClient } from "@/lib/server/actionClient"; import { prisma } from "@/lib/server/prisma"; -import { actionClient } from "@/lib/server/utils"; import { checkRootUser, getSelf, getSessionOrRedirect } from "@/users/auth"; // Admin-only query for upgrading model versions diff --git a/packages/hub/src/models/actions/createModelAction.ts b/packages/hub/src/models/actions/createModelAction.ts index 12da2c5723..8e06f31cb1 100644 --- a/packages/hub/src/models/actions/createModelAction.ts +++ b/packages/hub/src/models/actions/createModelAction.ts @@ -7,8 +7,11 @@ import { generateSeed } from "@quri/squiggle-lang"; import { defaultSquiggleVersion } from "@quri/versioned-squiggle-components"; import { modelRoute } from "@/lib/routes"; +import { + actionClient, + failValidationOnConstraint, +} from "@/lib/server/actionClient"; import { prisma } from "@/lib/server/prisma"; -import { actionClient } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { getWriteableOwner } from "@/owners/data/auth"; import { indexModelId } from "@/search/helpers"; @@ -50,26 +53,30 @@ export const createModelAction = actionClient const model = await prisma.$transaction(async (tx) => { const owner = await getWriteableOwner(session, input.groupSlug); - // nested create is not possible here; - // similar problem is described here: https://github.com/prisma/prisma/discussions/14937, - // seems to be caused by multiple Model -> ModelRevision relations - let model: { id: string }; - try { - model = await tx.model.create({ - data: { - slug, - ownerId: owner.id, - isPrivate: input.isPrivate, - }, - select: { id: true }, - }); - } catch { - returnValidationErrors(schema, { - slug: { - _errors: [`Model ${input.slug} already exists on this account`], - }, - }); - } + const model = await failValidationOnConstraint( + () => + // nested create is not possible here; + // similar problem is described here: https://github.com/prisma/prisma/discussions/14937, + // seems to be caused by multiple Model -> ModelRevision relations + tx.model.create({ + data: { + slug, + ownerId: owner.id, + isPrivate: input.isPrivate, + }, + select: { id: true }, + }), + { + schema, + handlers: [ + { + constraint: ["slug", "ownerId"], + input: "slug", + error: `Model ${input.slug} already exists on this account`, + }, + ], + } + ); const self = await getSelf(session); diff --git a/packages/hub/src/models/actions/deleteModelAction.ts b/packages/hub/src/models/actions/deleteModelAction.ts index 74799633c6..88a2c219df 100644 --- a/packages/hub/src/models/actions/deleteModelAction.ts +++ b/packages/hub/src/models/actions/deleteModelAction.ts @@ -2,8 +2,8 @@ import { z } from "zod"; +import { actionClient } from "@/lib/server/actionClient"; import { prisma } from "@/lib/server/prisma"; -import { actionClient } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { getWriteableModel } from "@/models/utils"; diff --git a/packages/hub/src/models/actions/loadModelCardAction.ts b/packages/hub/src/models/actions/loadModelCardAction.ts index c88d9a9498..4668f099d0 100644 --- a/packages/hub/src/models/actions/loadModelCardAction.ts +++ b/packages/hub/src/models/actions/loadModelCardAction.ts @@ -1,7 +1,7 @@ "use server"; import { z } from "zod"; -import { actionClient } from "@/lib/server/utils"; +import { actionClient } from "@/lib/server/actionClient"; import { zSlug } from "@/lib/zodUtils"; import { loadModelCard, ModelCardDTO } from "../data/cards"; diff --git a/packages/hub/src/models/actions/loadModelFullAction.ts b/packages/hub/src/models/actions/loadModelFullAction.ts index a5509288ed..89b066c5b4 100644 --- a/packages/hub/src/models/actions/loadModelFullAction.ts +++ b/packages/hub/src/models/actions/loadModelFullAction.ts @@ -1,7 +1,7 @@ "use server"; import { z } from "zod"; -import { actionClient } from "@/lib/server/utils"; +import { actionClient } from "@/lib/server/actionClient"; import { zSlug } from "@/lib/zodUtils"; import { loadModelFull, ModelFullDTO } from "../data/full"; diff --git a/packages/hub/src/models/actions/moveModelAction.ts b/packages/hub/src/models/actions/moveModelAction.ts index 715845a153..3def0272a3 100644 --- a/packages/hub/src/models/actions/moveModelAction.ts +++ b/packages/hub/src/models/actions/moveModelAction.ts @@ -1,10 +1,12 @@ "use server"; -import { returnValidationErrors } from "next-safe-action"; import { z } from "zod"; +import { + actionClient, + failValidationOnConstraint, +} from "@/lib/server/actionClient"; import { prisma } from "@/lib/server/prisma"; -import { actionClient } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { getWriteableModel } from "@/models/utils"; import { getWriteableOwnerBySlug } from "@/owners/data/auth"; @@ -31,22 +33,29 @@ export const moveModelAction = actionClient const newOwner = await getWriteableOwnerBySlug(session, input.owner.slug); - try { - const newModel = await prisma.model.update({ - where: { id: model.id }, - data: { ownerId: newOwner.id }, - select: { - slug: true, - owner: true, - }, - }); - return { model: newModel }; - } catch { - returnValidationErrors(schema, { - // `owner`, not `owner.slug` - the select name from the RHF point of view is just `owner` - owner: { - _errors: [`Model ${input.slug} already exists on the target account`], - }, - }); - } + const newModel = await failValidationOnConstraint( + () => + prisma.model.update({ + where: { id: model.id }, + data: { ownerId: newOwner.id }, + select: { + slug: true, + owner: true, + }, + }), + { + schema, + handlers: [ + { + constraint: ["slug", "ownerId"], + // `owner`, not `owner{ slug }` - the select name from the RHF point of view is just `owner`. + // (`rethrowOnConstraint` doesn't support nested keys, so we're lucky this is possible) + input: "owner", + error: `Model ${input.owner.slug} already exists on the target account`, + }, + ], + } + ); + + return { model: newModel }; }); diff --git a/packages/hub/src/models/actions/updateModelPrivacyAction.ts b/packages/hub/src/models/actions/updateModelPrivacyAction.ts index 62c33efd3c..b3ca61b380 100644 --- a/packages/hub/src/models/actions/updateModelPrivacyAction.ts +++ b/packages/hub/src/models/actions/updateModelPrivacyAction.ts @@ -4,8 +4,8 @@ import { revalidatePath } from "next/cache"; import { z } from "zod"; import { modelRoute } from "@/lib/routes"; +import { actionClient } from "@/lib/server/actionClient"; import { prisma } from "@/lib/server/prisma"; -import { actionClient } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { getWriteableModel } from "@/models/utils"; diff --git a/packages/hub/src/models/actions/updateModelSlugAction.ts b/packages/hub/src/models/actions/updateModelSlugAction.ts index fa84dba90d..58da0303aa 100644 --- a/packages/hub/src/models/actions/updateModelSlugAction.ts +++ b/packages/hub/src/models/actions/updateModelSlugAction.ts @@ -1,10 +1,12 @@ "use server"; -import { returnValidationErrors } from "next-safe-action"; import { z } from "zod"; +import { + actionClient, + failValidationOnConstraint, +} from "@/lib/server/actionClient"; import { prisma } from "@/lib/server/prisma"; -import { actionClient } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { getWriteableModel } from "@/models/utils"; @@ -44,26 +46,31 @@ export const updateModelSlugAction = actionClient }; } - try { - const newModel = await prisma.model.update({ - where: { id: model.id }, - data: { slug: input.slug }, - select: { - slug: true, - owner: { - select: { - slug: true, + const newModel = await failValidationOnConstraint( + () => + prisma.model.update({ + where: { id: model.id }, + data: { slug: input.slug }, + select: { + slug: true, + owner: { + select: { + slug: true, + }, }, }, - }, - }); + }), + { + schema, + handlers: [ + { + constraint: ["slug"], + input: "slug", + error: `Model ${input.slug} already exists`, + }, + ], + } + ); - return { model: newModel }; - } catch { - returnValidationErrors(schema, { - slug: { - _errors: [`Model ${input.slug} already exists`], - }, - }); - } + return { model: newModel }; }); diff --git a/packages/hub/src/models/actions/updateSquiggleSnippetModelAction.ts b/packages/hub/src/models/actions/updateSquiggleSnippetModelAction.ts index a0ab692e74..328fd63f38 100644 --- a/packages/hub/src/models/actions/updateSquiggleSnippetModelAction.ts +++ b/packages/hub/src/models/actions/updateSquiggleSnippetModelAction.ts @@ -6,8 +6,8 @@ import { z } from "zod"; import { squiggleVersions } from "@quri/versioned-squiggle-components"; import { modelRoute } from "@/lib/routes"; +import { actionClient, ActionError } from "@/lib/server/actionClient"; import { prisma } from "@/lib/server/prisma"; -import { actionClient, ActionError } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { getSelf, getSessionOrRedirect } from "@/users/auth"; diff --git a/packages/hub/src/models/utils.ts b/packages/hub/src/models/utils.ts index 33f982f726..716941e26a 100644 --- a/packages/hub/src/models/utils.ts +++ b/packages/hub/src/models/utils.ts @@ -1,7 +1,7 @@ import { Model, Prisma } from "@prisma/client"; +import { ActionError } from "@/lib/server/actionClient"; import { prisma } from "@/lib/server/prisma"; -import { ActionError } from "@/lib/server/utils"; import { getSessionOrRedirect } from "@/users/auth"; import { modelWhereHasAccess } from "./data/authHelpers"; diff --git a/packages/hub/src/relative-values/actions/buildRelativeValuesCacheAction.ts b/packages/hub/src/relative-values/actions/buildRelativeValuesCacheAction.ts index ed49589e1b..0c297b6471 100644 --- a/packages/hub/src/relative-values/actions/buildRelativeValuesCacheAction.ts +++ b/packages/hub/src/relative-values/actions/buildRelativeValuesCacheAction.ts @@ -3,8 +3,8 @@ import { revalidatePath } from "next/cache"; import { z } from "zod"; import { modelForRelativeValuesExportRoute } from "@/lib/routes"; +import { actionClient, ActionError } from "@/lib/server/actionClient"; import { prisma } from "@/lib/server/prisma"; -import { actionClient, ActionError } from "@/lib/server/utils"; import { cartesianProduct } from "@/relative-values/lib/utils"; import { relativeValuesItemsSchema } from "@/relative-values/types"; import { ModelEvaluator } from "@/relative-values/values/ModelEvaluator"; diff --git a/packages/hub/src/relative-values/actions/clearRelativeValuesCacheAction.ts b/packages/hub/src/relative-values/actions/clearRelativeValuesCacheAction.ts index 185a5633e9..f54c05397e 100644 --- a/packages/hub/src/relative-values/actions/clearRelativeValuesCacheAction.ts +++ b/packages/hub/src/relative-values/actions/clearRelativeValuesCacheAction.ts @@ -3,8 +3,8 @@ import { revalidatePath } from "next/cache"; import { z } from "zod"; import { modelForRelativeValuesExportRoute } from "@/lib/routes"; +import { actionClient } from "@/lib/server/actionClient"; import { prisma } from "@/lib/server/prisma"; -import { actionClient } from "@/lib/server/utils"; import { getRelativeValuesExportForWriteableModel } from "@/relative-values/utils"; export const clearRelativeValuesCacheAction = actionClient diff --git a/packages/hub/src/relative-values/actions/createRelativeValuesDefinitionAction.ts b/packages/hub/src/relative-values/actions/createRelativeValuesDefinitionAction.ts index 84e9f206b1..29600ac394 100644 --- a/packages/hub/src/relative-values/actions/createRelativeValuesDefinitionAction.ts +++ b/packages/hub/src/relative-values/actions/createRelativeValuesDefinitionAction.ts @@ -1,11 +1,11 @@ "use server"; -import { prisma } from "@/lib/server/prisma"; import { actionClient, ActionError, - rethrowOnConstraint, -} from "@/lib/server/utils"; + failValidationOnConstraint, +} from "@/lib/server/actionClient"; +import { prisma } from "@/lib/server/prisma"; import { getWriteableOwnerBySlug } from "@/owners/data/auth"; import { indexDefinitionId } from "@/search/helpers"; import { getSessionOrRedirect } from "@/users/auth"; @@ -36,7 +36,7 @@ export const createRelativeValuesDefinitionAction = actionClient }); const definition = await prisma.$transaction(async (tx) => { - const definition = await rethrowOnConstraint( + const definition = await failValidationOnConstraint( () => tx.relativeValuesDefinition.create({ data: { @@ -62,8 +62,14 @@ export const createRelativeValuesDefinitionAction = actionClient }, }), { - target: ["slug", "ownerId"], - error: `The definition ${input.slug} already exists on this account`, + schema: inputSchema, + handlers: [ + { + constraint: ["slug", "ownerId"], + input: "slug", + error: `The definition ${input.slug} already exists on this account`, + }, + ], } ); diff --git a/packages/hub/src/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx b/packages/hub/src/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx index 4c8ac5d78e..1a76a88c13 100644 --- a/packages/hub/src/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx +++ b/packages/hub/src/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx @@ -1,8 +1,8 @@ "use server"; import { z } from "zod"; +import { actionClient } from "@/lib/server/actionClient"; import { prisma } from "@/lib/server/prisma"; -import { actionClient } from "@/lib/server/utils"; import { zSlug } from "@/lib/zodUtils"; import { getWriteableOwnerBySlug } from "@/owners/data/auth"; import { getSessionOrRedirect } from "@/users/auth"; diff --git a/packages/hub/src/relative-values/actions/updateRelativeValuesDefinitionAction.ts b/packages/hub/src/relative-values/actions/updateRelativeValuesDefinitionAction.ts index 212ffd62c1..4f937fc56e 100644 --- a/packages/hub/src/relative-values/actions/updateRelativeValuesDefinitionAction.ts +++ b/packages/hub/src/relative-values/actions/updateRelativeValuesDefinitionAction.ts @@ -1,6 +1,6 @@ "use server"; +import { actionClient } from "@/lib/server/actionClient"; import { prisma } from "@/lib/server/prisma"; -import { actionClient } from "@/lib/server/utils"; import { getWriteableOwnerBySlug } from "@/owners/data/auth"; import { getSessionOrRedirect } from "@/users/auth"; diff --git a/packages/hub/src/search/actions/adminRebuildSearchIndexAction.ts b/packages/hub/src/search/actions/adminRebuildSearchIndexAction.ts index c319420ae4..05035366e2 100644 --- a/packages/hub/src/search/actions/adminRebuildSearchIndexAction.ts +++ b/packages/hub/src/search/actions/adminRebuildSearchIndexAction.ts @@ -2,7 +2,7 @@ import { z } from "zod"; -import { actionClient } from "@/lib/server/utils"; +import { actionClient } from "@/lib/server/actionClient"; import { checkRootUser } from "@/users/auth"; import { rebuildSearchableTable } from "../helpers"; From 49bd6981380eeddc096ba57afb263246d0d683ad Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 3 Dec 2024 16:06:21 -0300 Subject: [PATCH 60/68] setUserAction is safe; fix inFlight --- .../choose-username/ChooseUsername.tsx | 29 +++++----- .../hub/src/lib/hooks/useSafeActionForm.ts | 2 +- packages/hub/src/users/actions.ts | 53 ------------------- .../src/users/actions/setUsernameAction.ts | 48 +++++++++++++++++ 4 files changed, 62 insertions(+), 70 deletions(-) delete mode 100644 packages/hub/src/users/actions.ts create mode 100644 packages/hub/src/users/actions/setUsernameAction.ts diff --git a/packages/hub/src/app/settings/choose-username/ChooseUsername.tsx b/packages/hub/src/app/settings/choose-username/ChooseUsername.tsx index 2af45fc0c3..9c7be63b09 100644 --- a/packages/hub/src/app/settings/choose-username/ChooseUsername.tsx +++ b/packages/hub/src/app/settings/choose-username/ChooseUsername.tsx @@ -1,12 +1,13 @@ "use client"; import { useRouter } from "next/navigation"; import { FC } from "react"; -import { FormProvider, useForm } from "react-hook-form"; +import { FormProvider } from "react-hook-form"; import { Button } from "@quri/ui"; import { SlugFormField } from "@/components/ui/SlugFormField"; -import { setUsername } from "@/users/actions"; +import { useSafeActionForm } from "@/lib/hooks/useSafeActionForm"; +import { setUsernameAction } from "@/users/actions/setUsernameAction"; export const ChooseUsername: FC = () => { const router = useRouter(); @@ -15,22 +16,18 @@ export const ChooseUsername: FC = () => { username: string; }; - const form = useForm(); - - const onSubmit = form.handleSubmit(async (data) => { - const result = await setUsername(data); - if (result.ok) { + const { form, onSubmit, inFlight } = useSafeActionForm< + FormShape, + typeof setUsernameAction + >({ + action: setUsernameAction, + onCompleted: () => { router.replace("/"); - } else { - form.setError("username", { message: result.error }); - } + }, + formDataToVariables: (data) => ({ username: data.username }), + blockOnSuccess: true, }); - const disabled = - form.formState.isSubmitting || - form.formState.isSubmitSuccessful || - !form.formState.isValid; - return (
@@ -43,7 +40,7 @@ export const ChooseUsername: FC = () => { label="Pick a username" size="small" /> -
diff --git a/packages/hub/src/lib/hooks/useSafeActionForm.ts b/packages/hub/src/lib/hooks/useSafeActionForm.ts index 92b0f018d0..a7440b9218 100644 --- a/packages/hub/src/lib/hooks/useSafeActionForm.ts +++ b/packages/hub/src/lib/hooks/useSafeActionForm.ts @@ -107,6 +107,6 @@ export function useSafeActionForm< return { form, onSubmit, - inFlight: blockOnSuccess ? hasSucceeded : isPending, + inFlight: blockOnSuccess ? isPending || hasSucceeded : isPending, }; } diff --git a/packages/hub/src/users/actions.ts b/packages/hub/src/users/actions.ts deleted file mode 100644 index e17dbf0848..0000000000 --- a/packages/hub/src/users/actions.ts +++ /dev/null @@ -1,53 +0,0 @@ -"use server"; - -import { z } from "zod"; - -import { auth } from "@/lib/server/auth"; -import { prisma } from "@/lib/server/prisma"; - -const schema = z.object({ - username: z.string().min(1), -}); - -type SetUsernameResult = - | { - ok: true; - } - | { - ok: false; - error: string; - }; - -export async function setUsername( - formData: unknown -): Promise { - const session = await auth(); - if (!session?.user.email) { - throw new Error("Not signed in"); - } - if (session.user.username) { - return { ok: false, error: "Username is already set" }; - } - - const args = schema.parse(formData); - - const existingOwner = await prisma.owner.count({ - where: { slug: args.username }, - }); - if (existingOwner) { - return { ok: false, error: `Username ${args.username} is not available` }; - } - - await prisma.user.update({ - where: { - email: session.user.email, - }, - data: { - asOwner: { - create: { slug: args.username }, - }, - }, - }); - - return { ok: true }; -} diff --git a/packages/hub/src/users/actions/setUsernameAction.ts b/packages/hub/src/users/actions/setUsernameAction.ts new file mode 100644 index 0000000000..98a6767b11 --- /dev/null +++ b/packages/hub/src/users/actions/setUsernameAction.ts @@ -0,0 +1,48 @@ +"use server"; + +import { returnValidationErrors } from "next-safe-action"; +import { z } from "zod"; + +import { actionClient, ActionError } from "@/lib/server/actionClient"; +import { auth } from "@/lib/server/auth"; +import { prisma } from "@/lib/server/prisma"; + +const schema = z.object({ + username: z.string().min(1), +}); + +export const setUsernameAction = actionClient + .schema(schema) + .action(async ({ parsedInput: input }): Promise<"ok"> => { + const session = await auth(); + if (!session?.user.email) { + throw new ActionError("Not signed in"); + } + if (session.user.username) { + throw new ActionError("Username is already set"); + } + + const existingOwner = await prisma.owner.count({ + where: { slug: input.username }, + }); + if (existingOwner) { + returnValidationErrors(schema, { + username: { + _errors: [`Username ${input.username} is not available`], + }, + }); + } + + await prisma.user.update({ + where: { + email: session.user.email, + }, + data: { + asOwner: { + create: { slug: input.username }, + }, + }, + }); + + return "ok"; + }); From f5dbcc1bb42b8b15a560ca1a754976be86c2f6d8 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 3 Dec 2024 16:15:23 -0300 Subject: [PATCH 61/68] minor cleanups --- .../src/models/actions/adminUpdateModelVersionAction.ts | 9 +++------ packages/hub/src/users/auth.ts | 2 ++ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/hub/src/models/actions/adminUpdateModelVersionAction.ts b/packages/hub/src/models/actions/adminUpdateModelVersionAction.ts index b961498f06..a1615dedcf 100644 --- a/packages/hub/src/models/actions/adminUpdateModelVersionAction.ts +++ b/packages/hub/src/models/actions/adminUpdateModelVersionAction.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import { actionClient } from "@/lib/server/actionClient"; import { prisma } from "@/lib/server/prisma"; -import { checkRootUser, getSelf, getSessionOrRedirect } from "@/users/auth"; +import { checkRootUser } from "@/users/auth"; // Admin-only query for upgrading model versions export const adminUpdateModelVersionAction = actionClient @@ -14,10 +14,7 @@ export const adminUpdateModelVersionAction = actionClient }) ) .action(async ({ parsedInput: input }) => { - await checkRootUser(); - const session = await getSessionOrRedirect(); - - const self = await getSelf(session); + const self = await checkRootUser(); const model = await prisma.$transaction(async (tx) => { let model = await prisma.model.findUniqueOrThrow({ @@ -55,7 +52,7 @@ export const adminUpdateModelVersionAction = actionClient connect: { id: model.id }, }, author: { - connect: { email: self.email! }, + connect: { email: self.email }, }, comment: `Automated upgrade from ${model.currentRevision.squiggleSnippet.version} to ${input.version}`, relativeValuesExports: { diff --git a/packages/hub/src/users/auth.ts b/packages/hub/src/users/auth.ts index 5bc4f40fa9..7802d1ef8a 100644 --- a/packages/hub/src/users/auth.ts +++ b/packages/hub/src/users/auth.ts @@ -18,6 +18,7 @@ export async function getSessionUserOrRedirect() { return (await getSessionOrRedirect()).user; } +// Checks if the user is a root user. If so, returns the user. export async function checkRootUser() { const sessionUser = await getSessionUserOrRedirect(); const user = await prisma.user.findUniqueOrThrow({ @@ -26,6 +27,7 @@ export async function checkRootUser() { if (!(user.email && user.emailVerified && isRootEmail(user.email))) { throw new Error("Unauthorized"); } + return user as User & { email: NonNullable }; } export type SignedInSession = Session & { From 496537937a776ae5d32be685b89e3c76498ff31c Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 3 Dec 2024 16:27:32 -0300 Subject: [PATCH 62/68] update comments --- packages/hub/src/groups/actions/deleteMembershipAction.ts | 2 +- packages/hub/src/groups/actions/updateMembershipRoleAction.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/hub/src/groups/actions/deleteMembershipAction.ts b/packages/hub/src/groups/actions/deleteMembershipAction.ts index 4e08f781d8..ead9cbd5be 100644 --- a/packages/hub/src/groups/actions/deleteMembershipAction.ts +++ b/packages/hub/src/groups/actions/deleteMembershipAction.ts @@ -22,7 +22,7 @@ export const deleteMembershipAction = actionClient .action(async ({ parsedInput: input }): Promise<"ok"> => { const session = await getSessionOrRedirect(); - // somewhat repetitive compared to `updateMembershipRole`, but with slightly different error messages + // somewhat repetitive compared to `updateMembershipRoleAction`, but with slightly different error messages const myMembership = await getMyMembership({ groupSlug: input.group, }); diff --git a/packages/hub/src/groups/actions/updateMembershipRoleAction.ts b/packages/hub/src/groups/actions/updateMembershipRoleAction.ts index 7b020682ed..c0c75d0dab 100644 --- a/packages/hub/src/groups/actions/updateMembershipRoleAction.ts +++ b/packages/hub/src/groups/actions/updateMembershipRoleAction.ts @@ -28,7 +28,8 @@ export const updateMembershipRoleAction = actionClient ) .action(async ({ parsedInput: input }) => { const session = await getSessionOrRedirect(); - // somewhat repetitive compared to `deleteMembership`, but with slightly different error messages + + // somewhat repetitive compared to `deleteMembershipAction`, but with slightly different error messages const myMembership = await loadMyMembership({ groupSlug: input.group, From bcb7942e8c2223ff14fa17996d82398af9d79e81 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 3 Dec 2024 16:32:51 -0300 Subject: [PATCH 63/68] more docs --- packages/hub/docs/nextjs.md | 18 ++++++++++++++++++ packages/hub/src/app/README.md | 7 ------- 2 files changed, 18 insertions(+), 7 deletions(-) delete mode 100644 packages/hub/src/app/README.md diff --git a/packages/hub/docs/nextjs.md b/packages/hub/docs/nextjs.md index b8d7e94423..275c1119e7 100644 --- a/packages/hub/docs/nextjs.md +++ b/packages/hub/docs/nextjs.md @@ -1,5 +1,23 @@ # Notes on Next.js +## Conventions + +### Component files + +- store single use components in `src/app`, next to their `page.tsx` and `layout.tsx` +- if the component is shared between multiple pages, store it in `src/{topic}/components/`, where `{topic}` is something like "models" or "relative-values" +- if the component doesn't have an obvious topic, e.g. if it's a generic UI component, store it in `src/components/` + +### Actions + +- store actions in `src/{topic}/actions/`, where `{topic}` is something like "models" or "relative-values" +- name actions like this: `doSomethingAction` + +### Data loading + +- all data loading functions that expose data to the frontend should go in `src/{topic}/data/` +- data loading functions should sanitize the data that they select from the database, to avoid security issues + ## Loading pages Avoid generic `loading.tsx` files. Thoughtful loading states are good, but the generic top-level loading state was harmful: diff --git a/packages/hub/src/app/README.md b/packages/hub/src/app/README.md deleted file mode 100644 index e8c1f414c1..0000000000 --- a/packages/hub/src/app/README.md +++ /dev/null @@ -1,7 +0,0 @@ -Next.js app router root. - -Conventions: - -- store single use components in this folder, next to their `page.tsx` and `layout.tsx` -- if the component is shared between multiple pages, store it in `src/{topic}/components`, where `{topic}` is something like "models" or "relative-values" -- if the component doesn't have an obvious topic, e.g. if it's a generic UI component, store it in `src/components` From d9cf44e41d3243b0d8553508c21e2d283c48f682 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 3 Dec 2024 16:36:48 -0300 Subject: [PATCH 64/68] cleanups --- packages/hub/docs/nextjs.md | 2 + .../[slug]/members/AddUserToGroupAction.tsx | 4 +- .../[slug]/EditSquiggleSnippetModel.tsx | 4 +- .../models/[owner]/[slug]/MoveModelAction.tsx | 4 +- .../[owner]/[slug]/UpdateModelSlugAction.tsx | 4 +- .../src/app/new/definition/NewDefinition.tsx | 2 +- packages/hub/src/app/new/group/NewGroup.tsx | 4 +- .../edit/EditRelativeValuesDefinition.tsx | 2 +- .../choose-username/ChooseUsername.tsx | 4 +- .../components/ui/SafeActionModalAction.tsx | 14 +-- .../src/components/ui/ServerActionButton.tsx | 39 ------- .../ui/ServerActionDropdownAction.tsx | 47 -------- .../components/ui/ServerActionModalAction.tsx | 101 ------------------ .../hub/src/lib/hooks/useSafeActionForm.ts | 16 ++- .../hub/src/lib/hooks/useServerActionForm.ts | 67 ------------ .../RelativeValuesDefinitionForm/index.tsx | 8 +- 16 files changed, 34 insertions(+), 288 deletions(-) delete mode 100644 packages/hub/src/components/ui/ServerActionButton.tsx delete mode 100644 packages/hub/src/components/ui/ServerActionDropdownAction.tsx delete mode 100644 packages/hub/src/components/ui/ServerActionModalAction.tsx delete mode 100644 packages/hub/src/lib/hooks/useServerActionForm.ts diff --git a/packages/hub/docs/nextjs.md b/packages/hub/docs/nextjs.md index 275c1119e7..d0633c7e71 100644 --- a/packages/hub/docs/nextjs.md +++ b/packages/hub/docs/nextjs.md @@ -12,6 +12,8 @@ - store actions in `src/{topic}/actions/`, where `{topic}` is something like "models" or "relative-values" - name actions like this: `doSomethingAction` +- use `next-safe-action` to define all actions +- return _something_ from actions, even if it's just `"ok"`; some wrappers check whether `data` on the action is defined ### Data loading diff --git a/packages/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx b/packages/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx index 410eb86945..c218c1842b 100644 --- a/packages/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx +++ b/packages/hub/src/app/groups/[slug]/members/AddUserToGroupAction.tsx @@ -21,11 +21,11 @@ export const AddUserToGroupAction: FC = ({ groupSlug, append }) => { title="Add" icon={PlusIcon} action={addUserToGroupAction} - onCompleted={(membership) => { + onSuccess={(membership) => { append(membership); }} defaultValues={{ role: "Member" }} - formDataToVariables={(data) => ({ + formDataToInput={(data) => ({ group: groupSlug, username: data.user.slug, role: data.role, diff --git a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx index 268537fee5..6df4baed92 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/EditSquiggleSnippetModel.tsx @@ -173,11 +173,11 @@ export const EditSquiggleSnippetModel: FC = ({ >({ defaultValues: initialFormValues, action: updateSquiggleSnippetModelAction, - onCompleted: () => { + onSuccess: () => { toast("Saved", "confirmation"); draftUtils.discard(draftLocator); }, - formDataToVariables: (formData, extraData) => ({ + formDataToInput: (formData, extraData) => ({ content: { code: formData.code, version, diff --git a/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx index 1dc99bd4dc..25470b4401 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/MoveModelAction.tsx @@ -31,12 +31,12 @@ export const MoveModelAction: FC = ({ model }) => { owner: model.owner as SelectOwnerOption, }} action={moveModelAction} - formDataToVariables={(data) => ({ + formDataToInput={(data) => ({ oldOwner: model.owner.slug, owner: { slug: data.owner.slug }, slug: model.slug, })} - onCompleted={({ model: newModel }) => { + onSuccess={({ model: newModel }) => { draftUtils.rename( modelToDraftLocator(model), modelToDraftLocator(newModel) diff --git a/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx b/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx index c3bb4692ec..5cd77d72b1 100644 --- a/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx +++ b/packages/hub/src/app/models/[owner]/[slug]/UpdateModelSlugAction.tsx @@ -27,12 +27,12 @@ export const UpdateModelSlugAction: FC = ({ model, close }) => { icon={EditIcon} action={updateModelSlugAction} defaultValues={{ slug: model.slug }} - formDataToVariables={(data) => ({ + formDataToInput={(data) => ({ owner: model.owner.slug, oldSlug: model.slug, slug: data.slug, })} - onCompleted={({ model: newModel }) => { + onSuccess={({ model: newModel }) => { draftUtils.rename( modelToDraftLocator(model), modelToDraftLocator(newModel) diff --git a/packages/hub/src/app/new/definition/NewDefinition.tsx b/packages/hub/src/app/new/definition/NewDefinition.tsx index 2900d24b1f..317588966a 100644 --- a/packages/hub/src/app/new/definition/NewDefinition.tsx +++ b/packages/hub/src/app/new/definition/NewDefinition.tsx @@ -29,7 +29,7 @@ export const NewDefinition: FC = () => { })), recommendedUnit: data.recommendedUnit ?? undefined, })} - onCompleted={(data) => { + onSuccess={(data) => { router.push( relativeValuesRoute({ owner: data.owner, diff --git a/packages/hub/src/app/new/group/NewGroup.tsx b/packages/hub/src/app/new/group/NewGroup.tsx index 6e28babaf7..592d5b3bbe 100644 --- a/packages/hub/src/app/new/group/NewGroup.tsx +++ b/packages/hub/src/app/new/group/NewGroup.tsx @@ -25,11 +25,11 @@ export const NewGroup: FC = () => { defaultValues: {}, mode: "onChange", blockOnSuccess: true, - formDataToVariables: (data) => ({ + formDataToInput: (data) => ({ slug: data.slug ?? "", // shouldn't happen, but satisfies TypeScript }), action: createGroupAction, - onCompleted(result) { + onSuccess(result) { router.push(groupRoute({ slug: result.slug })); }, }); diff --git a/packages/hub/src/app/relative-values/[owner]/[slug]/edit/EditRelativeValuesDefinition.tsx b/packages/hub/src/app/relative-values/[owner]/[slug]/edit/EditRelativeValuesDefinition.tsx index d736b31d51..fc7deb9268 100644 --- a/packages/hub/src/app/relative-values/[owner]/[slug]/edit/EditRelativeValuesDefinition.tsx +++ b/packages/hub/src/app/relative-values/[owner]/[slug]/edit/EditRelativeValuesDefinition.tsx @@ -45,7 +45,7 @@ export const EditRelativeValuesDefinition: FC<{ })), recommendedUnit: data.recommendedUnit || undefined, })} - onCompleted={(data) => { + onSuccess={(data) => { router.push( relativeValuesRoute({ owner: data.owner, diff --git a/packages/hub/src/app/settings/choose-username/ChooseUsername.tsx b/packages/hub/src/app/settings/choose-username/ChooseUsername.tsx index 9c7be63b09..08c8b7fb83 100644 --- a/packages/hub/src/app/settings/choose-username/ChooseUsername.tsx +++ b/packages/hub/src/app/settings/choose-username/ChooseUsername.tsx @@ -21,10 +21,10 @@ export const ChooseUsername: FC = () => { typeof setUsernameAction >({ action: setUsernameAction, - onCompleted: () => { + onSuccess: () => { router.replace("/"); }, - formDataToVariables: (data) => ({ username: data.username }), + formDataToInput: (data) => ({ username: data.username }), blockOnSuccess: true, }); diff --git a/packages/hub/src/components/ui/SafeActionModalAction.tsx b/packages/hub/src/components/ui/SafeActionModalAction.tsx index 9eb76e9741..e4fbc726f9 100644 --- a/packages/hub/src/components/ui/SafeActionModalAction.tsx +++ b/packages/hub/src/components/ui/SafeActionModalAction.tsx @@ -16,10 +16,10 @@ type CommonProps< Action extends HookSafeActionFn, > = Pick< Parameters>[0], - | "formDataToVariables" + | "formDataToInput" | "defaultValues" | "action" - | "onCompleted" + | "onSuccess" | "blockOnSuccess" > & { initialFocus?: FieldPath; @@ -30,12 +30,12 @@ function SafeActionFormModal< TFormShape extends FieldValues, const Action extends HookSafeActionFn, >({ - formDataToVariables, + formDataToInput, initialFocus, defaultValues, submitText, action, - onCompleted, + onSuccess, title, children, }: PropsWithChildren> & { @@ -48,9 +48,9 @@ function SafeActionFormModal< mode: "onChange", defaultValues, action, - formDataToVariables, - async onCompleted(data) { - onCompleted?.(data); + formDataToInput, + async onSuccess(data) { + onSuccess?.(data); closeDropdown(); }, }); diff --git a/packages/hub/src/components/ui/ServerActionButton.tsx b/packages/hub/src/components/ui/ServerActionButton.tsx deleted file mode 100644 index c22d068f53..0000000000 --- a/packages/hub/src/components/ui/ServerActionButton.tsx +++ /dev/null @@ -1,39 +0,0 @@ -"use client"; -import { ReactNode, useActionState } from "react"; - -import { Button, useToast } from "@quri/ui"; - -/* - * Props for this component include: - * - some props that are passed to ` - - ); -} diff --git a/packages/hub/src/components/ui/ServerActionDropdownAction.tsx b/packages/hub/src/components/ui/ServerActionDropdownAction.tsx deleted file mode 100644 index 79c390cff4..0000000000 --- a/packages/hub/src/components/ui/ServerActionDropdownAction.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { FC, useTransition } from "react"; - -import { DropdownMenuActionItem, IconProps, useCloseDropdown } from "@quri/ui"; - -import { useCloseDropdownOnInvariantChange } from "./CloseDropdownOnInvariantChange"; - -export const ServerActionDropdownAction: FC<{ - title: string; - icon?: FC; - act: () => Promise; - // If set, the dropdown will close only after the invariant changes. - // This is useful, because server action returns before it sends back the revalidated UI. - // Re-rendering the new UI might take a while (it's async), so we don't want to close the dropdown immediately. - // This is an ugly workaround; see also: https://github.com/vercel/next.js/discussions/53206 - // Discussion in QURI Slack: https://quri.slack.com/archives/C059EEU0HMM/p1732810277978719 - // - // Also note that in some cases even `invariant` is not enough. Consider the scenario where the list of items in the dropdown is based on the component props. - // In this case, this action would be unmounted before it would get the chance to close the dropdown. - // In that scenario you might prefer to use `` instead. - // (The example of this is ``.) - invariant?: unknown; -}> = ({ title, icon, act: originalAct, invariant }) => { - const close = useCloseDropdown(); - - const [isPending, startTransition] = useTransition(); - const act = () => { - startTransition(async () => { - await originalAct(); - - // if there's no invariant, close the dropdown immediately - if (invariant === undefined) { - close(); - } - }); - }; - - useCloseDropdownOnInvariantChange(invariant); - - return ( - - ); -}; diff --git a/packages/hub/src/components/ui/ServerActionModalAction.tsx b/packages/hub/src/components/ui/ServerActionModalAction.tsx deleted file mode 100644 index 97526f5670..0000000000 --- a/packages/hub/src/components/ui/ServerActionModalAction.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { FC, PropsWithChildren, ReactNode } from "react"; -import { FieldPath, FieldValues } from "react-hook-form"; - -import { - DropdownMenuModalActionItem, - IconProps, - useCloseDropdown, -} from "@quri/ui"; - -import { FormModal } from "@/components/ui/FormModal"; -import { useServerActionForm } from "@/lib/hooks/useServerActionForm"; - -type CommonProps< - TFormShape extends FieldValues, - Action extends (input: any) => Promise, -> = Pick< - Parameters>[0], - | "formDataToVariables" - | "defaultValues" - | "action" - | "onCompleted" - | "blockOnSuccess" -> & { - initialFocus?: FieldPath; - submitText: string; -}; - -function ServerActionFormModal< - TFormShape extends FieldValues, - Action extends (input: any) => Promise, ->({ - formDataToVariables, - initialFocus, - defaultValues, - submitText, - action, - onCompleted, - title, - children, -}: PropsWithChildren> & { - title: string; -}): ReactNode { - // Note that we use the same `close` that's responsible for closing the dropdown. - const close = useCloseDropdown(); - - const { form, onSubmit, inFlight } = useServerActionForm({ - mode: "onChange", - defaultValues, - action, - formDataToVariables, - async onCompleted(data) { - onCompleted?.(data); - close(); - }, - }); - - return ( - - {children} - - ); -} - -export function ServerActionModalAction< - TFormShape extends FieldValues, - const Action extends (input: any) => Promise, ->({ - modalTitle, - title, - icon, - children, - ...modalProps -}: CommonProps & { - modalTitle: string; - title: string; - icon?: FC; - children: () => ReactNode; -}): ReactNode { - return ( - ( - - {...modalProps} - title={modalTitle} - > - {children()} - - )} - /> - ); -} diff --git a/packages/hub/src/lib/hooks/useSafeActionForm.ts b/packages/hub/src/lib/hooks/useSafeActionForm.ts index a7440b9218..9180c9999b 100644 --- a/packages/hub/src/lib/hooks/useSafeActionForm.ts +++ b/packages/hub/src/lib/hooks/useSafeActionForm.ts @@ -25,17 +25,17 @@ export function useSafeActionForm< defaultValues, mode, action, - onCompleted, - formDataToVariables, + onSuccess, + formDataToInput, blockOnSuccess, }: { // This is unfortunately not strictly type-safe: if you return extra variables that are not needed for mutation, TypeScript won't complain. // See also: https://stackoverflow.com/questions/72111571/typescript-exact-return-type-of-function // This could be solved by converting the return type to generic, but I expect that the lack of partial type parameters in TypeScript // would get in the way, so I won't even try. - formDataToVariables: (data: FormShape, extraData?: ExtraData) => ActionInput; + formDataToInput: (data: FormShape, extraData?: ExtraData) => ActionInput; action: Action; - onCompleted?: ( + onSuccess?: ( result: NonNullable["data"]> ) => void | Promise; blockOnSuccess?: boolean; @@ -49,7 +49,7 @@ export function useSafeActionForm< const { executeAsync, isPending, hasSucceeded } = useAction(action, { onSuccess: ({ data }) => { if (data) { - onCompleted?.(data); + onSuccess?.(data); } }, onError: ({ error }) => { @@ -94,14 +94,12 @@ export function useSafeActionForm< const onSubmit = useCallback( (event?: BaseSyntheticEvent, extraData?: ExtraData) => form.handleSubmit(async (formData) => { - const result = await executeAsync( - formDataToVariables(formData, extraData) - ); + const result = await executeAsync(formDataToInput(formData, extraData)); if (result?.serverError || result?.validationErrors) { throw new Error("Action failed"); } })(event), - [form, formDataToVariables, executeAsync] + [form, formDataToInput, executeAsync] ); return { diff --git a/packages/hub/src/lib/hooks/useServerActionForm.ts b/packages/hub/src/lib/hooks/useServerActionForm.ts deleted file mode 100644 index 5383b906d4..0000000000 --- a/packages/hub/src/lib/hooks/useServerActionForm.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { BaseSyntheticEvent, useCallback } from "react"; -import { FieldValues, useForm, UseFormProps } from "react-hook-form"; - -import { useToast } from "@quri/ui"; - -/** - * This hook ties together `useForm` and server actions. - * - * See also: - * - `` if your form is available through a Dropdown menu - * - * All generic type parameters to this function default to `never`, so you'll have to set them explicitly to pass type checks. - */ -export function useServerActionForm< - FormShape extends FieldValues = never, - const Action extends (input: any) => Promise = never, - ExtraData extends Record = Record, - ActionVariables = Parameters[0], - ActionResult = Awaited>, ->({ - defaultValues, - mode, - action, - onCompleted, - formDataToVariables, - blockOnSuccess, -}: { - // This is unfortunately not strictly type-safe: if you return extra variables that are not needed for mutation, TypeScript won't complain. - // See also: https://stackoverflow.com/questions/72111571/typescript-exact-return-type-of-function - // This could be solved by converting the return type to generic, but I expect that the lack of partial type parameters in TypeScript - // would get in the way, so I won't even try. - formDataToVariables: ( - data: FormShape, - extraData?: ExtraData - ) => ActionVariables; - action: (input: ActionVariables) => Promise; - onCompleted?: (result: ActionResult) => void | Promise; - blockOnSuccess?: boolean; -} & Pick, "defaultValues" | "mode">) { - const form = useForm({ defaultValues, mode }); - - const toast = useToast(); - - const onSubmit = useCallback( - (event?: BaseSyntheticEvent, extraData?: ExtraData) => - form.handleSubmit(async (formData) => { - // TODO - transition? - try { - const result = await action(formDataToVariables(formData, extraData)); - onCompleted?.(result); - } catch (error) { - toast(String(error), "error"); - // important to rethrow; otherwise form will have `isSubmitting` set to true, which can make it disabled if `blockOnSuccess` is enabled - throw error; - } - })(event), - [form, formDataToVariables, onCompleted, action, toast] - ); - - return { - form, - onSubmit, - inFlight: blockOnSuccess - ? form.formState.isSubmitting || form.formState.isSubmitSuccessful - : form.formState.isSubmitting, - }; -} diff --git a/packages/hub/src/relative-values/components/RelativeValuesDefinitionForm/index.tsx b/packages/hub/src/relative-values/components/RelativeValuesDefinitionForm/index.tsx index 5c5dfdd6ff..1fe4622bb8 100644 --- a/packages/hub/src/relative-values/components/RelativeValuesDefinitionForm/index.tsx +++ b/packages/hub/src/relative-values/components/RelativeValuesDefinitionForm/index.tsx @@ -27,7 +27,7 @@ export function RelativeValuesDefinitionForm< withoutSlug, action, formDataToInput, - onCompleted, + onSuccess, }: { defaultValues?: FormShape; withoutSlug?: boolean; @@ -35,7 +35,7 @@ export function RelativeValuesDefinitionForm< formDataToInput: ( data: FormShape ) => InferSafeActionFnInput["clientInput"]; - onCompleted?: ( + onSuccess?: ( data: NonNullable["data"]> ) => void; }) { @@ -43,8 +43,8 @@ export function RelativeValuesDefinitionForm< mode: "onChange", defaultValues, action, - formDataToVariables: formDataToInput, - onCompleted, + formDataToInput: formDataToInput, + onSuccess, }); return ( From f630ecaaa7e0eff26b3c5d4d7b6bab65b93460d3 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 3 Dec 2024 17:13:57 -0300 Subject: [PATCH 65/68] clean up auth functions --- .../hub/src/models/actions/createModelAction.ts | 4 ++-- packages/hub/src/models/actions/moveModelAction.ts | 5 +---- packages/hub/src/owners/data/auth.ts | 13 +++++++------ .../actions/createRelativeValuesDefinitionAction.ts | 12 ++---------- .../deleteRelativeValuesDefinitionAction.tsx | 5 +---- .../actions/updateRelativeValuesDefinitionAction.ts | 10 ++-------- packages/hub/src/users/auth.ts | 10 ++++++++-- 7 files changed, 23 insertions(+), 36 deletions(-) diff --git a/packages/hub/src/models/actions/createModelAction.ts b/packages/hub/src/models/actions/createModelAction.ts index 8e06f31cb1..4fb39c2b0f 100644 --- a/packages/hub/src/models/actions/createModelAction.ts +++ b/packages/hub/src/models/actions/createModelAction.ts @@ -13,7 +13,7 @@ import { } from "@/lib/server/actionClient"; import { prisma } from "@/lib/server/prisma"; import { zSlug } from "@/lib/zodUtils"; -import { getWriteableOwner } from "@/owners/data/auth"; +import { getWriteableOwnerOrSelf } from "@/owners/data/auth"; import { indexModelId } from "@/search/helpers"; import { getSelf, getSessionOrRedirect } from "@/users/auth"; @@ -51,7 +51,7 @@ export const createModelAction = actionClient const code = defaultCode; const model = await prisma.$transaction(async (tx) => { - const owner = await getWriteableOwner(session, input.groupSlug); + const owner = await getWriteableOwnerOrSelf(input.groupSlug); const model = await failValidationOnConstraint( () => diff --git a/packages/hub/src/models/actions/moveModelAction.ts b/packages/hub/src/models/actions/moveModelAction.ts index 3def0272a3..19829a0226 100644 --- a/packages/hub/src/models/actions/moveModelAction.ts +++ b/packages/hub/src/models/actions/moveModelAction.ts @@ -10,7 +10,6 @@ import { prisma } from "@/lib/server/prisma"; import { zSlug } from "@/lib/zodUtils"; import { getWriteableModel } from "@/models/utils"; import { getWriteableOwnerBySlug } from "@/owners/data/auth"; -import { getSessionOrRedirect } from "@/users/auth"; const schema = z.object({ oldOwner: zSlug, @@ -24,14 +23,12 @@ const schema = z.object({ export const moveModelAction = actionClient .schema(schema) .action(async ({ parsedInput: input }) => { - const session = await getSessionOrRedirect(); - const model = await getWriteableModel({ owner: input.oldOwner, slug: input.slug, }); - const newOwner = await getWriteableOwnerBySlug(session, input.owner.slug); + const newOwner = await getWriteableOwnerBySlug(input.owner.slug); const newModel = await failValidationOnConstraint( () => diff --git a/packages/hub/src/owners/data/auth.ts b/packages/hub/src/owners/data/auth.ts index ff26da93d1..6cc7b3531f 100644 --- a/packages/hub/src/owners/data/auth.ts +++ b/packages/hub/src/owners/data/auth.ts @@ -1,7 +1,6 @@ -import { Session } from "next-auth"; - import { auth } from "@/lib/server/auth"; import { prisma } from "@/lib/server/prisma"; +import { getSessionOrRedirect } from "@/users/auth"; export async function controlsOwnerId(ownerId: string): Promise { const session = await auth(); @@ -34,7 +33,9 @@ export async function controlsOwnerId(ownerId: string): Promise { ); } -export async function getWriteableOwnerBySlug(session: Session, slug: string) { +export async function getWriteableOwnerBySlug(slug: string) { + const session = await getSessionOrRedirect(); + const owner = await prisma.owner.findFirst({ where: { slug, @@ -65,11 +66,11 @@ export async function getWriteableOwnerBySlug(session: Session, slug: string) { return owner; } -// deprecated, need to migrate to getWriteableOwnerBySlug everywhere -export async function getWriteableOwner( - session: Session, +export async function getWriteableOwnerOrSelf( groupSlug?: string | null | undefined ) { + const session = await getSessionOrRedirect(); + const owner = await prisma.owner.findFirst({ where: { ...(groupSlug diff --git a/packages/hub/src/relative-values/actions/createRelativeValuesDefinitionAction.ts b/packages/hub/src/relative-values/actions/createRelativeValuesDefinitionAction.ts index 29600ac394..148e59a258 100644 --- a/packages/hub/src/relative-values/actions/createRelativeValuesDefinitionAction.ts +++ b/packages/hub/src/relative-values/actions/createRelativeValuesDefinitionAction.ts @@ -2,13 +2,11 @@ import { actionClient, - ActionError, failValidationOnConstraint, } from "@/lib/server/actionClient"; import { prisma } from "@/lib/server/prisma"; -import { getWriteableOwnerBySlug } from "@/owners/data/auth"; +import { getWriteableOwnerOrSelf } from "@/owners/data/auth"; import { indexDefinitionId } from "@/search/helpers"; -import { getSessionOrRedirect } from "@/users/auth"; import { inputSchema, validateRelativeValuesDefinition } from "./common"; @@ -21,13 +19,7 @@ export const createRelativeValuesDefinitionAction = actionClient owner: string; slug: string; }> => { - const session = await getSessionOrRedirect(); - const ownerSlug = input.owner ?? session.user.username; - if (!ownerSlug) { - // shouldn't happen unless this is an username-less user - throw new ActionError("Owner slug or username is required"); - } - const owner = await getWriteableOwnerBySlug(session, ownerSlug); + const owner = await getWriteableOwnerOrSelf(input.owner); validateRelativeValuesDefinition({ items: input.items, diff --git a/packages/hub/src/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx b/packages/hub/src/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx index 1a76a88c13..917d721c5f 100644 --- a/packages/hub/src/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx +++ b/packages/hub/src/relative-values/actions/deleteRelativeValuesDefinitionAction.tsx @@ -5,7 +5,6 @@ import { actionClient } from "@/lib/server/actionClient"; import { prisma } from "@/lib/server/prisma"; import { zSlug } from "@/lib/zodUtils"; import { getWriteableOwnerBySlug } from "@/owners/data/auth"; -import { getSessionOrRedirect } from "@/users/auth"; export const deleteRelativeValuesDefinitionAction = actionClient .schema( @@ -15,9 +14,7 @@ export const deleteRelativeValuesDefinitionAction = actionClient }) ) .action(async ({ parsedInput: input }): Promise<"ok"> => { - const session = await getSessionOrRedirect(); - - const owner = await getWriteableOwnerBySlug(session, input.owner); + const owner = await getWriteableOwnerBySlug(input.owner); await prisma.relativeValuesDefinition.delete({ where: { diff --git a/packages/hub/src/relative-values/actions/updateRelativeValuesDefinitionAction.ts b/packages/hub/src/relative-values/actions/updateRelativeValuesDefinitionAction.ts index 4f937fc56e..5bb69bb5d0 100644 --- a/packages/hub/src/relative-values/actions/updateRelativeValuesDefinitionAction.ts +++ b/packages/hub/src/relative-values/actions/updateRelativeValuesDefinitionAction.ts @@ -1,8 +1,7 @@ "use server"; import { actionClient } from "@/lib/server/actionClient"; import { prisma } from "@/lib/server/prisma"; -import { getWriteableOwnerBySlug } from "@/owners/data/auth"; -import { getSessionOrRedirect } from "@/users/auth"; +import { getWriteableOwnerOrSelf } from "@/owners/data/auth"; import { inputSchema, validateRelativeValuesDefinition } from "./common"; @@ -12,12 +11,7 @@ export const updateRelativeValuesDefinitionAction = actionClient async ({ parsedInput: input, }): Promise<{ owner: string; slug: string }> => { - const session = await getSessionOrRedirect(); - const ownerSlug = input.owner ?? session.user.username; - if (!ownerSlug) { - throw new Error("Owner slug or username is required"); - } - const owner = await getWriteableOwnerBySlug(session, ownerSlug); + const owner = await getWriteableOwnerOrSelf(input.owner); validateRelativeValuesDefinition({ items: input.items, diff --git a/packages/hub/src/users/auth.ts b/packages/hub/src/users/auth.ts index 7802d1ef8a..3235fb3353 100644 --- a/packages/hub/src/users/auth.ts +++ b/packages/hub/src/users/auth.ts @@ -31,13 +31,19 @@ export async function checkRootUser() { } export type SignedInSession = Session & { - user: NonNullable & { email: string }; + user: NonNullable & { + email: NonNullable; + username: NonNullable; + }; }; export function isSignedIn( session: Session | null ): session is SignedInSession { - return Boolean(session?.user.email); + // Note: username is not set initially, when the user first signs in. + // `useForceChooseUsername` hook will redirect the user to the choose username page if necessary. + // The server components and server actions involved in that shouldn't rely on this function to check if the user is signed in. + return Boolean(session?.user.email && session.user.username); } export async function getSelf(session: SignedInSession) { From 4469bbb9543142d844079fdc35095979e1dd8721 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 3 Dec 2024 17:38:48 -0300 Subject: [PATCH 66/68] replace graphql usage in ai and content-collections --- packages/ai/src/scripts/fine-tuning-setup.ts | 7 +- packages/ai/src/scripts/fine-tuning/setup.ts | 7 +- packages/ai/src/scripts/squiggleHubHelpers.ts | 98 +++++-------------- .../src/collections/squiggleAiLibraries.ts | 58 +++++++---- .../hub/src/app/api/get-group-models/route.ts | 23 +++++ packages/hub/src/squiggle/linker.ts | 2 +- 6 files changed, 94 insertions(+), 101 deletions(-) create mode 100644 packages/hub/src/app/api/get-group-models/route.ts diff --git a/packages/ai/src/scripts/fine-tuning-setup.ts b/packages/ai/src/scripts/fine-tuning-setup.ts index 24726c5385..5da4d19553 100644 --- a/packages/ai/src/scripts/fine-tuning-setup.ts +++ b/packages/ai/src/scripts/fine-tuning-setup.ts @@ -1,10 +1,7 @@ import * as fs from "fs"; import { examplesToImport } from "./fine-tuning/favoriteExamples.js"; -import { - fetchCodeFromGraphQL, - fetchGroupModels, -} from "./squiggleHubHelpers.js"; +import { fetchCodeFromHub, fetchGroupModels } from "./squiggleHubHelpers.js"; interface ProcessedModel { prompt: string; @@ -98,7 +95,7 @@ async function processFavoriteExamples(): Promise { for (const example of examplesToImport) { const [owner, slug] = example.id.split("/"); try { - const code = await fetchCodeFromGraphQL(owner, slug); + const code = await fetchCodeFromHub(owner, slug); processedExamples.push({ prompt: example.prompt, response: code.trim(), diff --git a/packages/ai/src/scripts/fine-tuning/setup.ts b/packages/ai/src/scripts/fine-tuning/setup.ts index f65ece1756..1f09415907 100644 --- a/packages/ai/src/scripts/fine-tuning/setup.ts +++ b/packages/ai/src/scripts/fine-tuning/setup.ts @@ -1,9 +1,6 @@ import * as fs from "fs"; -import { - fetchCodeFromGraphQL, - fetchGroupModels, -} from "../squiggleHubHelpers.js"; +import { fetchCodeFromHub, fetchGroupModels } from "../squiggleHubHelpers.js"; import { examplesToImport } from "./favoriteExamples.js"; interface ProcessedModel { @@ -98,7 +95,7 @@ async function processFavoriteExamples(): Promise { for (const example of examplesToImport) { const [owner, slug] = example.id.split("/"); try { - const code = await fetchCodeFromGraphQL(owner, slug); + const code = await fetchCodeFromHub(owner, slug); processedExamples.push({ prompt: example.prompt, response: code.trim(), diff --git a/packages/ai/src/scripts/squiggleHubHelpers.ts b/packages/ai/src/scripts/squiggleHubHelpers.ts index b2b97d1144..454f040708 100644 --- a/packages/ai/src/scripts/squiggleHubHelpers.ts +++ b/packages/ai/src/scripts/squiggleHubHelpers.ts @@ -1,84 +1,36 @@ -import axios from "axios"; +import { z } from "zod"; -export const librariesToImport = ["ozziegooen/sTest", "ozziegooen/helpers"]; +const SERVER = "https://squigglehub.org"; -const GRAPHQL_URL = "https://squigglehub.org/api/graphql"; - -export function getQuery(owner: string, slug: string) { - return ` - query GetModelCode { - model(input: {owner: "${owner}", slug: "${slug}"}) { - ... on Model { - id - currentRevision { - content { - ... on SquiggleSnippet { - id - code - } - } - } - } - } - } - `; -} - -export async function fetchCodeFromGraphQL( +export async function fetchCodeFromHub( owner: string, slug: string ): Promise { - const query = getQuery(owner, slug); - const response = await axios.post(GRAPHQL_URL, { query }); - return response.data.data.model.currentRevision.content.code; -} + const data = await fetch( + `${SERVER}/api/get-source?${new URLSearchParams({ + owner, + slug, + })}` + ).then((res) => res.json()); + const parsed = z.object({ code: z.string() }).safeParse(data); + if (!parsed.success) { + throw new Error(`Failed to fetch source for ${owner}/${slug}`); + } -export function getGroupModelsQuery(groupSlug: string) { - return { - query: ` - query GetGroupModels($groupSlug: String!) { - group(slug: $groupSlug) { - ... on Group { - id - models(first: 50) { - edges { - node { - currentRevision { - content { - ... on SquiggleSnippet { - code - } - } - } - } - } - } - } - } - } - `, - variables: { groupSlug }, - }; + return parsed.data.code; } export async function fetchGroupModels(groupSlug: string): Promise { - try { - const query = getGroupModelsQuery(groupSlug); - const response = await axios.post(GRAPHQL_URL, query); - - if (response.status === 200) { - const models = response.data.data.group.models.edges; - console.log(`Fetched ${models.length} models from group ${groupSlug}`); - - return models.map( - (model: { node: { currentRevision: { content: { code: string } } } }) => - model.node.currentRevision.content.code - ); - } else { - throw new Error(`Error fetching group models: ${response.statusText}`); - } - } catch (error) { - console.error("Error fetching group models:", error); - throw error; + const data = await fetch( + `${SERVER}/api/get-group-models?${new URLSearchParams({ slug: groupSlug })}` + ).then((res) => res.json()); + + const parsed = z + .object({ models: z.array(z.object({ slug: z.string() })) }) + .safeParse(data); + if (!parsed.success) { + throw new Error(`Failed to fetch group models for ${groupSlug}`); } + + return parsed.data.models.map((item) => item.slug); } diff --git a/packages/content/src/collections/squiggleAiLibraries.ts b/packages/content/src/collections/squiggleAiLibraries.ts index 4b64fe7ec8..f395b9d669 100644 --- a/packages/content/src/collections/squiggleAiLibraries.ts +++ b/packages/content/src/collections/squiggleAiLibraries.ts @@ -1,33 +1,34 @@ import { defineCollection } from "@content-collections/core"; +import { z } from "zod"; -const GRAPHQL_URL = "https://squigglehub.org/api/graphql"; +const SERVER = "https://squigglehub.org"; -export function getQuery(owner: string, slug: string) { +function getGraphqlQuery(owner: string, slug: string) { return ` - query GetModelCode { - model(input: {owner: "${owner}", slug: "${slug}"}) { - ... on Model { - id - currentRevision { - content { - ... on SquiggleSnippet { - id - code + query GetModelCode { + model(input: {owner: "${owner}", slug: "${slug}"}) { + ... on Model { + id + currentRevision { + content { + ... on SquiggleSnippet { + id + code + } } } } } } - } - `; + `; } -export async function fetchCodeFromGraphQL( +export async function fetchCodeFromHubLegacy( owner: string, slug: string ): Promise { - const query = getQuery(owner, slug); - const response = await fetch(GRAPHQL_URL, { + const query = getGraphqlQuery(owner, slug); + const response = await fetch("https://squigglehub.org/api/graphql", { headers: { "Content-Type": "application/json", }, @@ -42,6 +43,29 @@ export async function fetchCodeFromGraphQL( return code; } +// copy-pasted from squiggle/packages/ai/src/scripts/squiggleHubHelpers.ts +export async function fetchCodeFromHub( + owner: string, + slug: string +): Promise { + try { + const data = await fetch( + `${SERVER}/api/get-source?${new URLSearchParams({ + owner, + slug, + })}` + ).then((res) => res.json()); + const parsed = z.object({ code: z.string() }).safeParse(data); + if (!parsed.success) { + throw new Error(`Failed to fetch source for ${owner}/${slug}`); + } + + return parsed.data.code; + } catch (e) { + return await fetchCodeFromHubLegacy(owner, slug); + } +} + export const squiggleAiLibraries = defineCollection({ name: "squiggleAiLibraries", directory: "content/squiggleAiLibraries", @@ -52,7 +76,7 @@ export const squiggleAiLibraries = defineCollection({ slug: z.string(), }), transform: async (data) => { - const code = await fetchCodeFromGraphQL(data.owner, data.slug); + const code = await fetchCodeFromHub(data.owner, data.slug); const importName = `hub:${data.owner}/${data.slug}`; return { ...data, importName, code }; diff --git a/packages/hub/src/app/api/get-group-models/route.ts b/packages/hub/src/app/api/get-group-models/route.ts new file mode 100644 index 0000000000..db0a4e0e84 --- /dev/null +++ b/packages/hub/src/app/api/get-group-models/route.ts @@ -0,0 +1,23 @@ +import { NextRequest } from "next/server"; +import { z } from "zod"; + +import { loadModelCards } from "@/models/data/cards"; + +// We're not calling this as a server actions because it'd be too slow (server actions are sequential). +// TODO: it'd be good to use tRPC for this. +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + + const { slug } = z + .object({ + slug: z.string(), + }) + .parse(Object.fromEntries(searchParams.entries())); + + const page = await loadModelCards({ ownerSlug: slug, limit: 100 }); + + return Response.json({ + models: page.items, + hasMore: !!page.loadMore, + }); +} diff --git a/packages/hub/src/squiggle/linker.ts b/packages/hub/src/squiggle/linker.ts index 2177012652..b4ea7098f1 100644 --- a/packages/hub/src/squiggle/linker.ts +++ b/packages/hub/src/squiggle/linker.ts @@ -43,7 +43,7 @@ const linker: SqLinker = { const { owner, slug } = parseSourceId(sourceId); const data = await fetch( - `/api/get-source?owner=${owner}&slug=${slug}` + `/api/get-source?${new URLSearchParams({ owner, slug })}` ).then((res) => res.json()); const parsed = z.object({ code: z.string() }).safeParse(data); From 740c6a8326a855a9f2c832910a072d75bfdafabf Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 5 Dec 2024 15:57:52 -0300 Subject: [PATCH 67/68] lint fixes --- .../hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx | 2 +- .../hub/src/app/groups/[slug]/members/GroupMemberList.tsx | 4 ++-- .../components/layout/RootLayout/useForceChooseUsername.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx b/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx index 135472af08..0591fc28ab 100644 --- a/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx +++ b/packages/hub/src/app/admin/upgrade-versions/UpgradeableModel.tsx @@ -79,7 +79,7 @@ export const UpgradeableModel: FC<{ setModel(null); } }); - }, []); + }, [incompleteModel]); if (model === "loading") { return ; diff --git a/packages/hub/src/app/groups/[slug]/members/GroupMemberList.tsx b/packages/hub/src/app/groups/[slug]/members/GroupMemberList.tsx index 83df18c47c..6614b83309 100644 --- a/packages/hub/src/app/groups/[slug]/members/GroupMemberList.tsx +++ b/packages/hub/src/app/groups/[slug]/members/GroupMemberList.tsx @@ -30,14 +30,14 @@ export const GroupMemberList: FC = ({ (membership: GroupMemberDTO) => { page.update((item) => (item.id === membership.id ? membership : item)); }, - [page.update] + [page] ); const removeMembership = useCallback( (membership: GroupMemberDTO) => { page.remove((item) => item.id === membership.id); }, - [page.remove] + [page] ); return ( diff --git a/packages/hub/src/components/layout/RootLayout/useForceChooseUsername.ts b/packages/hub/src/components/layout/RootLayout/useForceChooseUsername.ts index 1dd5427d36..5143532655 100644 --- a/packages/hub/src/components/layout/RootLayout/useForceChooseUsername.ts +++ b/packages/hub/src/components/layout/RootLayout/useForceChooseUsername.ts @@ -16,7 +16,7 @@ export function useForceChooseUsername(session: Session | null) { if (shouldRedirect) { router.push(chooseUsernameRoute()); } - }, [shouldRedirect]); + }, [shouldRedirect, router]); return { shouldRedirect, shouldChoose }; } From 215ca10de6d615c4bd678f4443bcd38c74c56b59 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 5 Dec 2024 16:11:32 -0300 Subject: [PATCH 68/68] force-dynamic to fix CI builds --- packages/hub/src/app/(frontpage)/definitions/page.tsx | 2 ++ packages/hub/src/app/(frontpage)/groups/page.tsx | 2 ++ packages/hub/src/app/status/page.tsx | 2 ++ 3 files changed, 6 insertions(+) diff --git a/packages/hub/src/app/(frontpage)/definitions/page.tsx b/packages/hub/src/app/(frontpage)/definitions/page.tsx index a320040352..50efc9ad56 100644 --- a/packages/hub/src/app/(frontpage)/definitions/page.tsx +++ b/packages/hub/src/app/(frontpage)/definitions/page.tsx @@ -6,3 +6,5 @@ export default async function DefinitionsPage() { return ; } + +export const dynamic = "force-dynamic"; diff --git a/packages/hub/src/app/(frontpage)/groups/page.tsx b/packages/hub/src/app/(frontpage)/groups/page.tsx index 3aa2e4aa7d..7867e1cc62 100644 --- a/packages/hub/src/app/(frontpage)/groups/page.tsx +++ b/packages/hub/src/app/(frontpage)/groups/page.tsx @@ -6,3 +6,5 @@ export default async function OuterGroupsPage() { return ; } + +export const dynamic = "force-dynamic"; diff --git a/packages/hub/src/app/status/page.tsx b/packages/hub/src/app/status/page.tsx index 65b6f346ab..adb6381216 100644 --- a/packages/hub/src/app/status/page.tsx +++ b/packages/hub/src/app/status/page.tsx @@ -30,3 +30,5 @@ export default async function OuterFrontPage() { export const metadata: Metadata = { title: "Status", }; + +export const dynamic = "force-dynamic";