diff --git a/.gitignore b/.gitignore index a9443a50..7f2072ec 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,5 @@ Thumbs.db **/@generated .nx-container -.env.local \ No newline at end of file +.env.local +schema.graphql \ No newline at end of file diff --git a/README.md b/README.md index bb07527e..ef00975a 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ npm install ### Spin up development dependencies via Docker Compose -Pezzo relies on Postgres and [Supertokens](https://supertokens.com/). You can spin it up using Docker Compose: +Pezzo relies on Postgres, [InfluxDB](https://github.com/influxdata/influxdb) and [Supertokens](https://supertokens.com/). You can spin it up using Docker Compose: ``` docker-compose -f docker-compose.dev.yaml up diff --git a/apps/console/src/app/app.tsx b/apps/console/src/app/app.tsx index 6e269046..3cd8c8d1 100644 --- a/apps/console/src/app/app.tsx +++ b/apps/console/src/app/app.tsx @@ -1,4 +1,4 @@ -import { Navigate, Route, Routes } from "react-router-dom"; +import { Route, Routes, Navigate, Outlet } from "react-router-dom"; import * as reactRouterDom from "react-router-dom"; import "antd/dist/reset.css"; import "./styles.css"; @@ -12,99 +12,105 @@ import { EnvironmentsPage } from "./pages/environments"; import { PromptsPage } from "./pages/prompts"; import { PromptPage } from "./pages/prompts/[promptId]"; import { APIKeysPage } from "./pages/api-keys"; -import { SideNavigationLayout } from "./components/layout/SideNavigationLayout"; - import { initSuperTokens } from "./lib/auth/supertokens"; import { SuperTokensWrapper } from "supertokens-auth-react"; import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui"; -import { SessionAuth } from "supertokens-auth-react/recipe/session"; import { ThirdPartyEmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/thirdpartyemailpassword/prebuiltui"; import { InfoPage } from "./pages/InfoPage"; +import { ProjectsPage } from "./pages/projects/ProjectsPage"; +import { SessionAuth } from "supertokens-auth-react/recipe/session"; +import { LayoutWrapper } from "./components/layout/LayoutWrapper"; +import { OnboardingPage } from "./pages/onboarding"; +import { AuthProvider } from "./lib/providers/AuthProvider"; initSuperTokens(); export function App() { return ( - <> - -
- - - - - - {/* We don't render the SideNavigationLayout for non-authorized routes */} - {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [ - ThirdPartyEmailPasswordPreBuiltUI, - ])} - - {/* Authorized routes */} - }> - } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - } /> - - - - - - -
-
- - // - // + +
+ + + {/* Non-authorized routes */} + + {/* We don't render the LayoutWrapper for non-authorized routes */} + {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [ + ThirdPartyEmailPasswordPreBuiltUI, + ])} + + {/* Authorized routes */} + + + + + + + } + > + + + + } + /> - // {/* START: routes */} - // {/* These routes and navigation have been generated for you */} - // {/* Feel free to move and update them to fit your needs */} - //
- //
- //
- //
- //
    - //
  • - // Home - //
  • - //
  • - // Page 2 - //
  • - //
- //
+ {/* Projects selection */} + + + + } + > + } /> + } /> + - // {/* END: routes */} - // + {/* In-project routes */} + + + + + + + + } + > + } + /> + } + /> + } + /> + } + /> + } + /> + + +
+
+
+
+
); } diff --git a/apps/console/src/app/components/api-keys/ProviderApiKeyListItem.tsx b/apps/console/src/app/components/api-keys/ProviderApiKeyListItem.tsx index 4687ac98..a76ad08e 100644 --- a/apps/console/src/app/components/api-keys/ProviderApiKeyListItem.tsx +++ b/apps/console/src/app/components/api-keys/ProviderApiKeyListItem.tsx @@ -3,10 +3,11 @@ import styled from "@emotion/styled"; import { CloseOutlined, EditOutlined, SaveOutlined } from "@ant-design/icons"; import { useState } from "react"; import { useMutation } from "@tanstack/react-query"; -import { UPDATE_PROVIDER_API_KEY } from "../../graphql/mutations/provider-api-keys"; +import { UPDATE_PROVIDER_API_KEY } from "../../graphql/mutations/api-keys"; import { gqlClient, queryClient } from "../../lib/graphql"; import { CreateProviderApiKeyInput } from "@pezzo/graphql"; import { useEffect } from "react"; +import { useCurrentProject } from "../../lib/providers/CurrentProjectContext"; const APIKeyContainer = styled.div` display: flex; @@ -25,12 +26,14 @@ export const ProviderApiKeyListItem = ({ value, iconBase64, }: Props) => { + const { project } = useCurrentProject(); const updateKeyMutation = useMutation({ mutationFn: (data: CreateProviderApiKeyInput) => gqlClient.request(UPDATE_PROVIDER_API_KEY, { data: { provider: data.provider, value: data.value, + projectId: data.projectId, }, }), onSuccess: () => { @@ -52,7 +55,11 @@ export const ProviderApiKeyListItem = ({ }; const handleSave = async () => { - await updateKeyMutation.mutateAsync({ provider, value: editValue }); + await updateKeyMutation.mutateAsync({ + provider, + value: editValue, + projectId: project.id, + }); setIsEditing(false); }; diff --git a/apps/console/src/app/components/environments/CreateEnvironmentModal.tsx b/apps/console/src/app/components/environments/CreateEnvironmentModal.tsx index 8a6e037e..f6466e46 100644 --- a/apps/console/src/app/components/environments/CreateEnvironmentModal.tsx +++ b/apps/console/src/app/components/environments/CreateEnvironmentModal.tsx @@ -6,6 +6,7 @@ import { slugify } from "../../lib/utils/string-utils"; import { CREATE_ENVIRONMENT } from "../../graphql/mutations/environments"; import { CreateEnvironmentMutation } from "@pezzo/graphql"; import { GraphQLErrorResponse } from "../../graphql/types"; +import { useCurrentProject } from "../../lib/providers/CurrentProjectContext"; interface Props { open: boolean; @@ -19,6 +20,7 @@ type Inputs = { }; export const CreateEnvironmentModal = ({ open, onClose, onCreated }: Props) => { + const { project } = useCurrentProject(); const [form] = Form.useForm(); const { mutate, error } = useMutation< @@ -31,6 +33,7 @@ export const CreateEnvironmentModal = ({ open, onClose, onCreated }: Props) => { data: { slug: data.slug, name: data.name, + projectId: project.id, }, }), onSuccess: (data) => { diff --git a/apps/console/src/app/components/layout/Header.tsx b/apps/console/src/app/components/layout/Header.tsx new file mode 100644 index 00000000..b7ce624a --- /dev/null +++ b/apps/console/src/app/components/layout/Header.tsx @@ -0,0 +1,44 @@ +import { Breadcrumb, Layout, Menu, Space } from "antd"; +import styled from "@emotion/styled"; +import LogoSquare from "../../../assets/logo.svg"; +import { colors } from "../../lib/theme/colors"; + +const Logo = styled.img` + height: 40px; + display: block; +`; + +export const Header = () => { + return ( + +
+ + + +
+ +
+ + + + ); +}; diff --git a/apps/console/src/app/components/layout/LayoutWrapper.tsx b/apps/console/src/app/components/layout/LayoutWrapper.tsx new file mode 100644 index 00000000..d01a155d --- /dev/null +++ b/apps/console/src/app/components/layout/LayoutWrapper.tsx @@ -0,0 +1,57 @@ +import { Breadcrumb, Layout, Row, Space, theme } from "antd"; +import { SideNavigation } from "./SideNavigation"; +import styled from "@emotion/styled"; +import { Header } from "./Header"; +import { useBreadcrumbItems } from "../../lib/hooks/useBreadcrumbItems"; + +const StyledContent = styled(Layout.Content)` + padding: 18px; + max-width: 1280px; + margin-inline: auto; + + max-height: calc(100vh - 64px); + overflow-y: auto; + + scrollbar-width: none; /* Firefox */ + ::-webkit-scrollbar { + display: none; /* Chrome, Safari, Opera */ + } + ::-ms-scrollbar { + display: none; /* IE */ + } +`; + +interface Props { + children: React.ReactNode; + withSideNav: boolean; + withHeader?: boolean; +} + +export const LayoutWrapper = ({ + children, + withSideNav, + withHeader = true, +}: Props) => { + const breadcrumbItems = useBreadcrumbItems(); + const { token } = theme.useToken(); + + return ( + + {withHeader &&
} +
+ {withSideNav && } + + + {children} + +
+ + ); +}; diff --git a/apps/console/src/app/components/layout/SideNavigation.tsx b/apps/console/src/app/components/layout/SideNavigation.tsx index 800316b0..77dc50fe 100644 --- a/apps/console/src/app/components/layout/SideNavigation.tsx +++ b/apps/console/src/app/components/layout/SideNavigation.tsx @@ -10,9 +10,9 @@ import { useNavigate, useLocation } from "react-router-dom"; import styled from "@emotion/styled"; import { signOut } from "supertokens-auth-react/recipe/thirdpartyemailpassword"; -import LogoSquare from "../../../assets/logo-square.svg"; import { colors } from "../../lib/theme/colors"; import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline"; +import { useCurrentProject } from "../../lib/providers/CurrentProjectContext"; const topMenuItems = [ { @@ -46,19 +46,13 @@ const bottomMenuItems = [ ]; const SidebarContainer = styled.div` - width: 80px; background: #141414; - border-inline-end: 1px solid ${colors.neutral["800"]}; + border-inline-end: 1px solid ${colors.neutral["700"]}; height: 100%; display: flex; flex-direction: column; -`; - -const Logo = styled.img` - width: 36px; - margin: 20px auto; - display: block; + overflow: hidden; `; const BaseMenu = styled(Menu)` @@ -72,12 +66,13 @@ const TopMenu = styled(BaseMenu)` const BottomMenu = styled(BaseMenu)``; export const SideNavigation = () => { + const { project } = useCurrentProject(); const location = useLocation(); const navigate = useNavigate(); const [isCollapsed] = useState(true); const handleTopMenuClick = (item) => { - navigate(`/${item.key}`); + navigate(`/projects/${project.id}/${item.key}`); }; const handleBottomMenuClick = async (item) => { @@ -86,15 +81,12 @@ export const SideNavigation = () => { window.location.href = "/login"; } - if (item.key === "info") { - navigate("/info"); - } + if (item.key === "info") navigate(`/projects/${project.id}/info`); }; return ( - + - ( - - - - - - -); diff --git a/apps/console/src/app/components/projects/CreateNewProjectModal.tsx b/apps/console/src/app/components/projects/CreateNewProjectModal.tsx new file mode 100644 index 00000000..c08d4222 --- /dev/null +++ b/apps/console/src/app/components/projects/CreateNewProjectModal.tsx @@ -0,0 +1,84 @@ +import { Alert, Form, Input, Modal, Space, Typography, theme } from "antd"; +import { useCallback } from "react"; +import { useCreateProjectMutation } from "../../lib/hooks/mutations"; +import { useAuthContext } from "../../lib/providers/AuthProvider"; + +interface Props { + open: boolean; + onClose: () => void; + onCreated: () => void; +} + +export const CreateNewProjectModal = ({ open, onClose, onCreated }: Props) => { + const [form] = Form.useForm<{ projectName: string }>(); + const { + mutateAsync: createProject, + error, + isLoading: isCreatingProject, + } = useCreateProjectMutation({ + onSuccess: () => { + form.resetFields(); + onCreated(); + }, + }); + const { currentUser } = useAuthContext(); + const { token } = theme.useToken(); + + const handleCreateProject = useCallback(async () => { + void createProject({ + name: form.getFieldValue("projectName"), + organizationId: currentUser.organizationIds[0], + }).catch(() => { + form.resetFields(); + }); + }, [currentUser, createProject, form]); + + return ( + + Create Project + + } + open={open} + onCancel={onClose} + okText="Create" + okButtonProps={{ + form: "create-project-form", + htmlType: "submit", + loading: isCreatingProject, + }} + > +
+ + {error && ( + + )} + Name + + + + + +
+
+ ); +}; diff --git a/apps/console/src/app/components/projects/ProjectCard.tsx b/apps/console/src/app/components/projects/ProjectCard.tsx new file mode 100644 index 00000000..b8a2fda6 --- /dev/null +++ b/apps/console/src/app/components/projects/ProjectCard.tsx @@ -0,0 +1,28 @@ +import { ArrowRightCircleIcon } from "@heroicons/react/24/outline"; +import { Card, Row, Typography } from "antd"; +import { useNavigate } from "react-router-dom"; + +interface ProjectCardProps { + name: string; + slug: string; + id: string; +} +export const ProjectCard = ({ name, slug, id }: ProjectCardProps) => { + const navigate = useNavigate(); + + return ( + navigate(`/projects/${id}/prompts`)} + style={{ marginBottom: 16, height: 122 }} + > + + + {name} + + + + + + ); +}; diff --git a/apps/console/src/app/components/projects/index.ts b/apps/console/src/app/components/projects/index.ts new file mode 100644 index 00000000..36bbd7df --- /dev/null +++ b/apps/console/src/app/components/projects/index.ts @@ -0,0 +1 @@ +export * from "./ProjectCard"; diff --git a/apps/console/src/app/components/prompts/CreatePromptModal.tsx b/apps/console/src/app/components/prompts/CreatePromptModal.tsx index 2c9dd13e..00428790 100644 --- a/apps/console/src/app/components/prompts/CreatePromptModal.tsx +++ b/apps/console/src/app/components/prompts/CreatePromptModal.tsx @@ -7,6 +7,7 @@ import { PromptIntegrationSelector } from "./PromptIntegrationSelector"; import { integrations } from "@pezzo/integrations"; import { CreatePromptMutation } from "@pezzo/graphql"; import { GraphQLErrorResponse } from "../../graphql/types"; +import { useCurrentProject } from "../../lib/providers/CurrentProjectContext"; const integrationsArray = Object.values(integrations); @@ -22,6 +23,7 @@ type Inputs = { }; export const CreatePromptModal = ({ open, onClose, onCreated }: Props) => { + const { project } = useCurrentProject(); const [form] = Form.useForm(); const { mutate, error } = useMutation< @@ -34,6 +36,7 @@ export const CreatePromptModal = ({ open, onClose, onCreated }: Props) => { data: { name: data.name, integrationId: data.integrationId, + projectId: project.id, }, }), onSuccess: (data) => { diff --git a/apps/console/src/app/components/prompts/PromptTester/PromptTester.tsx b/apps/console/src/app/components/prompts/PromptTester.tsx similarity index 95% rename from apps/console/src/app/components/prompts/PromptTester/PromptTester.tsx rename to apps/console/src/app/components/prompts/PromptTester.tsx index 72634430..0d951c50 100644 --- a/apps/console/src/app/components/prompts/PromptTester/PromptTester.tsx +++ b/apps/console/src/app/components/prompts/PromptTester.tsx @@ -16,11 +16,11 @@ import { CloseCircleOutlined, RedoOutlined, } from "@ant-design/icons"; -import { PromptVariables } from "../PromptVariables"; -import { PromptEditor } from "../PromptEditor"; +import { PromptVariables } from "./PromptVariables"; +import { PromptEditor } from "./PromptEditor"; import { useEffect, useState } from "react"; -import { usePromptTester } from "../../../lib/providers/PromptTesterContext"; -import { isJson } from "../../../lib/utils/is-json"; +import { usePromptTester } from "../../lib/providers/PromptTesterContext"; +import { isJson } from "../../lib/utils/is-json"; const StyledPre = styled.pre` white-space: pre-wrap; diff --git a/apps/console/src/app/components/prompts/PublishPromptModal.tsx b/apps/console/src/app/components/prompts/PublishPromptModal.tsx index c6da8c49..1cc441b1 100644 --- a/apps/console/src/app/components/prompts/PublishPromptModal.tsx +++ b/apps/console/src/app/components/prompts/PublishPromptModal.tsx @@ -6,6 +6,7 @@ import { useMutation } from "@tanstack/react-query"; import { gqlClient, queryClient } from "../../lib/graphql"; import { PUBLISH_PROMPT } from "../../graphql/mutations/prompt-environments"; import { PublishPromptInput } from "@pezzo/graphql"; +import { useCurrentProject } from "../../lib/providers/CurrentProjectContext"; interface Props { open: boolean; @@ -13,6 +14,7 @@ interface Props { } export const PublishPromptModal = ({ open, onClose }: Props) => { + const { project } = useCurrentProject(); const { currentPromptVersion, prompt } = useCurrentPrompt(); const { environments } = useEnvironments(); const [selectedEnvironmentSlug, setSelectedEnvironmentSlug] = @@ -22,13 +24,7 @@ export const PublishPromptModal = ({ open, onClose }: Props) => { const publishPromptMutation = useMutation({ mutationFn: (data: PublishPromptInput) => - gqlClient.request(PUBLISH_PROMPT, { - data: { - promptId: data.promptId, - environmentSlug: data.environmentSlug, - promptVersionSha: data.promptVersionSha, - }, - }), + gqlClient.request(PUBLISH_PROMPT, { data }), mutationKey: ["publishPrompt", prompt.id, currentPromptVersion.sha], onSuccess: () => { queryClient.invalidateQueries(["promptEnvironments"]); @@ -39,6 +35,7 @@ export const PublishPromptModal = ({ open, onClose }: Props) => { publishPromptMutation.mutate({ promptId: prompt.id, environmentSlug: selectedEnvironmentSlug, + projectId: project.id, promptVersionSha: currentPromptVersion.sha, }); }; diff --git a/apps/console/src/app/components/prompts/metrics/SimpleChart.tsx b/apps/console/src/app/components/prompts/metrics/SimpleChart.tsx new file mode 100644 index 00000000..eea463be --- /dev/null +++ b/apps/console/src/app/components/prompts/metrics/SimpleChart.tsx @@ -0,0 +1,44 @@ +import { + CartesianGrid, + Tooltip, + Line, + LineChart, + ResponsiveContainer, + XAxis, + YAxis, +} from "recharts"; +import { useMetric } from "../../../lib/providers/MetricContext"; + +interface Props { + tooltipFormatter?: (value: string) => string; + lineLabel?: string; +} + +export const SimpleChart = ({ tooltipFormatter, lineLabel }: Props) => { + const { data: metricData, formatTimestamp } = useMetric(); + + const data = metricData.map((metric) => ({ + timestamp: formatTimestamp(metric.time), + value: metric.value, + })); + + return ( + + + + + + v)} /> + + + + ); +}; diff --git a/apps/console/src/app/components/prompts/views/DashboardView.tsx b/apps/console/src/app/components/prompts/views/DashboardView.tsx new file mode 100644 index 00000000..5663d3a0 --- /dev/null +++ b/apps/console/src/app/components/prompts/views/DashboardView.tsx @@ -0,0 +1,58 @@ +import { Aggregation, PromptExecutionMetricField } from "@pezzo/graphql"; +import { Col, Row, theme } from "antd"; +import { MetricProvider } from "../../../lib/providers/MetricContext"; +import { SimpleChart } from "../metrics/SimpleChart"; + +export const DashboardView = () => { + const { token } = theme.useToken(); + + return ( +
+ + + + `$${Number(v).toFixed(3)}`} + /> + + + + + + + + + + + + + + + + + + +
+ ); +}; diff --git a/apps/console/src/app/components/prompts/views/PromptEditView.tsx b/apps/console/src/app/components/prompts/views/PromptEditView.tsx index 27583242..6a98fc48 100644 --- a/apps/console/src/app/components/prompts/views/PromptEditView.tsx +++ b/apps/console/src/app/components/prompts/views/PromptEditView.tsx @@ -14,7 +14,7 @@ import { } from "../../../lib/hooks/usePromptEdit"; import { useCurrentPrompt } from "../../../lib/providers/CurrentPromptContext"; import { useEffect, useState } from "react"; -import { PromptTester } from "../PromptTester/PromptTester"; +import { PromptTester } from "../PromptTester"; import { PromptVariables } from "../PromptVariables"; import { usePromptTester } from "../../../lib/providers/PromptTesterContext"; import { PromptVersionSelector } from "../PromptVersionSelector"; diff --git a/apps/console/src/app/components/prompts/views/PromptHistoryView.tsx b/apps/console/src/app/components/prompts/views/PromptHistoryView.tsx index 29e189fc..667f47b4 100644 --- a/apps/console/src/app/components/prompts/views/PromptHistoryView.tsx +++ b/apps/console/src/app/components/prompts/views/PromptHistoryView.tsx @@ -4,7 +4,7 @@ import { Button, Space, Table, Tag, Tooltip } from "antd"; import { PromptExecutionStatus } from "@pezzo/graphql"; import { CheckCircleOutlined, CloseCircleOutlined } from "@ant-design/icons"; import { InlineCodeSnippet } from "../../common/InlineCodeSnippet"; -import { PromptTester } from "../PromptTester/PromptTester"; +import { PromptTester } from "../PromptTester"; import { usePromptTester } from "../../../lib/providers/PromptTesterContext"; import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { css } from "@emotion/css"; @@ -49,7 +49,7 @@ export const PromptHistoryView = () => { title: "Cost", dataIndex: "totalCost", key: "totalCost", - render: (value) => ${value}, + render: (value) => ${Number(value).toFixed(4)}, }, { title: "Duration", diff --git a/apps/console/src/app/graphql/mutations/provider-api-keys.tsx b/apps/console/src/app/graphql/mutations/api-keys.tsx similarity index 100% rename from apps/console/src/app/graphql/mutations/provider-api-keys.tsx rename to apps/console/src/app/graphql/mutations/api-keys.tsx diff --git a/apps/console/src/app/graphql/queries/api-keys.tsx b/apps/console/src/app/graphql/queries/api-keys.tsx index 09c62c12..8594d264 100644 --- a/apps/console/src/app/graphql/queries/api-keys.tsx +++ b/apps/console/src/app/graphql/queries/api-keys.tsx @@ -1,8 +1,8 @@ import { graphql } from "@pezzo/graphql"; export const GET_ALL_PROVIDER_API_KEYS = graphql(/* GraphQL */ ` - query ProviderApiKeys { - providerApiKeys { + query ProviderApiKeys($data: GetProviderApiKeysInput!) { + providerApiKeys(data: $data) { id provider value @@ -11,8 +11,8 @@ export const GET_ALL_PROVIDER_API_KEYS = graphql(/* GraphQL */ ` `); export const GET_CURRENT_PEZZO_API_KEY = graphql(/* GraphQL */ ` - query ApiKeys { - currentApiKey { + query ApiKeys($data: GetApiKeyInput!) { + currentApiKey(data: $data) { id } } diff --git a/apps/console/src/app/graphql/queries/environments.tsx b/apps/console/src/app/graphql/queries/environments.tsx index 24b1c5d8..fd5037a3 100644 --- a/apps/console/src/app/graphql/queries/environments.tsx +++ b/apps/console/src/app/graphql/queries/environments.tsx @@ -1,8 +1,8 @@ import { graphql } from "@pezzo/graphql"; export const GET_ALL_ENVIRONMENTS = graphql(/* GraphQL */ ` - query Environments { - environments { + query Environments($data: GetEnvironmentsInput!) { + environments(data: $data) { slug name } diff --git a/apps/console/src/app/graphql/queries/metrics.tsx b/apps/console/src/app/graphql/queries/metrics.tsx new file mode 100644 index 00000000..6b792fd6 --- /dev/null +++ b/apps/console/src/app/graphql/queries/metrics.tsx @@ -0,0 +1,10 @@ +import { graphql } from "@pezzo/graphql"; + +export const GET_PROMPT_EXECUTION_METRICS = graphql(/* GraphQL */ ` + query getMetrics($data: GetMetricsInput!) { + metrics(data: $data) { + value + time + } + } +`); diff --git a/apps/console/src/app/graphql/queries/projects.tsx b/apps/console/src/app/graphql/queries/projects.tsx new file mode 100644 index 00000000..f711f8a2 --- /dev/null +++ b/apps/console/src/app/graphql/queries/projects.tsx @@ -0,0 +1,30 @@ +import { graphql } from "@pezzo/graphql"; + +export const GET_PROJECT = graphql(/* GraphQL */ ` + query getProject($data: ProjectWhereUniqueInput!) { + project(data: $data) { + id + slug + name + } + } +`); + +export const GET_ALL_PROJECTS = graphql(/* GraphQL */ ` + query getProjects { + projects { + id + slug + name + } + } +`); + +export const CREATE_PROJECT = graphql(/* GraphQL */ ` + mutation createProject($data: CreateProjectInput!) { + createProject(data: $data) { + organizationId + name + } + } +`); diff --git a/apps/console/src/app/graphql/queries/prompts.tsx b/apps/console/src/app/graphql/queries/prompts.tsx index c5e7e888..1e2ef068 100644 --- a/apps/console/src/app/graphql/queries/prompts.tsx +++ b/apps/console/src/app/graphql/queries/prompts.tsx @@ -1,8 +1,8 @@ import { graphql } from "@pezzo/graphql"; export const GET_ALL_PROMPTS = graphql(/* GraphQL */ ` - query getAllPrompts { - prompts { + query getAllPrompts($data: GetProjectPromptsInput!) { + prompts(data: $data) { id name integrationId diff --git a/apps/console/src/app/graphql/queries/users.ts b/apps/console/src/app/graphql/queries/users.ts new file mode 100644 index 00000000..df795fe1 --- /dev/null +++ b/apps/console/src/app/graphql/queries/users.ts @@ -0,0 +1,21 @@ +import { graphql } from "@pezzo/graphql"; + +export const GET_ME = graphql(/* GraphQL */ ` + query GetMe { + me { + id + email + photoUrl + name + organizationIds + } + } +`); + +export const UPDATE_PROFILE = graphql(/* GraphQL */ ` + mutation UpdateProfile($data: UpdateProfileInput!) { + updateProfile(data: $data) { + name + } + } +`); diff --git a/apps/console/src/app/lib/graphql.ts b/apps/console/src/app/lib/graphql.ts index 9e680a90..fd1535bf 100644 --- a/apps/console/src/app/lib/graphql.ts +++ b/apps/console/src/app/lib/graphql.ts @@ -2,7 +2,9 @@ import { QueryClient } from "@tanstack/react-query"; import { GraphQLClient } from "graphql-request"; import { BASE_API_URL } from "../../env"; -export const gqlClient = new GraphQLClient(`${BASE_API_URL}/graphql`); +export const gqlClient = new GraphQLClient(`${BASE_API_URL}/graphql`, { + credentials: "include", +}); export const queryClient = new QueryClient({ defaultOptions: { diff --git a/apps/console/src/app/lib/hooks/mutations.ts b/apps/console/src/app/lib/hooks/mutations.ts new file mode 100644 index 00000000..7ddbd20b --- /dev/null +++ b/apps/console/src/app/lib/hooks/mutations.ts @@ -0,0 +1,37 @@ +import { + CreateProjectInput, + CreateProjectMutation, + UpdateProfileInput, +} from "@pezzo/graphql"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { gqlClient } from "../graphql"; +import { UPDATE_PROFILE } from "../../graphql/queries/users"; +import { CREATE_PROJECT } from "../../graphql/queries/projects"; +import { GraphQLErrorResponse } from "../../graphql/types"; + +export const useUpdateCurrentUserMutation = () => + useMutation({ + mutationFn: ({ name }: UpdateProfileInput) => + gqlClient.request(UPDATE_PROFILE, { data: { name } }), + }); + +export const useCreateProjectMutation = (props?: { + onSuccess?: () => void; +}) => { + const queryCache = useQueryClient(); + + return useMutation< + CreateProjectMutation, + GraphQLErrorResponse, + CreateProjectInput + >({ + mutationFn: (data: CreateProjectInput) => + gqlClient.request(CREATE_PROJECT, { data }), + onSuccess: () => { + queryCache.invalidateQueries({ + queryKey: ["projects"], + }); + props?.onSuccess?.(); + }, + }); +}; diff --git a/apps/console/src/app/lib/hooks/queries.ts b/apps/console/src/app/lib/hooks/queries.ts index 6b73f9c5..73261341 100644 --- a/apps/console/src/app/lib/hooks/queries.ts +++ b/apps/console/src/app/lib/hooks/queries.ts @@ -4,15 +4,38 @@ import { GET_ALL_PROVIDER_API_KEYS, GET_CURRENT_PEZZO_API_KEY, } from "../../graphql/queries/api-keys"; +import { useCurrentProject } from "../providers/CurrentProjectContext"; +import { GET_ME } from "../../graphql/queries/users"; +import { GET_ALL_PROJECTS } from "../../graphql/queries/projects"; -export const useApiKeys = () => - useQuery({ +export const useApiKeys = () => { + const { project } = useCurrentProject(); + return useQuery({ queryKey: ["apiKeys"], - queryFn: () => gqlClient.request(GET_CURRENT_PEZZO_API_KEY), + queryFn: () => + gqlClient.request(GET_CURRENT_PEZZO_API_KEY, { + data: { projectId: project.id }, + }), }); +}; -export const useProviderApiKeys = () => - useQuery({ +export const useProviderApiKeys = () => { + const { project } = useCurrentProject(); + + return useQuery({ queryKey: ["providerApiKeys"], - queryFn: () => gqlClient.request(GET_ALL_PROVIDER_API_KEYS), + queryFn: () => + gqlClient.request(GET_ALL_PROVIDER_API_KEYS, { + data: { projectId: project.id }, + }), + }); +}; + +export const useGetCurrentUser = () => + useQuery({ queryKey: ["me"], queryFn: () => gqlClient.request(GET_ME) }); + +export const useGetProjects = () => + useQuery({ + queryKey: ["projects"], + queryFn: () => gqlClient.request(GET_ALL_PROJECTS), }); diff --git a/apps/console/src/app/lib/hooks/useBreadcrumbItems.tsx b/apps/console/src/app/lib/hooks/useBreadcrumbItems.tsx new file mode 100644 index 00000000..15a860c6 --- /dev/null +++ b/apps/console/src/app/lib/hooks/useBreadcrumbItems.tsx @@ -0,0 +1,98 @@ +import { Link, useLocation, useParams } from "react-router-dom"; +import { useGetProjects } from "./queries"; +import React, { useMemo } from "react"; +import { useCurrentPrompt } from "../providers/CurrentPromptContext"; +import { Col, Row, Typography } from "antd"; +import { IntegrationDefinition } from "@pezzo/integrations"; +import { GetPromptQuery } from "@pezzo/graphql"; + +const breadcrumbNameMap = ( + projectId: string, + projectName: string, + prompt?: GetPromptQuery["prompt"], + integration?: IntegrationDefinition +) => { + const map: Record = { + "/projects": "Projects", + [`/projects/${projectId}`]: `${projectName}`, + [`/projects/${projectId}/prompts`]: `Prompts`, + [`/projects/${projectId}/environments`]: "Environments", + [`/projects/${projectId}/api-keys`]: "API Keys", + [`/projects/${projectId}/info`]: "Info", + }; + if (prompt && integration) { + map[`/projects/${projectId}/prompts/${prompt.id}`] = ( + + + prompt-icon + + + {prompt.name} + + + ); + } + + return map; +}; + +export const useBreadcrumbItems = () => { + const location = useLocation(); + const { projectId } = useParams(); + const { data } = useGetProjects(); + const { prompt, integration } = useCurrentPrompt(); + + const currentProject = useMemo( + () => data?.projects.find((p) => p.id === projectId), + [data, projectId] + ); + + const pathSnippets = location.pathname.split("/").filter((i) => i); + + const extraBreadcrumbItems = + projectId && data + ? pathSnippets.map((_, index) => { + const url = `/${pathSnippets.slice(0, index + 1).join("/")}`; + const selectedBreadcrumb = breadcrumbNameMap( + projectId, + currentProject.name, + prompt, + integration + )[url]; + + return { + key: url, + title: + index === pathSnippets.length - 1 ? ( + selectedBreadcrumb + ) : ( + + {selectedBreadcrumb} + + ), + }; + }) + : []; + + const breadcrumbItems = projectId + ? extraBreadcrumbItems + : [ + { + title: "Projects", + key: "projects", + }, + ]; + + if (breadcrumbItems.length === 1) return []; + + console.log(extraBreadcrumbItems); + return breadcrumbItems; +}; diff --git a/apps/console/src/app/lib/hooks/useEnvironments.ts b/apps/console/src/app/lib/hooks/useEnvironments.ts index fa5afdbf..780917c8 100644 --- a/apps/console/src/app/lib/hooks/useEnvironments.ts +++ b/apps/console/src/app/lib/hooks/useEnvironments.ts @@ -1,14 +1,21 @@ import { useQuery } from "@tanstack/react-query"; import { gqlClient } from "../graphql"; import { GET_ALL_ENVIRONMENTS } from "../../graphql/queries/environments"; +import { useCurrentProject } from "../providers/CurrentProjectContext"; export const useEnvironments = () => { - const { data } = useQuery({ + const { project } = useCurrentProject(); + + const { data, isLoading } = useQuery({ queryKey: ["environments"], - queryFn: () => gqlClient.request(GET_ALL_ENVIRONMENTS), + queryFn: () => + gqlClient.request(GET_ALL_ENVIRONMENTS, { + data: { projectId: project.id }, + }), }); return { environments: data?.environments, + isLoading, }; }; diff --git a/apps/console/src/app/lib/hooks/useGetPromptExecutionMetric.ts b/apps/console/src/app/lib/hooks/useGetPromptExecutionMetric.ts new file mode 100644 index 00000000..56fcfe33 --- /dev/null +++ b/apps/console/src/app/lib/hooks/useGetPromptExecutionMetric.ts @@ -0,0 +1,16 @@ +import { useQuery } from "@tanstack/react-query"; +import { GET_PROMPT_EXECUTION_METRICS } from "../../graphql/queries/metrics"; +import { gqlClient } from "../graphql"; +import { GetMetricsInput } from "@pezzo/graphql"; + +export const useGetPromptExecutionMetric = ( + queryKey: string[], + data: GetMetricsInput +) => + useQuery({ + queryKey, + queryFn: () => + gqlClient.request(GET_PROMPT_EXECUTION_METRICS, { + data, + }), + }); diff --git a/apps/console/src/app/lib/providers/AuthProvider.tsx b/apps/console/src/app/lib/providers/AuthProvider.tsx new file mode 100644 index 00000000..d0371a96 --- /dev/null +++ b/apps/console/src/app/lib/providers/AuthProvider.tsx @@ -0,0 +1,63 @@ +import styled from "@emotion/styled"; +import { useGetCurrentUser } from "../hooks/queries"; +import { Col, Empty, Row, Spin } from "antd"; +import { createContext, useContext, useEffect, useMemo, useState } from "react"; +import { GetMeQuery } from "@pezzo/graphql"; +import { LayoutWrapper } from "../../components/layout/LayoutWrapper"; +import { Loading3QuartersOutlined, LoadingOutlined } from "@ant-design/icons"; +import { colors } from "../theme/colors"; +import { Navigate } from "react-router-dom"; + +const SpinnerOverlay = styled(Row)` + height: 100%; +`; + +SpinnerOverlay.defaultProps = { + justify: "center", + align: "middle", +}; + +const AuthProviderContext = createContext<{ + currentUser: GetMeQuery["me"]; + isLoading: boolean; +}>({ + currentUser: undefined, + isLoading: false, +}); + +export const useAuthContext = () => useContext(AuthProviderContext); + +export const AuthProvider = ({ children }) => { + const { data, isLoading, isError } = useGetCurrentUser(); + + const value = useMemo( + () => ({ + currentUser: data?.me, + isLoading, + }), + [data, isLoading] + ); + + return ( + + {isLoading || isError || !data ? ( + + + + {isLoading ? ( + + ) : ( + + )} + + + + ) : ( + children + )} + + ); +}; diff --git a/apps/console/src/app/lib/providers/CurrentProjectContext.tsx b/apps/console/src/app/lib/providers/CurrentProjectContext.tsx new file mode 100644 index 00000000..2afaec8d --- /dev/null +++ b/apps/console/src/app/lib/providers/CurrentProjectContext.tsx @@ -0,0 +1,15 @@ +import { useMemo } from "react"; +import { useParams } from "react-router-dom"; +import { useGetProjects } from "../hooks/queries"; + +export const useCurrentProject = () => { + const { projectId } = useParams(); + const { data, isLoading } = useGetProjects(); + + const project = useMemo( + () => data?.projects.find((project) => project.id === projectId), + [data, projectId] + ); + + return { project, isLoading }; +}; diff --git a/apps/console/src/app/lib/providers/MetricContext.tsx b/apps/console/src/app/lib/providers/MetricContext.tsx new file mode 100644 index 00000000..c214d03f --- /dev/null +++ b/apps/console/src/app/lib/providers/MetricContext.tsx @@ -0,0 +1,131 @@ +import { createContext, useContext, useState } from "react"; +import { useCurrentPrompt } from "./CurrentPromptContext"; +import { + Aggregation, + GetMetricsQuery, + Granularity, + PromptExecutionMetricField, +} from "@pezzo/graphql"; +import { useGetPromptExecutionMetric } from "../hooks/useGetPromptExecutionMetric"; +import { format } from "date-fns"; +import { Card, Col, Empty, Radio, Row, Select, Typography, theme } from "antd"; +import { Loading3QuartersOutlined } from "@ant-design/icons"; + +interface MetricContextValue { + data: GetMetricsQuery["metrics"]; + formatTimestamp: (timestamp: number) => string; +} + +export const MetricContext = createContext({ + data: undefined, + formatTimestamp: () => void 0, +}); + +export const useMetric = () => useContext(MetricContext); + +interface Props { + children: React.ReactNode; + title: string; + field: PromptExecutionMetricField; + fillEmpty?: string; + aggregation: Aggregation; +} + +export const MetricProvider = ({ + children, + title, + field, + fillEmpty, + aggregation, +}: Props) => { + const { token } = theme.useToken(); + const { prompt } = useCurrentPrompt(); + const [granularity, setGranularity] = useState(Granularity.Day); + const [start, setStart] = useState("-7d"); + + const { data: metricsData, isLoading } = useGetPromptExecutionMetric( + [prompt.id, "metrics", title, granularity, start], + { + promptId: prompt.id, + start, + stop: "now()", + field, + granularity, + aggregation, + fillEmpty, + } + ); + + if (isLoading || !metricsData) { + return ; + } + + const formatTimestamp = (timestamp) => { + const date = new Date(timestamp); + + switch (granularity) { + case Granularity.Hour: + return format(date, "yyyy-MM-dd HH:mm"); + case Granularity.Day: + return format(date, "yyyy-MM-dd"); + case Granularity.Week: + return format(date, "yyyy-MM-dd"); + case Granularity.Month: + return format(date, "MMM yyyy"); + } + }; + + return ( + + +
+ {title} + {metricsData.metrics.length === 0 ? ( + + ) : ( + <> + + + + + + setGranularity(e.target.value)} + > + Hour + Day + Week + Month + + + +
{children}
+ + )} +
+
+
+ ); +}; diff --git a/apps/console/src/app/lib/providers/PromptTesterContext.tsx b/apps/console/src/app/lib/providers/PromptTesterContext.tsx index 25d8f31e..f36b1e74 100644 --- a/apps/console/src/app/lib/providers/PromptTesterContext.tsx +++ b/apps/console/src/app/lib/providers/PromptTesterContext.tsx @@ -5,6 +5,7 @@ import { gqlClient } from "../graphql"; import { GetPromptExecutionQuery, PromptExecution } from "@pezzo/graphql"; import { TestPromptResult } from "@pezzo/client"; import { useCurrentPrompt } from "./CurrentPromptContext"; +import { useCurrentProject } from "./CurrentProjectContext"; export interface PromptTestInput { content: string; @@ -39,6 +40,7 @@ export const usePromptTester = () => { }; export const PromptTesterProvider = ({ children }) => { + const { project } = useCurrentProject(); const [isTesterOpen, setIsTesterOpen] = useState(false); const [testResult, setTestResult] = useState>(undefined); @@ -59,6 +61,7 @@ export const PromptTesterProvider = ({ children }) => { setIsTestInProgress(true); const result = await gqlClient.request(TEST_PROMPT, { data: { + projectId: project.id, integrationId: integration.id, content: input.content, settings: input.settings, diff --git a/apps/console/src/app/pages/api-keys/index.tsx b/apps/console/src/app/pages/api-keys/index.tsx index a3f87fff..80d5bc03 100644 --- a/apps/console/src/app/pages/api-keys/index.tsx +++ b/apps/console/src/app/pages/api-keys/index.tsx @@ -1,10 +1,11 @@ -import { Space, Typography } from "antd"; +import { Space, Typography, theme } from "antd"; import { integrations } from "@pezzo/integrations"; import { ProviderApiKeyListItem } from "../../components/api-keys/ProviderApiKeyListItem"; import { PezzoApiKeyListItem } from "../../components/api-keys/PezzoApiKeyListItem"; import { useApiKeys, useProviderApiKeys } from "../../lib/hooks/queries"; export const APIKeysPage = () => { + const { token } = theme.useToken(); const providers = Object.values(integrations).map((integration) => ({ name: integration.name, iconBase64: integration.iconBase64, @@ -32,10 +33,10 @@ export const APIKeysPage = () => { return ( <> {apiKeyData && ( -
- Pezzo API Key +
+ Pezzo API Key - + Below you can find your Pezzo API key. This API key is provided to the Pezzo client when executing prompts. @@ -48,9 +49,9 @@ export const APIKeysPage = () => { {providerApiKeysData && (
- Provider API Keys + Provider API Keys - + In order to be able to test your prompts within the Pezzo Console, you must provide an API key for each provider you wish to test. This is optional. diff --git a/apps/console/src/app/pages/environments/index.tsx b/apps/console/src/app/pages/environments/index.tsx index 22be091d..12d68db7 100644 --- a/apps/console/src/app/pages/environments/index.tsx +++ b/apps/console/src/app/pages/environments/index.tsx @@ -1,20 +1,15 @@ import { PlusOutlined } from "@ant-design/icons"; -import { useQuery } from "@tanstack/react-query"; -import { Button, List, Typography } from "antd"; +import { Button, List, Spin, Typography, theme } from "antd"; import { InlineCodeSnippet } from "../../components/common/InlineCodeSnippet"; import { CreateEnvironmentModal } from "../../components/environments/CreateEnvironmentModal"; -import { GET_ALL_ENVIRONMENTS } from "../../graphql/queries/environments"; -import { gqlClient } from "../../lib/graphql"; import { useState } from "react"; +import { useEnvironments } from "../../lib/hooks/useEnvironments"; export const EnvironmentsPage = () => { + const { environments, isLoading } = useEnvironments(); const [isCreateEnvironmentModalOpen, setIsCreateEnvironmentModalOpen] = useState(false); - - const { data } = useQuery({ - queryKey: ["environments"], - queryFn: () => gqlClient.request(GET_ALL_ENVIRONMENTS), - }); + const { token } = theme.useToken(); return ( <> @@ -23,30 +18,32 @@ export const EnvironmentsPage = () => { onClose={() => setIsCreateEnvironmentModalOpen(false)} onCreated={() => setIsCreateEnvironmentModalOpen(false)} /> - Environments -
- -
- {data?.environments && ( - ( - - - {item.name} {item.slug} - - - )} - /> - )} + + Environments +
+ +
+ + {environments && ( + ( + + + {item.name} {item.slug} + + + )} + /> + )} +
); }; diff --git a/apps/console/src/app/pages/onboarding/OnboardingPage.tsx b/apps/console/src/app/pages/onboarding/OnboardingPage.tsx new file mode 100644 index 00000000..6135ae86 --- /dev/null +++ b/apps/console/src/app/pages/onboarding/OnboardingPage.tsx @@ -0,0 +1,165 @@ +import { InfoCircleFilled, LoadingOutlined } from "@ant-design/icons"; +import { + Button, + Col, + Input, + Row, + Space, + Tooltip, + Typography, + Card, + theme, +} from "antd"; +import { ArrowRightOutlined } from "@ant-design/icons"; +import { useNavigate } from "react-router-dom"; +import styled from "@emotion/styled"; +import { useGetProjects } from "../../lib/hooks/queries"; +import { + useCreateProjectMutation, + useUpdateCurrentUserMutation, +} from "../../lib/hooks/mutations"; +import { useCallback, useEffect } from "react"; +import { Form } from "antd"; +import { CreateProjectMutation, UpdateProfileMutation } from "@pezzo/graphql"; +import { useAuthContext } from "../../lib/providers/AuthProvider"; + +const StyledButton = styled(Button)<{ spacing: number }>` + margin-top: ${(props) => props.spacing}px; +`; + +const VerticalSpace = styled(Space)` + width: 100%; +`; +VerticalSpace.defaultProps = { + direction: "vertical", +}; + +interface FormValues { + name: string; + projectName: string; +} +export const OnboardingPage = () => { + const [form] = Form.useForm(); + const { mutateAsync: updateCurrentUser, isLoading: isUpdatingUserLoading } = + useUpdateCurrentUserMutation(); + const { mutateAsync: createProject, isLoading: isProjectCreationLoading } = + useCreateProjectMutation(); + + const { data: projectsData, isLoading: isProjectsLoading } = useGetProjects(); + + const { currentUser } = useAuthContext(); + + const { token } = theme.useToken(); + const navigate = useNavigate(); + + const isCreatingProject = isProjectCreationLoading || isUpdatingUserLoading; + const hasName = currentUser.name !== null; + + const handleCreateProject = useCallback( + async (values: FormValues) => { + const actions: [ + Promise, + Promise + ] = [ + createProject({ + name: values.projectName, + organizationId: currentUser.organizationIds[0], + }), + null, + ]; + + if (!hasName) { + actions.push( + updateCurrentUser({ + name: values.name, + }) + ); + } + + await Promise.all(actions.filter(Boolean)); + return navigate("/projects"); + }, + [updateCurrentUser, createProject, currentUser, hasName, navigate] + ); + + useEffect(() => { + if (projectsData?.projects && projectsData?.projects.length > 0) { + navigate("/projects", { replace: true }); + } + }, [projectsData, navigate]); + + if (isProjectsLoading) { + return ; + } + + return ( + + + + Let's create your first project{" "} + + 🎉 + + + } + > +
+ + {!hasName && ( + <> + + What's your name? + + + + + + )} + + + + + How do you wanna call your first project? + + + + + + + + + {data.projects?.map((project, index) => ( + + + + ))} + {!isOdd(data.projects.length) && ( + + setIsCreateNewProjectModalOpen(true)} + > + + + */} - - - - setActiveView(selectedView)} - > + + setIsDeleteConfirmationModalOpen(false)} + onConfirm={() => setIsDeleteConfirmationModalOpen(false)} + /> + + setActiveView(selectedView)} + > - {activeView === "history" && } - {activeView === "edit" && } - - ) + {prompt && ( + <> + {activeView === "history" && } + {activeView === "edit" && } + {activeView === "dashboard" && } + + )} + ); }; diff --git a/apps/console/src/app/pages/prompts/index.tsx b/apps/console/src/app/pages/prompts/index.tsx index bf261cf3..a5b43ac8 100644 --- a/apps/console/src/app/pages/prompts/index.tsx +++ b/apps/console/src/app/pages/prompts/index.tsx @@ -6,57 +6,66 @@ import { PlusOutlined } from "@ant-design/icons"; import { CreatePromptModal } from "../../components/prompts/CreatePromptModal"; import { useState } from "react"; import { css } from "@emotion/css"; -import { Button, Typography } from "antd"; +import { Button, Space, Spin, Typography, theme } from "antd"; import { useNavigate } from "react-router-dom"; +import { useCurrentProject } from "../../lib/providers/CurrentProjectContext"; export const PromptsPage = () => { + const { project, isLoading: isProjectsLoading } = useCurrentProject(); + const { token } = theme.useToken(); const navigate = useNavigate(); const [isCreatePromptModalOpen, setIsCreatePromptModalOpen] = useState(false); - const { data, isLoading } = useQuery({ + const { data, isLoading: isLoadingPrompts } = useQuery({ queryKey: ["prompts"], - queryFn: () => gqlClient.request(GET_ALL_PROMPTS), + queryFn: () => + gqlClient.request(GET_ALL_PROMPTS, { data: { projectId: project?.id } }), + enabled: !!project?.id, }); + const isLoading = isLoadingPrompts || isProjectsLoading; return ( <> setIsCreatePromptModalOpen(false)} - onCreated={(id) => navigate(`/prompts/${id}`)} + onCreated={(id) => navigate(`/projects/${project.id}/prompts/${id}`)} /> - Prompts - {isLoading &&

Loading...

} + + Prompts - {data && ( -
+
+ + {data?.prompts?.map((prompt) => ( +
+ + navigate(`/projects/${project.id}/prompts/${prompt.id}`) + } + /> +
+ ))}
- {data.prompts.map((prompt) => ( -
- navigate(`/prompts/${prompt.id}`)} - /> -
- ))} -
- )} + +
); }; diff --git a/apps/server/.env b/apps/server/.env index 9bebd438..006b2b69 100644 --- a/apps/server/.env +++ b/apps/server/.env @@ -1,2 +1,4 @@ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/pezzo -SUPERTOKENS_CONNECTION_URI="http://localhost:3567" \ No newline at end of file +SUPERTOKENS_CONNECTION_URI="http://localhost:3567" +INFLUXDB_URL="http://localhost:8086" +INFLUXDB_TOKEN="token123" \ No newline at end of file diff --git a/apps/server/prisma/migrations/20230519223302_add_projects/migration.sql b/apps/server/prisma/migrations/20230519223302_add_projects/migration.sql new file mode 100644 index 00000000..79032751 --- /dev/null +++ b/apps/server/prisma/migrations/20230519223302_add_projects/migration.sql @@ -0,0 +1,72 @@ +/* + Warnings: + + - You are about to drop the column `organizationId` on the `ApiKey` table. All the data in the column will be lost. + - You are about to drop the column `organizationId` on the `Environment` table. All the data in the column will be lost. + - You are about to drop the column `organizationId` on the `Prompt` table. All the data in the column will be lost. + - You are about to drop the column `organizationId` on the `ProviderApiKey` table. All the data in the column will be lost. + - Added the required column `projectId` to the `ApiKey` table without a default value. This is not possible if the table is not empty. + - Added the required column `projectId` to the `Environment` table without a default value. This is not possible if the table is not empty. + - Added the required column `projectId` to the `Prompt` table without a default value. This is not possible if the table is not empty. + - Added the required column `projectId` to the `ProviderApiKey` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "ApiKey" DROP CONSTRAINT "ApiKey_organizationId_fkey"; + +-- DropForeignKey +ALTER TABLE "ProviderApiKey" DROP CONSTRAINT "ProviderApiKey_organizationId_fkey"; + +-- AlterTable +ALTER TABLE "ApiKey" DROP COLUMN "organizationId", +ADD COLUMN "projectId" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "Environment" DROP COLUMN "organizationId", +ADD COLUMN "projectId" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "Prompt" DROP COLUMN "organizationId", +ADD COLUMN "projectId" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "ProviderApiKey" DROP COLUMN "organizationId", +ADD COLUMN "projectId" TEXT NOT NULL; + +-- CreateTable +CREATE TABLE "Project" ( + "id" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "name" TEXT NOT NULL, + "organizationId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Project_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ProjectMember" ( + "id" TEXT NOT NULL, + "projectId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "ProjectMember_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "Project" ADD CONSTRAINT "Project_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProjectMember" ADD CONSTRAINT "ProjectMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProjectMember" ADD CONSTRAINT "ProjectMember_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ApiKey" ADD CONSTRAINT "ApiKey_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProviderApiKey" ADD CONSTRAINT "ProviderApiKey_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/server/prisma/migrations/migration_lock.toml b/apps/server/prisma/migrations/migration_lock.toml deleted file mode 100644 index fbffa92c..00000000 --- a/apps/server/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "postgresql" \ No newline at end of file diff --git a/apps/server/prisma/schema.prisma b/apps/server/prisma/schema.prisma index 53c2ff18..9d224ca6 100644 --- a/apps/server/prisma/schema.prisma +++ b/apps/server/prisma/schema.prisma @@ -24,16 +24,39 @@ model User { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt orgMemberships OrganizationMember[] + ProjectMember ProjectMember[] } -model Organization { - id String @id @default(cuid()) +model Project { + id String @id @default(cuid()) + slug String name String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - members OrganizationMember[] + organization Organization @relation(fields: [organizationId], references: [id]) + organizationId String apiKeys ApiKey[] providerApiKeys ProviderApiKey[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + members ProjectMember[] +} + +model ProjectMember { + id String @id @default(cuid()) + projectId String + userId String + project Project @relation(fields: [projectId], references: [id]) + user User @relation(fields: [userId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Organization { + id String @id @default(cuid()) + name String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + members OrganizationMember[] + projects Project[] } model OrganizationMember { @@ -55,7 +78,7 @@ model Environment { id String @id @default(cuid()) slug String name String - organizationId String + projectId String promptEnvironments PromptEnvironment[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -64,7 +87,7 @@ model Environment { model Prompt { id String @id @default(cuid()) integrationId String - organizationId String + projectId String name String executions PromptExecution[] promptEnvironments PromptEnvironment[] @@ -123,19 +146,19 @@ enum PromptExecutionStatus { } model ApiKey { - id String @id - name String @default("Default") - organizationId String - Organization Organization @relation(fields: [organizationId], references: [id]) - createdAt DateTime @default(now()) + id String @id + name String @default("Default") + projectId String + project Project @relation(fields: [projectId], references: [id]) + createdAt DateTime @default(now()) } model ProviderApiKey { - id String @id @default(cuid()) - provider String - value String - organizationId String - organization Organization @relation(fields: [organizationId], references: [id]) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + provider String + value String + projectId String + project Project @relation(fields: [projectId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } diff --git a/apps/server/src/app/app.module.ts b/apps/server/src/app/app.module.ts index b5c8e824..cac6a219 100644 --- a/apps/server/src/app/app.module.ts +++ b/apps/server/src/app/app.module.ts @@ -2,7 +2,7 @@ import { join } from "path"; import { Module } from "@nestjs/common"; import { GraphQLModule } from "@nestjs/graphql"; import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo"; -import { ConfigModule } from "@nestjs/config"; +import { ConfigModule, ConfigService } from "@nestjs/config"; import Joi from "joi"; import { ApolloServerPluginLandingPageLocalDefault } from "@apollo/server/plugin/landingPage/default"; @@ -14,6 +14,9 @@ import { PromptEnvironmentsModule } from "./prompt-environments/prompt-environme import { CredentialsModule } from "./credentials/credentials.module"; import { AuthModule } from "./auth/auth.module"; import { IdentityModule } from "./identity/identity.module"; +import { InfluxDbModule } from "./influxdb/influxdb.module"; +import { InfluxModuleOptions } from "./influxdb/types"; +import { MetricsModule } from "./metrics/metrics.module"; const GQL_SCHEMA_PATH = join(process.cwd(), "apps/server/src/schema.graphql"); @@ -33,6 +36,8 @@ const GQL_SCHEMA_PATH = join(process.cwd(), "apps/server/src/schema.graphql"); ), GOOGLE_OAUTH_CLIENT_ID: Joi.string().optional().default(null), GOOGLE_OAUTH_CLIENT_SECRET: Joi.string().optional().default(null), + INFLUXDB_URL: Joi.string().required(), + INFLUXDB_TOKEN: Joi.string().required(), }), // In CI, we need to skip validation because we don't have a .env file // This is consumed by the graphql:schema-generate Nx target @@ -52,15 +57,28 @@ const GQL_SCHEMA_PATH = join(process.cwd(), "apps/server/src/schema.graphql"); PromptEnvironmentsModule, CredentialsModule, IdentityModule, + MetricsModule, ], formatError, }), + InfluxDbModule.forRootAsync({ + inject: [ConfigService], + useFactory: async ( + config: ConfigService + ): Promise => { + return { + url: config.get("INFLUXDB_URL"), + token: config.get("INFLUXDB_TOKEN"), + }; + }, + }), AuthModule.forRoot(), PromptsModule, EnvironmentsModule, PromptEnvironmentsModule, CredentialsModule, IdentityModule, + MetricsModule, ], controllers: [HealthController], }) diff --git a/apps/server/src/app/auth/auth.guard.ts b/apps/server/src/app/auth/auth.guard.ts index 0d965b64..a78c1a9a 100644 --- a/apps/server/src/app/auth/auth.guard.ts +++ b/apps/server/src/app/auth/auth.guard.ts @@ -1,19 +1,17 @@ import { CanActivate, ExecutionContext, - ForbiddenException, Injectable, InternalServerErrorException, UnauthorizedException, } from "@nestjs/common"; -import { verifySession } from "supertokens-node/recipe/session/framework/express"; import { GqlExecutionContext } from "@nestjs/graphql"; import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword"; import { UsersService } from "../identity/users.service"; import { RequestUser } from "../identity/users.types"; import { APIKeysService } from "../identity/api-keys.service"; -import { User } from "supertokens-node/recipe/thirdpartyemailpassword"; import Session, { SessionContainer } from "supertokens-node/recipe/session"; +import { ProjectsService } from "../identity/projects.service"; export enum AuthMethod { ApiKey = "ApiKey", @@ -24,7 +22,8 @@ export enum AuthMethod { export class AuthGuard implements CanActivate { constructor( private readonly usersService: UsersService, - private readonly apiKeysService: APIKeysService + private readonly apiKeysService: APIKeysService, + private readonly projectsService: ProjectsService ) {} async canActivate(context: ExecutionContext): Promise { @@ -49,7 +48,7 @@ export class AuthGuard implements CanActivate { throw new UnauthorizedException(); } - req.organizationId = apiKey.organizationId; + req.projectId = apiKey.projectId; req.authMethod = AuthMethod.ApiKey; return true; @@ -66,6 +65,7 @@ export class AuthGuard implements CanActivate { try { session = await Session.getSession(req, res, { sessionRequired: false, + antiCsrfCheck: process.env.NODE_ENV === "development" ? false : true, }); } catch (error) { throw new UnauthorizedException(); @@ -78,17 +78,21 @@ export class AuthGuard implements CanActivate { const supertokensUser = await ThirdPartyEmailPassword.getUserById( session.getUserId() ); + req["supertokensUser"] = supertokensUser; try { - const user = await this.usersService.getOrCreateUser(supertokensUser); + const projects = await this.projectsService.getProjectsByUser( + supertokensUser.id + ); const memberships = await this.usersService.getUserOrgMemberships( - user.id + supertokensUser.id ); const reqUser: RequestUser = { - id: user.id, - email: user.email, + id: supertokensUser.id, + email: supertokensUser.email, + projects: projects.map((p) => ({ id: p.id })), orgMemberships: memberships.map((m) => ({ organizationId: m.organizationId, memberSince: m.createdAt, diff --git a/apps/server/src/app/auth/supertokens.service.ts b/apps/server/src/app/auth/supertokens.service.ts index a621cbdd..f594bc67 100644 --- a/apps/server/src/app/auth/supertokens.service.ts +++ b/apps/server/src/app/auth/supertokens.service.ts @@ -1,16 +1,24 @@ import { Injectable } from "@nestjs/common"; import supertokens from "supertokens-node"; import Session from "supertokens-node/recipe/session"; +import UserMetadata from "supertokens-node/recipe/usermetadata"; import ThirdPartyEmailPassword, { TypeProvider, } from "supertokens-node/recipe/thirdpartyemailpassword"; import Dashboard from "supertokens-node/recipe/dashboard"; import ThirdParty from "supertokens-node/recipe/thirdparty"; import { ConfigService } from "@nestjs/config"; +import { google } from "googleapis"; +import { UsersService } from "../identity/users.service"; +import { UserCreateRequest } from "../identity/users.types"; +console.log(process.env.SUPERTOKENS_API_KEY); @Injectable() export class SupertokensService { - constructor(private readonly config: ConfigService) { + constructor( + private readonly config: ConfigService, + private readonly usersService: UsersService + ) { supertokens.init({ appInfo: { appName: "Pezzo", @@ -26,8 +34,72 @@ export class SupertokensService { recipeList: [ Dashboard.init(), Session.init(), + UserMetadata.init(), ThirdPartyEmailPassword.init({ providers: this.getActiveProviders(), + override: { + apis: (originalImplementation) => { + return { + ...originalImplementation, + emailPasswordSignUpPOST: async (input) => { + const res = + await originalImplementation.emailPasswordSignUpPOST(input); + if (res?.status === "OK") { + const userCreateRequest: UserCreateRequest = { + email: res.user.email, + id: res.user.id, + }; + + this.usersService.createUser(userCreateRequest); + } + return res; + }, + thirdPartySignInUpPOST: async (input) => { + const res = + await originalImplementation.thirdPartySignInUpPOST(input); + + if (res.status === "OK") { + const { access_token } = res.authCodeResponse; + const client = new google.auth.OAuth2( + config.get("GOOGLE_OAUTH_CLIENT_ID"), + config.get("GOOGLE_OAUTH_CLIENT_SECRET") + ); + + client.setCredentials({ access_token }); + + // get user info from google since supertokens doesn't return it + const { data } = await google.oauth2("v2").userinfo.get({ + auth: client, + fields: "email,given_name,family_name,picture", + }); + + const userCreateRequest: UserCreateRequest = { + email: data.email, + id: res.user.id, + }; + + const metadataFields = { + name: `${data.given_name} ${data.family_name}`, + photoUrl: data.picture, + }; + + const user = await this.usersService.getUser(data.email); + + if (!user) { + await this.usersService.createUser(userCreateRequest); + } + + await UserMetadata.updateUserMetadata(res.user.id, { + profile: metadataFields, + }).catch((err) => { + console.log("Failed to update user metadata fields", err); + }); + } + return res; + }, + }; + }, + }, }), ], }); @@ -58,6 +130,7 @@ export class SupertokensService { ThirdParty.Google({ clientId: this.config.get("GOOGLE_OAUTH_CLIENT_ID"), clientSecret: this.config.get("GOOGLE_OAUTH_CLIENT_SECRET"), + scope: ["email", "profile"], }) ); } diff --git a/apps/server/src/app/credentials/inputs/create-provider-api-key.input.ts b/apps/server/src/app/credentials/inputs/create-provider-api-key.input.ts index 40942cfb..099dc68f 100644 --- a/apps/server/src/app/credentials/inputs/create-provider-api-key.input.ts +++ b/apps/server/src/app/credentials/inputs/create-provider-api-key.input.ts @@ -7,4 +7,7 @@ export class CreateProviderApiKeyInput { @Field(() => String, { nullable: false }) value: string; + + @Field(() => String, { nullable: false }) + projectId: string; } diff --git a/apps/server/src/app/credentials/inputs/get-provider-api-keys.input.ts b/apps/server/src/app/credentials/inputs/get-provider-api-keys.input.ts new file mode 100644 index 00000000..420e0a69 --- /dev/null +++ b/apps/server/src/app/credentials/inputs/get-provider-api-keys.input.ts @@ -0,0 +1,7 @@ +import { Field, InputType } from "@nestjs/graphql"; + +@InputType() +export class GetProviderApiKeysInput { + @Field(() => String, { nullable: false }) + projectId: string; +} diff --git a/apps/server/src/app/credentials/provider-api-keys.resolver.ts b/apps/server/src/app/credentials/provider-api-keys.resolver.ts index 13e3cdc0..7f8b3e24 100644 --- a/apps/server/src/app/credentials/provider-api-keys.resolver.ts +++ b/apps/server/src/app/credentials/provider-api-keys.resolver.ts @@ -2,11 +2,12 @@ import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; import { ProviderApiKey } from "../../@generated/provider-api-key/provider-api-key.model"; import { CreateProviderApiKeyInput } from "./inputs/create-provider-api-key.input"; import { ProviderApiKeysService } from "./provider-api-keys.service"; -import { ProviderApiKeyWhereUniqueInput } from "../../@generated/provider-api-key/provider-api-key-where-unique.input"; import { CurrentUser } from "../identity/current-user.decorator"; import { RequestUser } from "../identity/users.types"; import { UseGuards } from "@nestjs/common"; import { AuthGuard } from "../auth/auth.guard"; +import { GetProviderApiKeysInput } from "./inputs/get-provider-api-keys.input"; +import { isProjectMemberOrThrow } from "../identity/identity.utils"; @UseGuards(AuthGuard) @Resolver(() => ProviderApiKey) @@ -14,9 +15,13 @@ export class ProviderApiKeysResolver { constructor(private providerAPIKeysService: ProviderApiKeysService) {} @Query(() => [ProviderApiKey]) - async providerApiKeys(@CurrentUser() user: RequestUser) { + async providerApiKeys( + @Args("data") data: GetProviderApiKeysInput, + @CurrentUser() user: RequestUser + ) { + isProjectMemberOrThrow(user, data.projectId); const keys = await this.providerAPIKeysService.getAllProviderApiKeys( - user.orgMemberships[0].organizationId + data.projectId ); return keys.map((key) => ({ ...key, value: this.censorApiKey(key.value) })); } @@ -29,7 +34,7 @@ export class ProviderApiKeysResolver { const key = await this.providerAPIKeysService.upsertProviderApiKey( data.provider, data.value, - user.orgMemberships[0].organizationId + data.projectId ); return { diff --git a/apps/server/src/app/credentials/provider-api-keys.service.ts b/apps/server/src/app/credentials/provider-api-keys.service.ts index 0dc6be99..00eb9a46 100644 --- a/apps/server/src/app/credentials/provider-api-keys.service.ts +++ b/apps/server/src/app/credentials/provider-api-keys.service.ts @@ -5,16 +5,16 @@ import { PrismaService } from "../prisma.service"; export class ProviderApiKeysService { constructor(private readonly prisma: PrismaService) {} - async getByProvider(provider: string, organizationId: string) { + async getByProvider(provider: string, projectId: string) { const keys = await this.prisma.providerApiKey.findFirst({ - where: { provider, organizationId }, + where: { provider, projectId }, }); return keys; } - async getAllProviderApiKeys(organizationId: string) { + async getAllProviderApiKeys(projectId: string) { const keys = await this.prisma.providerApiKey.findMany({ - where: { organizationId }, + where: { projectId }, }); return keys; } @@ -22,13 +22,13 @@ export class ProviderApiKeysService { async createProviderApiKey( provider: string, value: string, - organizationId: string + projectId: string ) { const key = await this.prisma.providerApiKey.create({ data: { provider, value, - organizationId, + projectId, }, }); @@ -38,7 +38,7 @@ export class ProviderApiKeysService { async upsertProviderApiKey( provider: string, value: string, - organizationId: string + projectId: string ) { const exists = await this.prisma.providerApiKey.findFirst({ where: { provider }, @@ -61,7 +61,7 @@ export class ProviderApiKeysService { data: { provider, value, - organizationId, + projectId, }, }); diff --git a/apps/server/src/app/environments/environments.resolver.ts b/apps/server/src/app/environments/environments.resolver.ts index 993ca272..3b5418ac 100644 --- a/apps/server/src/app/environments/environments.resolver.ts +++ b/apps/server/src/app/environments/environments.resolver.ts @@ -12,6 +12,8 @@ import { RequestUser } from "../identity/users.types"; import { AuthGuard } from "../auth/auth.guard"; import { GetEnvironmentBySlugInput } from "./inputs/get-environment-by-slug.input"; import { EnvironmentsService } from "./environments.service"; +import { GetEnvironmentsInput } from "./inputs/get-environments.input"; +import { isProjectMemberOrThrow } from "../identity/identity.utils"; @UseGuards(AuthGuard) @Resolver(() => Environment) @@ -22,10 +24,13 @@ export class EnvironmentsResolver { ) {} @Query(() => [Environment]) - async environments(@CurrentUser() user: RequestUser) { + async environments( + @Args("data") data: GetEnvironmentsInput, + @CurrentUser() user: RequestUser + ) { const environments = await this.prisma.environment.findMany({ where: { - organizationId: user.orgMemberships[0].organizationId, + projectId: data.projectId, }, }); return environments; @@ -36,9 +41,11 @@ export class EnvironmentsResolver { @Args("data") data: GetEnvironmentBySlugInput, @CurrentUser() user: RequestUser ) { + isProjectMemberOrThrow(user, data.projectId); + const environment = await this.environmentsService.getBySlug( data.slug, - user.orgMemberships[0].organizationId + data.projectId ); if (!environment) { @@ -57,7 +64,7 @@ export class EnvironmentsResolver { ) { const exists = await this.environmentsService.getBySlug( data.slug, - user.orgMemberships[0].organizationId + data.projectId ); if (exists) { @@ -70,7 +77,7 @@ export class EnvironmentsResolver { data: { name: data.name, slug: data.slug, - organizationId: user.orgMemberships[0].organizationId, + projectId: data.projectId, }, }); diff --git a/apps/server/src/app/environments/environments.service.ts b/apps/server/src/app/environments/environments.service.ts index a714219e..e057a76c 100644 --- a/apps/server/src/app/environments/environments.service.ts +++ b/apps/server/src/app/environments/environments.service.ts @@ -5,11 +5,11 @@ import { PrismaService } from "../prisma.service"; export class EnvironmentsService { constructor(private readonly prisma: PrismaService) {} - async getBySlug(slug: string, organizationId: string) { + async getBySlug(slug: string, projectId: string) { const environment = await this.prisma.environment.findFirst({ where: { slug, - organizationId, + projectId, }, }); diff --git a/apps/server/src/app/environments/inputs/create-environment.input.ts b/apps/server/src/app/environments/inputs/create-environment.input.ts index a1346f26..fb4ce5c0 100644 --- a/apps/server/src/app/environments/inputs/create-environment.input.ts +++ b/apps/server/src/app/environments/inputs/create-environment.input.ts @@ -7,4 +7,7 @@ export class CreateEnvironmentInput { @Field(() => String, { nullable: false }) slug: string; + + @Field(() => String, { nullable: false }) + projectId: string; } diff --git a/apps/server/src/app/environments/inputs/get-environment-by-slug.input.ts b/apps/server/src/app/environments/inputs/get-environment-by-slug.input.ts index bef58018..87617317 100644 --- a/apps/server/src/app/environments/inputs/get-environment-by-slug.input.ts +++ b/apps/server/src/app/environments/inputs/get-environment-by-slug.input.ts @@ -4,4 +4,7 @@ import { Field, InputType } from "@nestjs/graphql"; export class GetEnvironmentBySlugInput { @Field(() => String, { nullable: false }) slug: string; + + @Field(() => String, { nullable: false }) + projectId: string; } diff --git a/apps/server/src/app/environments/inputs/get-environments.input.ts b/apps/server/src/app/environments/inputs/get-environments.input.ts new file mode 100644 index 00000000..2ef41fda --- /dev/null +++ b/apps/server/src/app/environments/inputs/get-environments.input.ts @@ -0,0 +1,7 @@ +import { Field, InputType } from "@nestjs/graphql"; + +@InputType() +export class GetEnvironmentsInput { + @Field(() => String, { nullable: false }) + projectId: string; +} diff --git a/apps/server/src/app/identity/api-key-org-id.ts b/apps/server/src/app/identity/api-key-project-id.decorator.ts similarity index 77% rename from apps/server/src/app/identity/api-key-org-id.ts rename to apps/server/src/app/identity/api-key-project-id.decorator.ts index 1a0876d9..1fb699df 100644 --- a/apps/server/src/app/identity/api-key-org-id.ts +++ b/apps/server/src/app/identity/api-key-project-id.decorator.ts @@ -1,10 +1,10 @@ import { createParamDecorator, ExecutionContext } from "@nestjs/common"; import { GqlExecutionContext } from "@nestjs/graphql"; -export const ApiKeyOrgId = createParamDecorator( +export const ApiKeyProjectId = createParamDecorator( (_: unknown, context: ExecutionContext): string => { const gqlCtx = GqlExecutionContext.create(context); const ctx = gqlCtx.getContext(); - return ctx.req.organizationId; + return ctx.req.projectId; } ); diff --git a/apps/server/src/app/identity/api-keys.resolver.ts b/apps/server/src/app/identity/api-keys.resolver.ts index 3034cd6f..38b83cc2 100644 --- a/apps/server/src/app/identity/api-keys.resolver.ts +++ b/apps/server/src/app/identity/api-keys.resolver.ts @@ -1,10 +1,13 @@ -import { Query, Resolver } from "@nestjs/graphql"; +import { Args, Query, Resolver } from "@nestjs/graphql"; import { ApiKey } from "../../@generated/api-key/api-key.model"; import { APIKeysService } from "./api-keys.service"; import { UseGuards } from "@nestjs/common"; import { AuthGuard } from "../auth/auth.guard"; import { CurrentUser } from "./current-user.decorator"; import { RequestUser } from "./users.types"; +import { GetApiKeyInput } from "./inputs/get-api-key.input"; +import { isProjectMemberOrThrow } from "./identity.utils"; +import { OrganizationMember } from "../../@generated/organization-member/organization-member.model"; @UseGuards(AuthGuard) @Resolver(() => ApiKey) @@ -12,8 +15,11 @@ export class ApiKeysResolver { constructor(private apiKeysService: APIKeysService) {} @Query(() => ApiKey) - currentApiKey(@CurrentUser() user: RequestUser) { - const organizationid = user.orgMemberships[0].organizationId; - return this.apiKeysService.getApiKeyByOrganizationId(organizationid); + currentApiKey( + @Args("data") data: GetApiKeyInput, + @CurrentUser() user: RequestUser + ) { + isProjectMemberOrThrow(user, data.projectId); + return this.apiKeysService.getApiKeyByProjectId(data.projectId); } } diff --git a/apps/server/src/app/identity/api-keys.service.ts b/apps/server/src/app/identity/api-keys.service.ts index 10daf1be..9fc76df8 100644 --- a/apps/server/src/app/identity/api-keys.service.ts +++ b/apps/server/src/app/identity/api-keys.service.ts @@ -5,20 +5,20 @@ import { PrismaService } from "../prisma.service"; export class APIKeysService { constructor(private readonly prisma: PrismaService) {} - async getApiKeyByOrganizationId(organizationId: string) { + async getApiKeyByProjectId(projectId: string) { const apiKey = await this.prisma.apiKey.findFirst({ where: { - organizationId: organizationId, + projectId, }, }); return apiKey; } - async getApiKey(id: string) { - const apiKey = await this.prisma.apiKey.findUnique({ + async getApiKey(value: string) { + const apiKey = await this.prisma.apiKey.findFirst({ where: { - id: id, + id: value, }, }); diff --git a/apps/server/src/app/identity/identity.module.ts b/apps/server/src/app/identity/identity.module.ts index a4ebbfaf..a3090ac3 100644 --- a/apps/server/src/app/identity/identity.module.ts +++ b/apps/server/src/app/identity/identity.module.ts @@ -4,15 +4,26 @@ import { PrismaService } from "../prisma.service"; import { OrganizationsService } from "./organizations.service"; import { APIKeysService } from "./api-keys.service"; import { ApiKeysResolver } from "./api-keys.resolver"; +import { ProjectsResolver } from "./projects.resolver"; +import { ProjectsService } from "./projects.service"; +import { UsersResolver } from "./users.resolver"; @Module({ providers: [ + OrganizationsService, + ApiKeysResolver, + ProjectsResolver, + UsersResolver, PrismaService, + UsersService, + APIKeysService, + ProjectsService, + ], + exports: [ UsersService, OrganizationsService, + ProjectsService, APIKeysService, - ApiKeysResolver, ], - exports: [UsersService, OrganizationsService, APIKeysService], }) export class IdentityModule {} diff --git a/apps/server/src/app/identity/identity.utils.ts b/apps/server/src/app/identity/identity.utils.ts index c2b2b657..980dbe9d 100644 --- a/apps/server/src/app/identity/identity.utils.ts +++ b/apps/server/src/app/identity/identity.utils.ts @@ -6,6 +6,12 @@ export function isOrgMember(user: RequestUser, organizationId: string) { return !!user.orgMemberships.find((m) => m.organizationId === organizationId); } +export function isProjectMemberOrThrow(user: RequestUser, projectId: string) { + if (!user.projects.find((p) => p.id === projectId)) { + throw new ForbiddenException(); + } +} + export function isOrgMemberOrThrow(user: RequestUser, organizationId: string) { if (!user.orgMemberships.find((m) => m.organizationId === organizationId)) { throw new ForbiddenException(); @@ -16,14 +22,15 @@ export function isOrgAdmin(user: RequestUser, organizationId: string) { const membership = user.orgMemberships.find( (m) => m.organizationId === organizationId ); - return membership.role === OrgRole.Admin; + return !!membership && membership.role === OrgRole.Admin; } export function isOrgAdminOrThrow(user: RequestUser, organizationId: string) { const membership = user.orgMemberships.find( (m) => m.organizationId === organizationId ); - if (membership.role !== OrgRole.Admin) { + + if (!membership || membership.role !== OrgRole.Admin) { throw new ForbiddenException(); } } diff --git a/apps/server/src/app/identity/inputs/create-project.input.ts b/apps/server/src/app/identity/inputs/create-project.input.ts new file mode 100644 index 00000000..481601f4 --- /dev/null +++ b/apps/server/src/app/identity/inputs/create-project.input.ts @@ -0,0 +1,10 @@ +import { Field, InputType } from "@nestjs/graphql"; + +@InputType() +export class CreateProjectInput { + @Field(() => String, { nullable: false }) + name: string; + + @Field(() => String, { nullable: false }) + organizationId: string; +} diff --git a/apps/server/src/app/identity/inputs/get-api-key.input.ts b/apps/server/src/app/identity/inputs/get-api-key.input.ts new file mode 100644 index 00000000..b5ae88e3 --- /dev/null +++ b/apps/server/src/app/identity/inputs/get-api-key.input.ts @@ -0,0 +1,7 @@ +import { Field, InputType } from "@nestjs/graphql"; + +@InputType() +export class GetApiKeyInput { + @Field(() => String, { nullable: false }) + projectId: string; +} diff --git a/apps/server/src/app/identity/inputs/update-profile.input.ts b/apps/server/src/app/identity/inputs/update-profile.input.ts new file mode 100644 index 00000000..322f400c --- /dev/null +++ b/apps/server/src/app/identity/inputs/update-profile.input.ts @@ -0,0 +1,7 @@ +import { Field, InputType } from "@nestjs/graphql"; + +@InputType() +export class UpdateProfileInput { + @Field(() => String, { nullable: false }) + name: string; +} diff --git a/apps/server/src/app/identity/projects.resolver.ts b/apps/server/src/app/identity/projects.resolver.ts new file mode 100644 index 00000000..e9ad6abf --- /dev/null +++ b/apps/server/src/app/identity/projects.resolver.ts @@ -0,0 +1,66 @@ +import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; +import { Project } from "../../@generated/project/project.model"; +import { CreateProjectInput } from "./inputs/create-project.input"; +import { ProjectsService } from "./projects.service"; +import { AuthGuard } from "../auth/auth.guard"; +import { + ConflictException, + NotFoundException, + UseGuards, +} from "@nestjs/common"; +import { isOrgAdminOrThrow, isOrgMemberOrThrow } from "./identity.utils"; +import { CurrentUser } from "./current-user.decorator"; +import { RequestUser } from "./users.types"; +import { slugify } from "@pezzo/common"; +import { ProjectWhereUniqueInput } from "../../@generated/project/project-where-unique.input"; + +@UseGuards(AuthGuard) +@Resolver(() => Project) +export class ProjectsResolver { + constructor(private projectsService: ProjectsService) {} + + @Query(() => Project) + async project( + @Args("data") data: ProjectWhereUniqueInput, + @CurrentUser() user: RequestUser + ) { + const project = await this.projectsService.getProjectById(data.id); + + if (!project) { + throw new NotFoundException(); + } + + isOrgMemberOrThrow(user, project.organizationId); + return project; + } + + @Query(() => [Project]) + async projects(@CurrentUser() user: RequestUser) { + const orgId = user.orgMemberships[0].organizationId; + return this.projectsService.getProjectsByOrgId(orgId); + } + + @Mutation(() => Project) + async createProject( + @Args("data") data: CreateProjectInput, + @CurrentUser() user: RequestUser + ) { + isOrgAdminOrThrow(user, data.organizationId); + const slug = slugify(data.name); + const exists = await this.projectsService.getProjectBySlug( + slug, + data.organizationId + ); + + if (exists) { + throw new ConflictException(`Project with slug "${slug}" already exists`); + } + + return this.projectsService.createProject( + data.name, + slug, + data.organizationId, + user.id + ); + } +} diff --git a/apps/server/src/app/identity/projects.service.ts b/apps/server/src/app/identity/projects.service.ts new file mode 100644 index 00000000..988ccab8 --- /dev/null +++ b/apps/server/src/app/identity/projects.service.ts @@ -0,0 +1,75 @@ +import { Injectable } from "@nestjs/common"; +import { PrismaService } from "../prisma.service"; +import { randomBytes } from "crypto"; + +@Injectable() +export class ProjectsService { + constructor(private prisma: PrismaService) {} + + async createProject( + name: string, + slug: string, + organizationId: string, + creatorUserId: string + ) { + const project = await this.prisma.project.create({ + data: { + name, + slug, + organizationId, + members: { + create: { + userId: creatorUserId, + }, + }, + }, + }); + + const projectApiKeyValue = `pez_${randomBytes(32).toString("hex")}`; + await this.prisma.apiKey.create({ + data: { + id: projectApiKeyValue, + projectId: project.id, + }, + }); + + return project; + } + + async getProjectById(id: string) { + return this.prisma.project.findUnique({ + where: { + id, + }, + }); + } + + async getProjectsByOrgId(organizationId: string) { + return this.prisma.project.findMany({ + where: { + organizationId, + }, + }); + } + + async getProjectBySlug(slug: string, organizationId: string) { + return this.prisma.project.findFirst({ + where: { + slug, + organizationId, + }, + }); + } + + async getProjectsByUser(userId: string) { + return this.prisma.project.findMany({ + where: { + members: { + some: { + userId, + }, + }, + }, + }); + } +} diff --git a/apps/server/src/app/identity/users.resolver.ts b/apps/server/src/app/identity/users.resolver.ts new file mode 100644 index 00000000..65b9527f --- /dev/null +++ b/apps/server/src/app/identity/users.resolver.ts @@ -0,0 +1,84 @@ +import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; +import UserMetadata from "supertokens-node/recipe/usermetadata"; +import { AuthGuard } from "../auth/auth.guard"; +import { NotFoundException, UseGuards } from "@nestjs/common"; +import { CurrentUser } from "./current-user.decorator"; +import { RequestUser, ExtendedUser } from "./users.types"; + +import { UsersService } from "./users.service"; +import { UpdateProfileInput } from "./inputs/update-profile.input"; + +type SupertokensMetadata = { + metadata: + | { profile: { name: string | null; photoUrl: string | null } } + | undefined; +}; + +@UseGuards(AuthGuard) +@Resolver(() => ExtendedUser) +export class UsersResolver { + constructor(private usersService: UsersService) {} + + @Query(() => ExtendedUser) + async me(@CurrentUser() userInfo: RequestUser) { + const user = await this.usersService.getUser(userInfo.email); + + if (!user) { + throw new NotFoundException(); + } + + const organizationIds = userInfo.orgMemberships.map( + (m) => m.organizationId + ); + const { metadata } = (await UserMetadata.getUserMetadata( + user.id + )) as SupertokensMetadata; + + if (metadata) { + return { + ...user, + ...metadata.profile, + organizationIds, + }; + } + + return { + ...user, + organizationIds, + }; + } + + @Mutation(() => ExtendedUser) + async updateProfile( + @CurrentUser() userInfo: RequestUser, + @Args("data") { name }: UpdateProfileInput + ) { + const user = await this.usersService.getUser(userInfo.email); + + if (!user) { + throw new NotFoundException(); + } + + const { metadata } = (await UserMetadata.getUserMetadata( + user.id + )) as SupertokensMetadata; + + const profileMetadata = metadata?.profile ?? { + name, + photoUrl: null, + }; + + await UserMetadata.updateUserMetadata(user.id, { + ...metadata, + profile: { + ...profileMetadata, + name, + }, + }); + + return { + ...user, + name, + }; + } +} diff --git a/apps/server/src/app/identity/users.service.ts b/apps/server/src/app/identity/users.service.ts index d369d3a2..a63be224 100644 --- a/apps/server/src/app/identity/users.service.ts +++ b/apps/server/src/app/identity/users.service.ts @@ -1,19 +1,18 @@ import { Injectable } from "@nestjs/common"; import { PrismaService } from "../prisma.service"; import { User } from "@prisma/client"; -import { User as SupertokensUser } from "supertokens-node/recipe/thirdpartyemailpassword"; -import { randomBytes } from "crypto"; +import { UserCreateRequest } from "./users.types"; @Injectable() export class UsersService { constructor(private readonly prisma: PrismaService) {} - async createUser(userInfo: SupertokensUser): Promise { - const [user, org] = await this.prisma.$transaction([ + async createUser(userCreateRequest: UserCreateRequest): Promise { + const [user] = await this.prisma.$transaction([ this.prisma.user.create({ data: { - id: userInfo.id, - email: userInfo.email, + id: userCreateRequest.id, + email: userCreateRequest.email, }, include: { orgMemberships: true, @@ -24,34 +23,23 @@ export class UsersService { name: "Default Organization", members: { create: { - userId: userInfo.id, + userId: userCreateRequest.id, }, }, }, }), ]); - const key = `pez_${randomBytes(32).toString("hex")}`; - await this.prisma.apiKey.create({ - data: { - id: key, - organizationId: org.id, - }, - }); return user; } - async getOrCreateUser(userInfo: SupertokensUser): Promise { + async getUser(email: string): Promise { const user = await this.prisma.user.findUnique({ - where: { email: userInfo.email }, + where: { email }, include: { orgMemberships: true }, }); - if (user) { - return user; - } - - return this.createUser(userInfo); + return user; } async getUserOrgMemberships(userId: string) { diff --git a/apps/server/src/app/identity/users.types.ts b/apps/server/src/app/identity/users.types.ts index 68c4c687..c0ac63ab 100644 --- a/apps/server/src/app/identity/users.types.ts +++ b/apps/server/src/app/identity/users.types.ts @@ -1,5 +1,6 @@ import { OrgRole } from "@prisma/client"; - +import { User } from "../../@generated/user/user.model"; +import { Field, ObjectType } from "@nestjs/graphql"; export interface RequestUser { id: string; email: string; @@ -8,4 +9,24 @@ export interface RequestUser { role: OrgRole; memberSince: Date; }[]; + projects: { + id: string; + }[]; +} + +export interface UserCreateRequest { + id: string; + email: string; +} + +@ObjectType() +export class ExtendedUser extends User { + @Field(() => String, { nullable: true }) + name: string; + + @Field(() => String, { nullable: true }) + photoUrl: string; + + @Field(() => [String]) + organizationIds: string[]; } diff --git a/apps/server/src/app/influxdb/influxdb.module.ts b/apps/server/src/app/influxdb/influxdb.module.ts new file mode 100644 index 00000000..fa698b81 --- /dev/null +++ b/apps/server/src/app/influxdb/influxdb.module.ts @@ -0,0 +1,36 @@ +import { DynamicModule, Module, Global } from "@nestjs/common"; +import { InfluxDbService } from "./influxdb.service"; +import { InfluxModuleAsyncOptions, InfluxModuleOptions } from "./types"; + +@Global() +@Module({}) +export class InfluxDbModule { + static forRoot(options: InfluxModuleOptions): DynamicModule { + return { + module: InfluxDbModule, + providers: [ + { + provide: "INFLUX_DB_OPTIONS", + useValue: options, + }, + InfluxDbService, + ], + exports: [InfluxDbService], + }; + } + static forRootAsync(options: InfluxModuleAsyncOptions): DynamicModule { + return { + module: InfluxDbModule, + providers: [ + { + provide: "INFLUX_DB_OPTIONS", + useFactory: options.useFactory, + inject: options.inject || [], + }, + InfluxDbService, + ], + imports: options.imports || [], + exports: [InfluxDbService], + }; + } +} diff --git a/apps/server/src/app/influxdb/influxdb.service.ts b/apps/server/src/app/influxdb/influxdb.service.ts new file mode 100644 index 00000000..1a299b9b --- /dev/null +++ b/apps/server/src/app/influxdb/influxdb.service.ts @@ -0,0 +1,22 @@ +import { Inject, Injectable } from "@nestjs/common"; +import { InfluxDB } from "@influxdata/influxdb-client"; +import { ClientOptions } from "@influxdata/influxdb-client"; + +@Injectable() +export class InfluxDbService { + connection: InfluxDB | null; + + constructor( + @Inject("INFLUX_DB_OPTIONS") private readonly config: ClientOptions + ) { + this.connection = new InfluxDB(this.config); + } + + getWriteApi(org: string, bucket: string) { + return this.connection.getWriteApi(org, bucket, "ns"); + } + + getQueryApi(org: string) { + return this.connection.getQueryApi(org); + } +} diff --git a/apps/server/src/app/influxdb/types.ts b/apps/server/src/app/influxdb/types.ts new file mode 100644 index 00000000..ced4fdaf --- /dev/null +++ b/apps/server/src/app/influxdb/types.ts @@ -0,0 +1,10 @@ +import { ModuleMetadata } from "@nestjs/common/interfaces"; +import { ClientOptions } from "@influxdata/influxdb-client"; + +export interface InfluxModuleAsyncOptions + extends Pick { + useFactory: (...args: any[]) => Promise | ClientOptions; + inject: any[]; +} + +export type InfluxModuleOptions = ClientOptions; diff --git a/apps/server/src/app/metrics/inputs/get-metrics.input.ts b/apps/server/src/app/metrics/inputs/get-metrics.input.ts new file mode 100644 index 00000000..b7d17fe1 --- /dev/null +++ b/apps/server/src/app/metrics/inputs/get-metrics.input.ts @@ -0,0 +1,59 @@ +import { Field, InputType, registerEnumType } from "@nestjs/graphql"; + +export enum PromptExecutionMetricField { + total_cost = "total_cost", + duration = "duration", + total_tokens = "total_tokens", + status = "status", +} + +registerEnumType(PromptExecutionMetricField, { + name: "PromptExecutionMetricField", +}); + +export enum Aggregation { + sum = "sum", + mean = "mean", + min = "min", + max = "max", + count = "count", +} + +registerEnumType(Aggregation, { + name: "Aggregation", +}); + +export enum Granularity { + hour = "hour", + day = "day", + week = "week", + month = "month", +} + +registerEnumType(Granularity, { + name: "Granularity", +}); + +@InputType() +export class GetMetricsInput { + @Field(() => String, { nullable: false }) + promptId: string; + + @Field(() => PromptExecutionMetricField, { nullable: true }) + field?: PromptExecutionMetricField; + + @Field(() => String, { nullable: false }) + start: string; + + @Field(() => String, { nullable: true, defaultValue: "now()" }) + stop?: string = "now()"; + + @Field(() => Aggregation, { nullable: false }) + aggregation: Aggregation; + + @Field(() => Granularity, { nullable: false }) + granularity: Granularity; + + @Field(() => String, { nullable: true }) + fillEmpty?: string = null; +} diff --git a/apps/server/src/app/metrics/metrics.module.ts b/apps/server/src/app/metrics/metrics.module.ts new file mode 100644 index 00000000..91f02947 --- /dev/null +++ b/apps/server/src/app/metrics/metrics.module.ts @@ -0,0 +1,7 @@ +import { Module } from "@nestjs/common"; +import { MetricsResolver } from "./metrics.resolver"; + +@Module({ + providers: [MetricsResolver], +}) +export class MetricsModule {} diff --git a/apps/server/src/app/metrics/metrics.resolver.ts b/apps/server/src/app/metrics/metrics.resolver.ts new file mode 100644 index 00000000..4be1e30b --- /dev/null +++ b/apps/server/src/app/metrics/metrics.resolver.ts @@ -0,0 +1,66 @@ +import { Args, Query, Resolver } from "@nestjs/graphql"; +import { Metric } from "./models/metric.model"; +import { InfluxDbService } from "../influxdb/influxdb.service"; +import { InfluxQueryResult } from "./types"; +import { GetMetricsInput, Granularity } from "./inputs/get-metrics.input"; + +interface PromptExecutionQueryResult extends InfluxQueryResult { + prompt_id: string; + prompt_version_sha: string; + prompt_integration_id: string; + prompt_name: string; +} + +const granularityMapping = { + [Granularity.hour]: "1h", + [Granularity.day]: "1d", + [Granularity.week]: "1w", + [Granularity.month]: "1mo", +}; + +@Resolver(() => Metric) +export class MetricsResolver { + constructor(private influxService: InfluxDbService) {} + + @Query(() => [Metric]) + async metrics(@Args("data") data: GetMetricsInput) { + const queryClient = this.influxService.getQueryApi("primary"); + + const { + start, + stop, + field, + aggregation, + granularity, + promptId, + fillEmpty, + } = data; + + let query = `from(bucket: "primary") + |> range(start: ${start}, stop: ${stop}) + |> filter(fn: (r) => r["_measurement"] == "prompt_execution") + |> filter(fn: (r) => r["prompt_id"] == "${promptId}") + |> filter(fn: (r) => r["_field"] == "${field}") + `; + + query += `|> aggregateWindow(every: ${ + granularityMapping[granularity] + }, fn: ${aggregation}, createEmpty: ${fillEmpty ? "true" : "false"})`; + + if (fillEmpty) { + query += `|> fill(value: ${fillEmpty})`; + } + + const queryResults: InfluxQueryResult[] = await queryClient.collectRows( + query + ); + + return queryResults.map((r: PromptExecutionQueryResult) => ({ + value: r._value, + time: new Date(r._time), + metadata: { + prompt_version_sha: r.prompt_version_sha, + }, + })); + } +} diff --git a/apps/server/src/app/metrics/models/metric.model.ts b/apps/server/src/app/metrics/models/metric.model.ts new file mode 100644 index 00000000..1a44c6ea --- /dev/null +++ b/apps/server/src/app/metrics/models/metric.model.ts @@ -0,0 +1,15 @@ +import { Field } from "@nestjs/graphql"; +import { ObjectType } from "@nestjs/graphql"; +import GraphQLJSON from "graphql-type-json"; + +@ObjectType() +export class Metric { + @Field(() => Date, { nullable: false }) + time: Date; + + @Field(() => Number, { nullable: false }) + value: number; + + @Field(() => GraphQLJSON, { nullable: true, defaultValue: {} }) + metadata = {}; +} diff --git a/apps/server/src/app/metrics/types.ts b/apps/server/src/app/metrics/types.ts new file mode 100644 index 00000000..70e450b4 --- /dev/null +++ b/apps/server/src/app/metrics/types.ts @@ -0,0 +1,10 @@ +export interface InfluxQueryResult { + result: string; + table: number; + _start: string; + _stop: string; + _time: string; + _value: number; + _field: string; + _measurement: string; +} diff --git a/apps/server/src/app/prompt-environments/inputs/create-prompt-environment.input.ts b/apps/server/src/app/prompt-environments/inputs/create-prompt-environment.input.ts index bbc7890a..553b9f6c 100644 --- a/apps/server/src/app/prompt-environments/inputs/create-prompt-environment.input.ts +++ b/apps/server/src/app/prompt-environments/inputs/create-prompt-environment.input.ts @@ -8,6 +8,9 @@ export class PublishPromptInput { @Field(() => String, { nullable: false }) environmentSlug: string; + @Field(() => String, { nullable: false }) + projectId: string; + @Field(() => String, { nullable: false }) promptVersionSha: string; } diff --git a/apps/server/src/app/prompt-environments/prompt-environments.resolver.ts b/apps/server/src/app/prompt-environments/prompt-environments.resolver.ts index 898a6449..718220ef 100644 --- a/apps/server/src/app/prompt-environments/prompt-environments.resolver.ts +++ b/apps/server/src/app/prompt-environments/prompt-environments.resolver.ts @@ -12,6 +12,7 @@ import { EnvironmentsService } from "../environments/environments.service"; import { AuthGuard } from "../auth/auth.guard"; import { CurrentUser } from "../identity/current-user.decorator"; import { RequestUser } from "../identity/users.types"; +import { isProjectMemberOrThrow } from "../identity/identity.utils"; @UseGuards(AuthGuard) @Resolver() @@ -27,11 +28,11 @@ export class PromptEnvironmentsResolver { @Args("data") data: PublishPromptInput, @CurrentUser() user: RequestUser ) { - const organizationId = user.orgMemberships[0].organizationId; + isProjectMemberOrThrow(user, data.projectId); const environment = await this.environmentsService.getBySlug( data.environmentSlug, - organizationId + data.projectId ); if (!environment) { diff --git a/apps/server/src/app/prompts/inputs/create-prompt.input.ts b/apps/server/src/app/prompts/inputs/create-prompt.input.ts index c23142b7..6f7b3a8f 100644 --- a/apps/server/src/app/prompts/inputs/create-prompt.input.ts +++ b/apps/server/src/app/prompts/inputs/create-prompt.input.ts @@ -7,4 +7,7 @@ export class CreatePromptInput { @Field(() => String, { nullable: false }) integrationId: string; + + @Field(() => String, { nullable: false }) + projectId: string; } diff --git a/apps/server/src/app/prompts/inputs/get-project-prompts.input.ts b/apps/server/src/app/prompts/inputs/get-project-prompts.input.ts new file mode 100644 index 00000000..3e095abf --- /dev/null +++ b/apps/server/src/app/prompts/inputs/get-project-prompts.input.ts @@ -0,0 +1,7 @@ +import { Field, InputType } from "@nestjs/graphql"; + +@InputType() +export class GetProjectPromptsInput { + @Field(() => String, { nullable: false }) + projectId: string; +} diff --git a/apps/server/src/app/prompts/inputs/resolve-deployed-version.input.ts b/apps/server/src/app/prompts/inputs/resolve-deployed-version.input.ts new file mode 100644 index 00000000..0372f7fd --- /dev/null +++ b/apps/server/src/app/prompts/inputs/resolve-deployed-version.input.ts @@ -0,0 +1,7 @@ +import { Field, InputType } from "@nestjs/graphql"; + +@InputType() +export class ResolveDeployedVersionInput { + @Field(() => String, { nullable: false }) + environmentSlug: string; +} diff --git a/apps/server/src/app/prompts/inputs/test-prompt.input.ts b/apps/server/src/app/prompts/inputs/test-prompt.input.ts index 2201fa9e..28c00df7 100644 --- a/apps/server/src/app/prompts/inputs/test-prompt.input.ts +++ b/apps/server/src/app/prompts/inputs/test-prompt.input.ts @@ -3,6 +3,9 @@ import GraphQLJSON from "graphql-type-json"; @InputType() export class TestPromptInput { + @Field(() => String, { nullable: false }) + projectId: string; + @Field(() => String, { nullable: false }) integrationId: string; diff --git a/apps/server/src/app/prompts/prompt-executions.resolver.ts b/apps/server/src/app/prompts/prompt-executions.resolver.ts index 3a458785..9494ea08 100644 --- a/apps/server/src/app/prompts/prompt-executions.resolver.ts +++ b/apps/server/src/app/prompts/prompt-executions.resolver.ts @@ -17,16 +17,19 @@ import { NotFoundException, UseGuards, } from "@nestjs/common"; -import { ApiKeyOrgId } from "../identity/api-key-org-id"; -import { isOrgMemberOrThrow } from "../identity/identity.utils"; +import { ApiKeyProjectId } from "../identity/api-key-project-id.decorator"; +import { isProjectMemberOrThrow } from "../identity/identity.utils"; +import { InfluxDbService } from "../influxdb/influxdb.service"; +import { Point } from "@influxdata/influxdb-client"; @UseGuards(AuthGuard) @Resolver(() => Prompt) export class PromptExecutionsResolver { constructor( private prisma: PrismaService, - private readonly promptsService: PromptsService, - private readonly promptTesterService: PromptTesterService + private promptsService: PromptsService, + private promptTesterService: PromptTesterService, + private influxService: InfluxDbService ) {} @Query(() => PromptExecution) @@ -46,7 +49,7 @@ export class PromptExecutionsResolver { promptExecution.promptId ); - isOrgMemberOrThrow(user, prompt.organizationId); + isProjectMemberOrThrow(user, prompt.projectId); return promptExecution; } @@ -63,7 +66,7 @@ export class PromptExecutionsResolver { throw new NotFoundException(); } - isOrgMemberOrThrow(user, prompt.organizationId); + isProjectMemberOrThrow(user, prompt.projectId); const executions = await this.prisma.promptExecution.findMany({ where: data, @@ -78,7 +81,7 @@ export class PromptExecutionsResolver { @Mutation(() => PromptExecution) async reportPromptExecutionWithApiKey( @Args("data") data: PromptExecutionCreateInput, - @ApiKeyOrgId() orgId: string + @ApiKeyProjectId() projectId: string ) { const promptId = data.prompt.connect.id; const prompt = await this.promptsService.getPrompt(promptId); @@ -87,13 +90,33 @@ export class PromptExecutionsResolver { throw new NotFoundException(); } - if (prompt.organizationId !== orgId) { + if (prompt.projectId !== projectId) { throw new ForbiddenException(); } const execution = await this.prisma.promptExecution.create({ data, }); + + const writeClient = this.influxService.getWriteApi("primary", "primary"); + const point = new Point("prompt_execution") + .tag("prompt_id", execution.promptId) + .tag("prompt_version_sha", execution.promptVersionSha) + .tag("project_id", prompt.projectId) + .tag("prompt_name", prompt.name) + .tag("prompt_integration_id", prompt.integrationId) + .stringField("status", execution.status) + .floatField("duration", execution.duration / 1000) + .floatField("prompt_cost", execution.promptCost) + .floatField("completion_cost", execution.completionCost) + .floatField("total_cost", execution.totalCost) + .intField("prompt_tokens", execution.promptTokens) + .intField("completion_tokens", execution.completionTokens) + .intField("total_tokens", execution.totalTokens); + + writeClient.writePoint(point); + writeClient.flush(); + return execution; } @@ -103,9 +126,11 @@ export class PromptExecutionsResolver { @Args("data") data: TestPromptInput, @CurrentUser() user: RequestUser ) { + isProjectMemberOrThrow(user, data.projectId); + const result = await this.promptTesterService.testPrompt( data, - user.orgMemberships[0].organizationId + data.projectId ); const execution = new PromptExecution(); diff --git a/apps/server/src/app/prompts/prompt-tester.service.ts b/apps/server/src/app/prompts/prompt-tester.service.ts index e5ccdebf..4c14dd7f 100644 --- a/apps/server/src/app/prompts/prompt-tester.service.ts +++ b/apps/server/src/app/prompts/prompt-tester.service.ts @@ -11,11 +11,11 @@ const noop = {} as Pezzo; export class PromptTesterService { constructor(private providerAPIKeysService: ProviderApiKeysService) {} - private async executorFactory(integrationId: string, organizationId: string) { + private async executorFactory(integrationId: string, projectId: string) { const { provider } = getIntegration(integrationId); const apiKey = await this.providerAPIKeysService.getByProvider( provider, - organizationId + projectId ); if (!apiKey) { @@ -32,13 +32,11 @@ export class PromptTesterService { async testPrompt( input: TestPromptInput, - organizationId: string + projectId: string ): Promise { const { integrationId, content, variables } = input; const interpolatedContent = interpolateVariables(content, variables); - - const executor = await this.executorFactory(integrationId, organizationId); - + const executor = await this.executorFactory(integrationId, projectId); const settings = input.settings; const start = performance.now(); diff --git a/apps/server/src/app/prompts/prompts.resolver.ts b/apps/server/src/app/prompts/prompts.resolver.ts index 9afe669c..bacdfb21 100644 --- a/apps/server/src/app/prompts/prompts.resolver.ts +++ b/apps/server/src/app/prompts/prompts.resolver.ts @@ -11,7 +11,7 @@ import { PrismaService } from "../prisma.service"; import { PromptWhereUniqueInput } from "../../@generated/prompt/prompt-where-unique.input"; import { PromptUpdateInput } from "../../@generated/prompt/prompt-update.input"; import { PromptExecution } from "../../@generated/prompt-execution/prompt-execution.model"; -import { Prompt as PrismaPrompt, User } from "@prisma/client"; +import { Prompt as PrismaPrompt } from "@prisma/client"; import { CreatePromptInput } from "./inputs/create-prompt.input"; import { PromptsService } from "./prompts.service"; import { PromptVersion } from "../../@generated/prompt-version/prompt-version.model"; @@ -24,13 +24,15 @@ import { NotFoundException, UseGuards, } from "@nestjs/common"; -import { AuthGuard, AuthMethod } from "../auth/auth.guard"; +import { AuthGuard } from "../auth/auth.guard"; import { CurrentUser } from "../identity/current-user.decorator"; import { RequestUser } from "../identity/users.types"; -import { isOrgMemberOrThrow } from "../identity/identity.utils"; -import { ApiKeyOrgId } from "../identity/api-key-org-id"; +import { isProjectMemberOrThrow } from "../identity/identity.utils"; import { FindPromptByNameInput } from "./inputs/find-prompt-by-name.input"; import { EnvironmentsService } from "../environments/environments.service"; +import { GetProjectPromptsInput } from "./inputs/get-project-prompts.input"; +import { ApiKeyProjectId } from "../identity/api-key-project-id.decorator"; +import { ResolveDeployedVersionInput } from "./inputs/resolve-deployed-version.input"; @UseGuards(AuthGuard) @Resolver(() => Prompt) @@ -42,17 +44,21 @@ export class PromptsResolver { ) {} @Query(() => [Prompt]) - async prompts(@CurrentUser() user: RequestUser) { + async prompts( + @Args("data") data: GetProjectPromptsInput, + @CurrentUser() user: RequestUser + ) { + isProjectMemberOrThrow(user, data.projectId); + const prompts = await this.prisma.prompt.findMany({ where: { - organizationId: { - in: user.orgMemberships.map((m) => m.organizationId), - }, + projectId: data.projectId, }, orderBy: { createdAt: "desc", }, }); + return prompts; } @@ -71,7 +77,7 @@ export class PromptsResolver { throw new NotFoundException(); } - isOrgMemberOrThrow(user, prompt.organizationId); + isProjectMemberOrThrow(user, prompt.projectId); return prompt; } @@ -86,8 +92,7 @@ export class PromptsResolver { throw new NotFoundException(); } - isOrgMemberOrThrow(user, prompt.organizationId); - + isProjectMemberOrThrow(user, prompt.projectId); const promptVersions = await this.promptsService.getPromptVersions(data.id); return promptVersions; } @@ -100,7 +105,9 @@ export class PromptsResolver { const prompt = await this.prisma.prompt.findFirst({ where: { name: data.name, - organizationId: user.orgMemberships[0].organizationId, + projectId: { + in: user.projects.map((p) => p.id), + }, }, }); @@ -108,19 +115,19 @@ export class PromptsResolver { throw new NotFoundException(`Prompt "${data.name}" not found"`); } - isOrgMemberOrThrow(user, prompt.organizationId); + isProjectMemberOrThrow(user, prompt.projectId); return prompt; } @Query(() => Prompt) async findPromptWithApiKey( @Args("data") data: FindPromptByNameInput, - @ApiKeyOrgId() organizationId: string + @ApiKeyProjectId() projectId: string ) { const prompt = await this.prisma.prompt.findFirst({ where: { name: data.name, - organizationId, + projectId, }, }); @@ -151,7 +158,7 @@ export class PromptsResolver { throw new NotFoundException(); } - isOrgMemberOrThrow(user, promptVersion.prompt.organizationId); + isProjectMemberOrThrow(user, promptVersion.prompt.projectId); return promptVersion; } @@ -165,7 +172,7 @@ export class PromptsResolver { if (!prompt) { throw new NotFoundException(); } - isOrgMemberOrThrow(user, prompt.organizationId); + isProjectMemberOrThrow(user, prompt.projectId); const promptVersion = await this.promptsService.getLatestPromptVersion( data.id @@ -173,98 +180,16 @@ export class PromptsResolver { return promptVersion; } - @Query(() => PromptVersion) - async deployedPromptVersionWithApiKey( - @Args("data") data: GetDeployedPromptVersionInput, - @ApiKeyOrgId() organizationId: string - ) { - const { environmentSlug, promptId } = data; - const prompt = await this.promptsService.getPrompt(promptId); - - if (!prompt) { - throw new NotFoundException(`Prompt with id "${promptId}" not found`); - } - - const environment = await this.environmentsService.getBySlug( - environmentSlug, - organizationId - ); - - if (!environment) { - throw new NotFoundException( - `Environment with slug "${environmentSlug}" not found` - ); - } - - const deployedPrompt = await this.prisma.promptEnvironment.findFirst({ - where: { promptId, environmentId: environment.id }, - }); - - if (!deployedPrompt) { - throw new NotFoundException( - `Prompt was not deployed to the "${environmentSlug}" environment` - ); - } - - const promptVersion = await this.promptsService.getPromptVersion( - deployedPrompt.promptVersionSha - ); - - return promptVersion; - } - - @Query(() => PromptVersion) - async deployedPromptVersion( - @Args("data") data: GetDeployedPromptVersionInput, - @CurrentUser() user: RequestUser - ) { - const { environmentSlug, promptId } = data; - const organizationId = user.orgMemberships[0].organizationId; - - const prompt = await this.promptsService.getPrompt(promptId); - - if (!prompt) { - throw new NotFoundException(`Prompt with id "${promptId}" not found`); - } - - isOrgMemberOrThrow(user, prompt.organizationId); - - const environment = await this.environmentsService.getBySlug( - environmentSlug, - organizationId - ); - - if (!environment) { - throw new NotFoundException( - `Environment with slug "${environmentSlug}" not found` - ); - } - - const deployedPrompt = await this.prisma.promptEnvironment.findFirst({ - where: { promptId, environmentId: environment.id }, - }); - - if (!deployedPrompt) { - throw new NotFoundException( - `Prompt was not deployed to the "${environmentSlug}" environment` - ); - } - - const promptVersion = await this.promptsService.getPromptVersion( - deployedPrompt.promptVersionSha - ); - - return promptVersion; - } - @Mutation(() => Prompt) async createPrompt( @Args("data") data: CreatePromptInput, @CurrentUser() user: RequestUser ) { + isProjectMemberOrThrow(user, data.projectId); + const exists = await this.promptsService.getPromptByName( data.name, - user.orgMemberships[0].organizationId + data.projectId ); if (exists) { @@ -274,7 +199,7 @@ export class PromptsResolver { const prompt = await this.promptsService.createPrompt( data.name, data.integrationId, - user.orgMemberships[0].organizationId + data.projectId ); return prompt; } @@ -290,7 +215,7 @@ export class PromptsResolver { throw new NotFoundException(); } - isOrgMemberOrThrow(user, exists.organizationId); + isProjectMemberOrThrow(user, exists.projectId); const prompt = await this.prisma.prompt.update({ where: { @@ -314,7 +239,7 @@ export class PromptsResolver { throw new NotFoundException(); } - isOrgMemberOrThrow(user, prompt.organizationId); + isProjectMemberOrThrow(user, prompt.projectId); return this.promptsService.createPromptVersion(data); } @@ -328,6 +253,42 @@ export class PromptsResolver { return executions; } + @ResolveField(() => PromptVersion) + async deployedVersion( + @Parent() prompt: PrismaPrompt, + @Args("data") data: ResolveDeployedVersionInput + ) { + const { projectId } = prompt; + const { environmentSlug } = data; + + const environment = await this.environmentsService.getBySlug( + environmentSlug, + projectId + ); + + if (!environment) { + throw new NotFoundException( + `Environment with slug "${environmentSlug}" not found` + ); + } + + const deployedPrompt = await this.prisma.promptEnvironment.findFirst({ + where: { promptId: prompt.id, environmentId: environment.id }, + }); + + if (!deployedPrompt) { + throw new NotFoundException( + `Prompt was not deployed to the "${environmentSlug}" environment` + ); + } + + const promptVersion = await this.promptsService.getPromptVersion( + deployedPrompt.promptVersionSha + ); + + return promptVersion; + } + @ResolveField(() => [PromptVersion]) async versions(@Parent() prompt: PrismaPrompt) { return this.promptsService.getPromptVersions(prompt.id); diff --git a/apps/server/src/app/prompts/prompts.service.ts b/apps/server/src/app/prompts/prompts.service.ts index 896cd206..7a88186a 100644 --- a/apps/server/src/app/prompts/prompts.service.ts +++ b/apps/server/src/app/prompts/prompts.service.ts @@ -16,25 +16,21 @@ export class PromptsService { return prompt; } - async getPromptByName(name: string, organizationId: string) { + async getPromptByName(name: string, projectId: string) { const prompt = await this.prisma.prompt.findFirst({ where: { name, - organizationId, + projectId, }, }); return prompt; } - async createPrompt( - name: string, - integrationId: string, - organizationId: string - ) { + async createPrompt(name: string, integrationId: string, projectId: string) { const prompt = await this.prisma.prompt.create({ data: { integrationId, - organizationId, + projectId, name, versions: { create: [], diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 44892582..1e52b214 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -1,4 +1,4 @@ -import { Logger } from "@nestjs/common"; +import { Logger, ValidationPipe } from "@nestjs/common"; import { NestFactory } from "@nestjs/core"; import supertokens from "supertokens-node"; @@ -17,6 +17,7 @@ async function bootstrap() { app.setGlobalPrefix(globalPrefix); app.useGlobalFilters(new SupertokensExceptionFilter()); + app.useGlobalPipes(new ValidationPipe({ transform: true })); const port = process.env.PORT || 3000; await app.listen(port); diff --git a/apps/server/src/schema.graphql b/apps/server/src/schema.graphql deleted file mode 100644 index 6c88e334..00000000 --- a/apps/server/src/schema.graphql +++ /dev/null @@ -1,1196 +0,0 @@ -# ------------------------------------------------------ -# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) -# ------------------------------------------------------ - -type ApiKey { - Organization: Organization! - createdAt: DateTime! - id: ID! - name: String! - organizationId: String! -} - -input CreateEnvironmentInput { - name: String! - slug: String! -} - -input CreatePromptInput { - integrationId: String! - name: String! -} - -input CreatePromptVersionInput { - content: String! - message: String! - promptId: String! - settings: JSON! -} - -input CreateProviderApiKeyInput { - provider: String! - value: String! -} - -""" -A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. -""" -scalar DateTime - -input DateTimeFieldUpdateOperationsInput { - set: DateTime -} - -input DateTimeFilter { - equals: DateTime - gt: DateTime - gte: DateTime - in: [DateTime!] - lt: DateTime - lte: DateTime - not: NestedDateTimeFilter - notIn: [DateTime!] -} - -input EnumPromptExecutionStatusFieldUpdateOperationsInput { - set: PromptExecutionStatus -} - -input EnumPromptExecutionStatusFilter { - equals: PromptExecutionStatus - in: [PromptExecutionStatus!] - not: NestedEnumPromptExecutionStatusFilter - notIn: [PromptExecutionStatus!] -} - -type Environment { - _count: EnvironmentCount! - createdAt: DateTime! - id: ID! - name: String! - organizationId: String! - promptEnvironments: [PromptEnvironment!] - slug: String! - updatedAt: DateTime! -} - -type EnvironmentCount { - promptEnvironments: Int! -} - -input EnvironmentCreateNestedOneWithoutPromptEnvironmentsInput { - connect: EnvironmentWhereUniqueInput - connectOrCreate: EnvironmentCreateOrConnectWithoutPromptEnvironmentsInput - create: EnvironmentCreateWithoutPromptEnvironmentsInput -} - -input EnvironmentCreateOrConnectWithoutPromptEnvironmentsInput { - create: EnvironmentCreateWithoutPromptEnvironmentsInput! - where: EnvironmentWhereUniqueInput! -} - -input EnvironmentCreateWithoutPromptEnvironmentsInput { - createdAt: DateTime - id: String - name: String! - organizationId: String! - slug: String! - updatedAt: DateTime -} - -input EnvironmentRelationFilter { - is: EnvironmentWhereInput - isNot: EnvironmentWhereInput -} - -input EnvironmentUpdateOneRequiredWithoutPromptEnvironmentsInput { - connect: EnvironmentWhereUniqueInput - connectOrCreate: EnvironmentCreateOrConnectWithoutPromptEnvironmentsInput - create: EnvironmentCreateWithoutPromptEnvironmentsInput - update: EnvironmentUpdateWithoutPromptEnvironmentsInput - upsert: EnvironmentUpsertWithoutPromptEnvironmentsInput -} - -input EnvironmentUpdateWithoutPromptEnvironmentsInput { - createdAt: DateTimeFieldUpdateOperationsInput - id: StringFieldUpdateOperationsInput - name: StringFieldUpdateOperationsInput - organizationId: StringFieldUpdateOperationsInput - slug: StringFieldUpdateOperationsInput - updatedAt: DateTimeFieldUpdateOperationsInput -} - -input EnvironmentUpsertWithoutPromptEnvironmentsInput { - create: EnvironmentCreateWithoutPromptEnvironmentsInput! - update: EnvironmentUpdateWithoutPromptEnvironmentsInput! -} - -input EnvironmentWhereInput { - AND: [EnvironmentWhereInput!] - NOT: [EnvironmentWhereInput!] - OR: [EnvironmentWhereInput!] - createdAt: DateTimeFilter - id: StringFilter - name: StringFilter - organizationId: StringFilter - promptEnvironments: PromptEnvironmentListRelationFilter - slug: StringFilter - updatedAt: DateTimeFilter -} - -input EnvironmentWhereUniqueInput { - id: String -} - -input FindPromptByNameInput { - name: String! -} - -input FloatFieldUpdateOperationsInput { - decrement: Float - divide: Float - increment: Float - multiply: Float - set: Float -} - -input FloatFilter { - equals: Float - gt: Float - gte: Float - in: [Float!] - lt: Float - lte: Float - not: NestedFloatFilter - notIn: [Float!] -} - -input GetDeployedPromptVersionInput { - environmentSlug: String! - promptId: String! -} - -input GetEnvironmentBySlugInput { - slug: String! -} - -input GetPromptInput { - promptId: String! - version: String = "latest" -} - -input GetPromptVersionInput { - promptId: String! - sha: String! -} - -input IntFieldUpdateOperationsInput { - decrement: Int - divide: Int - increment: Int - multiply: Int - set: Int -} - -input IntFilter { - equals: Int - gt: Int - gte: Int - in: [Int!] - lt: Int - lte: Int - not: NestedIntFilter - notIn: [Int!] -} - -""" -The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). -""" -scalar JSON - -input JsonFilter { - equals: JSON - not: JSON -} - -type Mutation { - createEnvironment(data: CreateEnvironmentInput!): Environment! - createPrompt(data: CreatePromptInput!): Prompt! - createPromptVersion(data: CreatePromptVersionInput!): PromptVersion! - publishPrompt(data: PublishPromptInput!): PromptEnvironment! - reportPromptExecutionWithApiKey(data: PromptExecutionCreateInput!): PromptExecution! - testPrompt(data: TestPromptInput!): PromptExecution! - updatePrompt(data: PromptUpdateInput!): Prompt! - updateProviderApiKey(data: CreateProviderApiKeyInput!): ProviderApiKey! -} - -input NestedDateTimeFilter { - equals: DateTime - gt: DateTime - gte: DateTime - in: [DateTime!] - lt: DateTime - lte: DateTime - not: NestedDateTimeFilter - notIn: [DateTime!] -} - -input NestedEnumPromptExecutionStatusFilter { - equals: PromptExecutionStatus - in: [PromptExecutionStatus!] - not: NestedEnumPromptExecutionStatusFilter - notIn: [PromptExecutionStatus!] -} - -input NestedFloatFilter { - equals: Float - gt: Float - gte: Float - in: [Float!] - lt: Float - lte: Float - not: NestedFloatFilter - notIn: [Float!] -} - -input NestedIntFilter { - equals: Int - gt: Int - gte: Int - in: [Int!] - lt: Int - lte: Int - not: NestedIntFilter - notIn: [Int!] -} - -input NestedStringFilter { - contains: String - endsWith: String - equals: String - gt: String - gte: String - in: [String!] - lt: String - lte: String - not: NestedStringFilter - notIn: [String!] - startsWith: String -} - -input NestedStringNullableFilter { - contains: String - endsWith: String - equals: String - gt: String - gte: String - in: [String!] - lt: String - lte: String - not: NestedStringNullableFilter - notIn: [String!] - startsWith: String -} - -input NullableStringFieldUpdateOperationsInput { - set: String -} - -enum OrgRole { - Admin -} - -type Organization { - _count: OrganizationCount! - apiKeys: [ApiKey!] - createdAt: DateTime! - id: ID! - members: [OrganizationMember!] - name: String! - providerApiKeys: [ProviderApiKey!] - updatedAt: DateTime! -} - -type OrganizationCount { - apiKeys: Int! - members: Int! - providerApiKeys: Int! -} - -type OrganizationMember { - createdAt: DateTime! - id: ID! - organization: Organization! - organizationId: String! - role: OrgRole! - updatedAt: DateTime! - user: User! - userId: String! -} - -type Prompt { - _count: PromptCount! - createdAt: DateTime! - executions: [PromptExecution!] - id: ID! - integrationId: String! - latestVersion: PromptVersion - name: String! - organizationId: String! - promptEnvironments: [PromptEnvironment!] - updatedAt: DateTime! - version(data: GetPromptInput!): PromptVersion - versions: [PromptVersion!] -} - -type PromptCount { - executions: Int! - promptEnvironments: Int! - versions: Int! -} - -input PromptCreateNestedOneWithoutExecutionsInput { - connect: PromptWhereUniqueInput - connectOrCreate: PromptCreateOrConnectWithoutExecutionsInput - create: PromptCreateWithoutExecutionsInput -} - -input PromptCreateNestedOneWithoutPromptEnvironmentsInput { - connect: PromptWhereUniqueInput - connectOrCreate: PromptCreateOrConnectWithoutPromptEnvironmentsInput - create: PromptCreateWithoutPromptEnvironmentsInput -} - -input PromptCreateNestedOneWithoutVersionsInput { - connect: PromptWhereUniqueInput - connectOrCreate: PromptCreateOrConnectWithoutVersionsInput - create: PromptCreateWithoutVersionsInput -} - -input PromptCreateOrConnectWithoutExecutionsInput { - create: PromptCreateWithoutExecutionsInput! - where: PromptWhereUniqueInput! -} - -input PromptCreateOrConnectWithoutPromptEnvironmentsInput { - create: PromptCreateWithoutPromptEnvironmentsInput! - where: PromptWhereUniqueInput! -} - -input PromptCreateOrConnectWithoutVersionsInput { - create: PromptCreateWithoutVersionsInput! - where: PromptWhereUniqueInput! -} - -input PromptCreateWithoutExecutionsInput { - createdAt: DateTime - id: String - integrationId: String! - name: String! - organizationId: String! - promptEnvironments: PromptEnvironmentCreateNestedManyWithoutPromptInput - updatedAt: DateTime - versions: PromptVersionCreateNestedManyWithoutPromptInput -} - -input PromptCreateWithoutPromptEnvironmentsInput { - createdAt: DateTime - executions: PromptExecutionCreateNestedManyWithoutPromptInput - id: String - integrationId: String! - name: String! - organizationId: String! - updatedAt: DateTime - versions: PromptVersionCreateNestedManyWithoutPromptInput -} - -input PromptCreateWithoutVersionsInput { - createdAt: DateTime - executions: PromptExecutionCreateNestedManyWithoutPromptInput - id: String - integrationId: String! - name: String! - organizationId: String! - promptEnvironments: PromptEnvironmentCreateNestedManyWithoutPromptInput - updatedAt: DateTime -} - -type PromptEnvironment { - createdAt: DateTime! - environment: Environment! - environmentId: String! - id: ID! - prompt: Prompt! - promptId: String! - promptVersion: PromptVersion - promptVersionSha: String! -} - -input PromptEnvironmentCreateManyPromptInput { - createdAt: DateTime - environmentId: String! - id: String - promptVersionSha: String! -} - -input PromptEnvironmentCreateManyPromptInputEnvelope { - data: [PromptEnvironmentCreateManyPromptInput!]! - skipDuplicates: Boolean -} - -input PromptEnvironmentCreateManyPromptVersionInput { - createdAt: DateTime - environmentId: String! - id: String - promptId: String! -} - -input PromptEnvironmentCreateManyPromptVersionInputEnvelope { - data: [PromptEnvironmentCreateManyPromptVersionInput!]! - skipDuplicates: Boolean -} - -input PromptEnvironmentCreateNestedManyWithoutPromptInput { - connect: [PromptEnvironmentWhereUniqueInput!] - connectOrCreate: [PromptEnvironmentCreateOrConnectWithoutPromptInput!] - create: [PromptEnvironmentCreateWithoutPromptInput!] - createMany: PromptEnvironmentCreateManyPromptInputEnvelope -} - -input PromptEnvironmentCreateNestedManyWithoutPromptVersionInput { - connect: [PromptEnvironmentWhereUniqueInput!] - connectOrCreate: [PromptEnvironmentCreateOrConnectWithoutPromptVersionInput!] - create: [PromptEnvironmentCreateWithoutPromptVersionInput!] - createMany: PromptEnvironmentCreateManyPromptVersionInputEnvelope -} - -input PromptEnvironmentCreateOrConnectWithoutPromptInput { - create: PromptEnvironmentCreateWithoutPromptInput! - where: PromptEnvironmentWhereUniqueInput! -} - -input PromptEnvironmentCreateOrConnectWithoutPromptVersionInput { - create: PromptEnvironmentCreateWithoutPromptVersionInput! - where: PromptEnvironmentWhereUniqueInput! -} - -input PromptEnvironmentCreateWithoutPromptInput { - createdAt: DateTime - environment: EnvironmentCreateNestedOneWithoutPromptEnvironmentsInput! - id: String - promptVersion: PromptVersionCreateNestedOneWithoutPromptEnvironmentsInput -} - -input PromptEnvironmentCreateWithoutPromptVersionInput { - createdAt: DateTime - environment: EnvironmentCreateNestedOneWithoutPromptEnvironmentsInput! - id: String - prompt: PromptCreateNestedOneWithoutPromptEnvironmentsInput! -} - -input PromptEnvironmentListRelationFilter { - every: PromptEnvironmentWhereInput - none: PromptEnvironmentWhereInput - some: PromptEnvironmentWhereInput -} - -input PromptEnvironmentScalarWhereInput { - AND: [PromptEnvironmentScalarWhereInput!] - NOT: [PromptEnvironmentScalarWhereInput!] - OR: [PromptEnvironmentScalarWhereInput!] - createdAt: DateTimeFilter - environmentId: StringFilter - id: StringFilter - promptId: StringFilter - promptVersionSha: StringFilter -} - -input PromptEnvironmentUpdateManyMutationInput { - createdAt: DateTimeFieldUpdateOperationsInput - id: StringFieldUpdateOperationsInput -} - -input PromptEnvironmentUpdateManyWithWhereWithoutPromptInput { - data: PromptEnvironmentUpdateManyMutationInput! - where: PromptEnvironmentScalarWhereInput! -} - -input PromptEnvironmentUpdateManyWithWhereWithoutPromptVersionInput { - data: PromptEnvironmentUpdateManyMutationInput! - where: PromptEnvironmentScalarWhereInput! -} - -input PromptEnvironmentUpdateManyWithoutPromptInput { - connect: [PromptEnvironmentWhereUniqueInput!] - connectOrCreate: [PromptEnvironmentCreateOrConnectWithoutPromptInput!] - create: [PromptEnvironmentCreateWithoutPromptInput!] - createMany: PromptEnvironmentCreateManyPromptInputEnvelope - delete: [PromptEnvironmentWhereUniqueInput!] - deleteMany: [PromptEnvironmentScalarWhereInput!] - disconnect: [PromptEnvironmentWhereUniqueInput!] - set: [PromptEnvironmentWhereUniqueInput!] - update: [PromptEnvironmentUpdateWithWhereUniqueWithoutPromptInput!] - updateMany: [PromptEnvironmentUpdateManyWithWhereWithoutPromptInput!] - upsert: [PromptEnvironmentUpsertWithWhereUniqueWithoutPromptInput!] -} - -input PromptEnvironmentUpdateManyWithoutPromptVersionInput { - connect: [PromptEnvironmentWhereUniqueInput!] - connectOrCreate: [PromptEnvironmentCreateOrConnectWithoutPromptVersionInput!] - create: [PromptEnvironmentCreateWithoutPromptVersionInput!] - createMany: PromptEnvironmentCreateManyPromptVersionInputEnvelope - delete: [PromptEnvironmentWhereUniqueInput!] - deleteMany: [PromptEnvironmentScalarWhereInput!] - disconnect: [PromptEnvironmentWhereUniqueInput!] - set: [PromptEnvironmentWhereUniqueInput!] - update: [PromptEnvironmentUpdateWithWhereUniqueWithoutPromptVersionInput!] - updateMany: [PromptEnvironmentUpdateManyWithWhereWithoutPromptVersionInput!] - upsert: [PromptEnvironmentUpsertWithWhereUniqueWithoutPromptVersionInput!] -} - -input PromptEnvironmentUpdateWithWhereUniqueWithoutPromptInput { - data: PromptEnvironmentUpdateWithoutPromptInput! - where: PromptEnvironmentWhereUniqueInput! -} - -input PromptEnvironmentUpdateWithWhereUniqueWithoutPromptVersionInput { - data: PromptEnvironmentUpdateWithoutPromptVersionInput! - where: PromptEnvironmentWhereUniqueInput! -} - -input PromptEnvironmentUpdateWithoutPromptInput { - createdAt: DateTimeFieldUpdateOperationsInput - environment: EnvironmentUpdateOneRequiredWithoutPromptEnvironmentsInput - id: StringFieldUpdateOperationsInput - promptVersion: PromptVersionUpdateOneWithoutPromptEnvironmentsInput -} - -input PromptEnvironmentUpdateWithoutPromptVersionInput { - createdAt: DateTimeFieldUpdateOperationsInput - environment: EnvironmentUpdateOneRequiredWithoutPromptEnvironmentsInput - id: StringFieldUpdateOperationsInput - prompt: PromptUpdateOneRequiredWithoutPromptEnvironmentsInput -} - -input PromptEnvironmentUpsertWithWhereUniqueWithoutPromptInput { - create: PromptEnvironmentCreateWithoutPromptInput! - update: PromptEnvironmentUpdateWithoutPromptInput! - where: PromptEnvironmentWhereUniqueInput! -} - -input PromptEnvironmentUpsertWithWhereUniqueWithoutPromptVersionInput { - create: PromptEnvironmentCreateWithoutPromptVersionInput! - update: PromptEnvironmentUpdateWithoutPromptVersionInput! - where: PromptEnvironmentWhereUniqueInput! -} - -input PromptEnvironmentWhereInput { - AND: [PromptEnvironmentWhereInput!] - NOT: [PromptEnvironmentWhereInput!] - OR: [PromptEnvironmentWhereInput!] - createdAt: DateTimeFilter - environment: EnvironmentRelationFilter - environmentId: StringFilter - id: StringFilter - prompt: PromptRelationFilter - promptId: StringFilter - promptVersion: PromptVersionRelationFilter - promptVersionSha: StringFilter -} - -input PromptEnvironmentWhereUniqueInput { - id: String -} - -type PromptExecution { - completionCost: Float! - completionTokens: Int! - content: String! - duration: Int! - error: String - id: ID! - interpolatedContent: String! - prompt: Prompt! - promptCost: Float! - promptId: String! - promptTokens: Int! - promptVersionSha: String! - result: String - settings: JSON! - status: PromptExecutionStatus! - timestamp: DateTime! - totalCost: Float! - totalTokens: Int! - variables: JSON! -} - -input PromptExecutionCreateInput { - completionCost: Float! - completionTokens: Int! - content: String! - duration: Int! - error: String - id: String - interpolatedContent: String! - prompt: PromptCreateNestedOneWithoutExecutionsInput! - promptCost: Float! - promptTokens: Int! - promptVersionSha: String! - result: String - settings: JSON! - status: PromptExecutionStatus! - timestamp: DateTime - totalCost: Float! - totalTokens: Int! - variables: JSON -} - -input PromptExecutionCreateManyPromptInput { - completionCost: Float! - completionTokens: Int! - content: String! - duration: Int! - error: String - id: String - interpolatedContent: String! - promptCost: Float! - promptTokens: Int! - promptVersionSha: String! - result: String - settings: JSON! - status: PromptExecutionStatus! - timestamp: DateTime - totalCost: Float! - totalTokens: Int! - variables: JSON -} - -input PromptExecutionCreateManyPromptInputEnvelope { - data: [PromptExecutionCreateManyPromptInput!]! - skipDuplicates: Boolean -} - -input PromptExecutionCreateNestedManyWithoutPromptInput { - connect: [PromptExecutionWhereUniqueInput!] - connectOrCreate: [PromptExecutionCreateOrConnectWithoutPromptInput!] - create: [PromptExecutionCreateWithoutPromptInput!] - createMany: PromptExecutionCreateManyPromptInputEnvelope -} - -input PromptExecutionCreateOrConnectWithoutPromptInput { - create: PromptExecutionCreateWithoutPromptInput! - where: PromptExecutionWhereUniqueInput! -} - -input PromptExecutionCreateWithoutPromptInput { - completionCost: Float! - completionTokens: Int! - content: String! - duration: Int! - error: String - id: String - interpolatedContent: String! - promptCost: Float! - promptTokens: Int! - promptVersionSha: String! - result: String - settings: JSON! - status: PromptExecutionStatus! - timestamp: DateTime - totalCost: Float! - totalTokens: Int! - variables: JSON -} - -input PromptExecutionListRelationFilter { - every: PromptExecutionWhereInput - none: PromptExecutionWhereInput - some: PromptExecutionWhereInput -} - -input PromptExecutionScalarWhereInput { - AND: [PromptExecutionScalarWhereInput!] - NOT: [PromptExecutionScalarWhereInput!] - OR: [PromptExecutionScalarWhereInput!] - completionCost: FloatFilter - completionTokens: IntFilter - content: StringFilter - duration: IntFilter - error: StringNullableFilter - id: StringFilter - interpolatedContent: StringFilter - promptCost: FloatFilter - promptId: StringFilter - promptTokens: IntFilter - promptVersionSha: StringFilter - result: StringNullableFilter - settings: JsonFilter - status: EnumPromptExecutionStatusFilter - timestamp: DateTimeFilter - totalCost: FloatFilter - totalTokens: IntFilter - variables: JsonFilter -} - -enum PromptExecutionStatus { - Error - Success -} - -input PromptExecutionUpdateManyMutationInput { - completionCost: FloatFieldUpdateOperationsInput - completionTokens: IntFieldUpdateOperationsInput - content: StringFieldUpdateOperationsInput - duration: IntFieldUpdateOperationsInput - error: NullableStringFieldUpdateOperationsInput - id: StringFieldUpdateOperationsInput - interpolatedContent: StringFieldUpdateOperationsInput - promptCost: FloatFieldUpdateOperationsInput - promptTokens: IntFieldUpdateOperationsInput - promptVersionSha: StringFieldUpdateOperationsInput - result: NullableStringFieldUpdateOperationsInput - settings: JSON - status: EnumPromptExecutionStatusFieldUpdateOperationsInput - timestamp: DateTimeFieldUpdateOperationsInput - totalCost: FloatFieldUpdateOperationsInput - totalTokens: IntFieldUpdateOperationsInput - variables: JSON -} - -input PromptExecutionUpdateManyWithWhereWithoutPromptInput { - data: PromptExecutionUpdateManyMutationInput! - where: PromptExecutionScalarWhereInput! -} - -input PromptExecutionUpdateManyWithoutPromptInput { - connect: [PromptExecutionWhereUniqueInput!] - connectOrCreate: [PromptExecutionCreateOrConnectWithoutPromptInput!] - create: [PromptExecutionCreateWithoutPromptInput!] - createMany: PromptExecutionCreateManyPromptInputEnvelope - delete: [PromptExecutionWhereUniqueInput!] - deleteMany: [PromptExecutionScalarWhereInput!] - disconnect: [PromptExecutionWhereUniqueInput!] - set: [PromptExecutionWhereUniqueInput!] - update: [PromptExecutionUpdateWithWhereUniqueWithoutPromptInput!] - updateMany: [PromptExecutionUpdateManyWithWhereWithoutPromptInput!] - upsert: [PromptExecutionUpsertWithWhereUniqueWithoutPromptInput!] -} - -input PromptExecutionUpdateWithWhereUniqueWithoutPromptInput { - data: PromptExecutionUpdateWithoutPromptInput! - where: PromptExecutionWhereUniqueInput! -} - -input PromptExecutionUpdateWithoutPromptInput { - completionCost: FloatFieldUpdateOperationsInput - completionTokens: IntFieldUpdateOperationsInput - content: StringFieldUpdateOperationsInput - duration: IntFieldUpdateOperationsInput - error: NullableStringFieldUpdateOperationsInput - id: StringFieldUpdateOperationsInput - interpolatedContent: StringFieldUpdateOperationsInput - promptCost: FloatFieldUpdateOperationsInput - promptTokens: IntFieldUpdateOperationsInput - promptVersionSha: StringFieldUpdateOperationsInput - result: NullableStringFieldUpdateOperationsInput - settings: JSON - status: EnumPromptExecutionStatusFieldUpdateOperationsInput - timestamp: DateTimeFieldUpdateOperationsInput - totalCost: FloatFieldUpdateOperationsInput - totalTokens: IntFieldUpdateOperationsInput - variables: JSON -} - -input PromptExecutionUpsertWithWhereUniqueWithoutPromptInput { - create: PromptExecutionCreateWithoutPromptInput! - update: PromptExecutionUpdateWithoutPromptInput! - where: PromptExecutionWhereUniqueInput! -} - -input PromptExecutionWhereInput { - AND: [PromptExecutionWhereInput!] - NOT: [PromptExecutionWhereInput!] - OR: [PromptExecutionWhereInput!] - completionCost: FloatFilter - completionTokens: IntFilter - content: StringFilter - duration: IntFilter - error: StringNullableFilter - id: StringFilter - interpolatedContent: StringFilter - prompt: PromptRelationFilter - promptCost: FloatFilter - promptId: StringFilter - promptTokens: IntFilter - promptVersionSha: StringFilter - result: StringNullableFilter - settings: JsonFilter - status: EnumPromptExecutionStatusFilter - timestamp: DateTimeFilter - totalCost: FloatFilter - totalTokens: IntFilter - variables: JsonFilter -} - -input PromptExecutionWhereUniqueInput { - id: String -} - -input PromptRelationFilter { - is: PromptWhereInput - isNot: PromptWhereInput -} - -input PromptUpdateInput { - createdAt: DateTimeFieldUpdateOperationsInput - executions: PromptExecutionUpdateManyWithoutPromptInput - id: StringFieldUpdateOperationsInput - integrationId: StringFieldUpdateOperationsInput - name: StringFieldUpdateOperationsInput - organizationId: StringFieldUpdateOperationsInput - promptEnvironments: PromptEnvironmentUpdateManyWithoutPromptInput - updatedAt: DateTimeFieldUpdateOperationsInput - versions: PromptVersionUpdateManyWithoutPromptInput -} - -input PromptUpdateOneRequiredWithoutPromptEnvironmentsInput { - connect: PromptWhereUniqueInput - connectOrCreate: PromptCreateOrConnectWithoutPromptEnvironmentsInput - create: PromptCreateWithoutPromptEnvironmentsInput - update: PromptUpdateWithoutPromptEnvironmentsInput - upsert: PromptUpsertWithoutPromptEnvironmentsInput -} - -input PromptUpdateOneRequiredWithoutVersionsInput { - connect: PromptWhereUniqueInput - connectOrCreate: PromptCreateOrConnectWithoutVersionsInput - create: PromptCreateWithoutVersionsInput - update: PromptUpdateWithoutVersionsInput - upsert: PromptUpsertWithoutVersionsInput -} - -input PromptUpdateWithoutPromptEnvironmentsInput { - createdAt: DateTimeFieldUpdateOperationsInput - executions: PromptExecutionUpdateManyWithoutPromptInput - id: StringFieldUpdateOperationsInput - integrationId: StringFieldUpdateOperationsInput - name: StringFieldUpdateOperationsInput - organizationId: StringFieldUpdateOperationsInput - updatedAt: DateTimeFieldUpdateOperationsInput - versions: PromptVersionUpdateManyWithoutPromptInput -} - -input PromptUpdateWithoutVersionsInput { - createdAt: DateTimeFieldUpdateOperationsInput - executions: PromptExecutionUpdateManyWithoutPromptInput - id: StringFieldUpdateOperationsInput - integrationId: StringFieldUpdateOperationsInput - name: StringFieldUpdateOperationsInput - organizationId: StringFieldUpdateOperationsInput - promptEnvironments: PromptEnvironmentUpdateManyWithoutPromptInput - updatedAt: DateTimeFieldUpdateOperationsInput -} - -input PromptUpsertWithoutPromptEnvironmentsInput { - create: PromptCreateWithoutPromptEnvironmentsInput! - update: PromptUpdateWithoutPromptEnvironmentsInput! -} - -input PromptUpsertWithoutVersionsInput { - create: PromptCreateWithoutVersionsInput! - update: PromptUpdateWithoutVersionsInput! -} - -type PromptVersion { - _count: PromptVersionCount! - content: String! - createdAt: DateTime! - message: String - prompt: Prompt! - promptEnvironments: [PromptEnvironment!] - promptId: String! - settings: JSON! - sha: ID! -} - -type PromptVersionCount { - promptEnvironments: Int! -} - -input PromptVersionCreateManyPromptInput { - content: String! - createdAt: DateTime - message: String - settings: JSON! - sha: String! -} - -input PromptVersionCreateManyPromptInputEnvelope { - data: [PromptVersionCreateManyPromptInput!]! - skipDuplicates: Boolean -} - -input PromptVersionCreateNestedManyWithoutPromptInput { - connect: [PromptVersionWhereUniqueInput!] - connectOrCreate: [PromptVersionCreateOrConnectWithoutPromptInput!] - create: [PromptVersionCreateWithoutPromptInput!] - createMany: PromptVersionCreateManyPromptInputEnvelope -} - -input PromptVersionCreateNestedOneWithoutPromptEnvironmentsInput { - connect: PromptVersionWhereUniqueInput - connectOrCreate: PromptVersionCreateOrConnectWithoutPromptEnvironmentsInput - create: PromptVersionCreateWithoutPromptEnvironmentsInput -} - -input PromptVersionCreateOrConnectWithoutPromptEnvironmentsInput { - create: PromptVersionCreateWithoutPromptEnvironmentsInput! - where: PromptVersionWhereUniqueInput! -} - -input PromptVersionCreateOrConnectWithoutPromptInput { - create: PromptVersionCreateWithoutPromptInput! - where: PromptVersionWhereUniqueInput! -} - -input PromptVersionCreateWithoutPromptEnvironmentsInput { - content: String! - createdAt: DateTime - message: String - prompt: PromptCreateNestedOneWithoutVersionsInput! - settings: JSON! - sha: String! -} - -input PromptVersionCreateWithoutPromptInput { - content: String! - createdAt: DateTime - message: String - promptEnvironments: PromptEnvironmentCreateNestedManyWithoutPromptVersionInput - settings: JSON! - sha: String! -} - -input PromptVersionListRelationFilter { - every: PromptVersionWhereInput - none: PromptVersionWhereInput - some: PromptVersionWhereInput -} - -input PromptVersionRelationFilter { - is: PromptVersionWhereInput - isNot: PromptVersionWhereInput -} - -input PromptVersionScalarWhereInput { - AND: [PromptVersionScalarWhereInput!] - NOT: [PromptVersionScalarWhereInput!] - OR: [PromptVersionScalarWhereInput!] - content: StringFilter - createdAt: DateTimeFilter - message: StringNullableFilter - promptId: StringFilter - settings: JsonFilter - sha: StringFilter -} - -input PromptVersionUpdateManyMutationInput { - content: StringFieldUpdateOperationsInput - createdAt: DateTimeFieldUpdateOperationsInput - message: NullableStringFieldUpdateOperationsInput - settings: JSON - sha: StringFieldUpdateOperationsInput -} - -input PromptVersionUpdateManyWithWhereWithoutPromptInput { - data: PromptVersionUpdateManyMutationInput! - where: PromptVersionScalarWhereInput! -} - -input PromptVersionUpdateManyWithoutPromptInput { - connect: [PromptVersionWhereUniqueInput!] - connectOrCreate: [PromptVersionCreateOrConnectWithoutPromptInput!] - create: [PromptVersionCreateWithoutPromptInput!] - createMany: PromptVersionCreateManyPromptInputEnvelope - delete: [PromptVersionWhereUniqueInput!] - deleteMany: [PromptVersionScalarWhereInput!] - disconnect: [PromptVersionWhereUniqueInput!] - set: [PromptVersionWhereUniqueInput!] - update: [PromptVersionUpdateWithWhereUniqueWithoutPromptInput!] - updateMany: [PromptVersionUpdateManyWithWhereWithoutPromptInput!] - upsert: [PromptVersionUpsertWithWhereUniqueWithoutPromptInput!] -} - -input PromptVersionUpdateOneWithoutPromptEnvironmentsInput { - connect: PromptVersionWhereUniqueInput - connectOrCreate: PromptVersionCreateOrConnectWithoutPromptEnvironmentsInput - create: PromptVersionCreateWithoutPromptEnvironmentsInput - delete: Boolean - disconnect: Boolean - update: PromptVersionUpdateWithoutPromptEnvironmentsInput - upsert: PromptVersionUpsertWithoutPromptEnvironmentsInput -} - -input PromptVersionUpdateWithWhereUniqueWithoutPromptInput { - data: PromptVersionUpdateWithoutPromptInput! - where: PromptVersionWhereUniqueInput! -} - -input PromptVersionUpdateWithoutPromptEnvironmentsInput { - content: StringFieldUpdateOperationsInput - createdAt: DateTimeFieldUpdateOperationsInput - message: NullableStringFieldUpdateOperationsInput - prompt: PromptUpdateOneRequiredWithoutVersionsInput - settings: JSON - sha: StringFieldUpdateOperationsInput -} - -input PromptVersionUpdateWithoutPromptInput { - content: StringFieldUpdateOperationsInput - createdAt: DateTimeFieldUpdateOperationsInput - message: NullableStringFieldUpdateOperationsInput - promptEnvironments: PromptEnvironmentUpdateManyWithoutPromptVersionInput - settings: JSON - sha: StringFieldUpdateOperationsInput -} - -input PromptVersionUpsertWithWhereUniqueWithoutPromptInput { - create: PromptVersionCreateWithoutPromptInput! - update: PromptVersionUpdateWithoutPromptInput! - where: PromptVersionWhereUniqueInput! -} - -input PromptVersionUpsertWithoutPromptEnvironmentsInput { - create: PromptVersionCreateWithoutPromptEnvironmentsInput! - update: PromptVersionUpdateWithoutPromptEnvironmentsInput! -} - -input PromptVersionWhereInput { - AND: [PromptVersionWhereInput!] - NOT: [PromptVersionWhereInput!] - OR: [PromptVersionWhereInput!] - content: StringFilter - createdAt: DateTimeFilter - message: StringNullableFilter - prompt: PromptRelationFilter - promptEnvironments: PromptEnvironmentListRelationFilter - promptId: StringFilter - settings: JsonFilter - sha: StringFilter -} - -input PromptVersionWhereUniqueInput { - sha: String -} - -input PromptWhereInput { - AND: [PromptWhereInput!] - NOT: [PromptWhereInput!] - OR: [PromptWhereInput!] - createdAt: DateTimeFilter - executions: PromptExecutionListRelationFilter - id: StringFilter - integrationId: StringFilter - name: StringFilter - organizationId: StringFilter - promptEnvironments: PromptEnvironmentListRelationFilter - updatedAt: DateTimeFilter - versions: PromptVersionListRelationFilter -} - -input PromptWhereUniqueInput { - id: String -} - -type ProviderApiKey { - createdAt: DateTime! - id: ID! - organization: Organization! - organizationId: String! - provider: String! - updatedAt: DateTime! - value: String! -} - -input PublishPromptInput { - environmentSlug: String! - promptId: String! - promptVersionSha: String! -} - -type Query { - currentApiKey: ApiKey! - deployedPromptVersion(data: GetDeployedPromptVersionInput!): PromptVersion! - deployedPromptVersionWithApiKey(data: GetDeployedPromptVersionInput!): PromptVersion! - environment(data: GetEnvironmentBySlugInput!): Environment! - environments: [Environment!]! - findPrompt(data: FindPromptByNameInput!): Prompt! - findPromptWithApiKey(data: FindPromptByNameInput!): Prompt! - getLatestPrompt(data: PromptWhereUniqueInput!): PromptVersion! - prompt(data: GetPromptInput!): Prompt! - promptExecution(data: PromptExecutionWhereUniqueInput!): PromptExecution! - promptExecutions(data: PromptExecutionWhereInput!): [PromptExecution!]! - promptVersion(data: GetPromptVersionInput!): PromptVersion! - promptVersions(data: PromptWhereUniqueInput!): [PromptVersion!]! - prompts: [Prompt!]! - providerApiKeys: [ProviderApiKey!]! -} - -enum QueryMode { - default - insensitive -} - -input StringFieldUpdateOperationsInput { - set: String -} - -input StringFilter { - contains: String - endsWith: String - equals: String - gt: String - gte: String - in: [String!] - lt: String - lte: String - mode: QueryMode - not: NestedStringFilter - notIn: [String!] - startsWith: String -} - -input StringNullableFilter { - contains: String - endsWith: String - equals: String - gt: String - gte: String - in: [String!] - lt: String - lte: String - mode: QueryMode - not: NestedStringNullableFilter - notIn: [String!] - startsWith: String -} - -input TestPromptInput { - content: String! - integrationId: String! - settings: JSON! - variables: JSON -} - -type User { - _count: UserCount! - createdAt: DateTime! - email: String! - id: ID! - orgMemberships: [OrganizationMember!] - updatedAt: DateTime! -} - -type UserCount { - orgMemberships: Int! -} \ No newline at end of file diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index bb9c54ae..39ae0b0b 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -20,5 +20,19 @@ services: environment: POSTGRES_CONNECTION_URI: postgres://postgres:postgres@postgres:5432/supertokens + influxdb: + image: bitnami/influxdb:2.7.1 + restart: always + environment: + INFLUXDB_ADMIN_USER_TOKEN: "token123" + INFLUXDB_ADMIN_USER_PASSWORD: "influxdb-admin" + INFLUXDB_USER: "admin" + INFLUXDB_USER_PASSWORD: "influxdb-admin" + ports: + - "8086:8086" + volumes: + - influxdb_data:/bitnami/influxdb + volumes: postgres_data: ~ + influxdb_data: ~ diff --git a/docker-compose.yaml b/docker-compose.yaml index e9201c56..9ed165da 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -25,6 +25,19 @@ services: environment: POSTGRES_CONNECTION_URI: postgres://postgres:postgres@postgres:5432/supertokens + influxdb: + image: bitnami/influxdb:2.7.1 + restart: always + environment: + INFLUXDB_ADMIN_USER_TOKEN: "token123" + INFLUXDB_ADMIN_USER_PASSWORD: "influxdb-admin" + INFLUXDB_USER: "admin" + INFLUXDB_USER_PASSWORD: "influxdb-admin" + ports: + - "8086:8086" + volumes: + - influxdb_data:/bitnami/influxdb + pezzo-server: image: ghcr.io/pezzolabs/pezzo/server:latest restart: always @@ -34,6 +47,8 @@ services: environment: - DATABASE_URL=postgres://postgres:postgres@postgres:5432/pezzo - SUPERTOKENS_CONNECTION_URI=http://supertokens:3567 + - INFLUXDB_URL=http://influxdb:8086 + - INFLUXDB_TOKEN=token123 ports: - "3000:3000" depends_on: @@ -59,3 +74,4 @@ services: volumes: postgres_data: ~ + influxdb_data: ~ diff --git a/libs/client/src/client/Pezzo.ts b/libs/client/src/client/Pezzo.ts index 6f0d9f32..518bc74c 100644 --- a/libs/client/src/client/Pezzo.ts +++ b/libs/client/src/client/Pezzo.ts @@ -1,7 +1,6 @@ -import { FIND_PROMPT, GET_DEPLOYED_PROMPT_VERSION } from "../graphql/queries"; +import { GET_DEPLOYED_PROMPT_VERSION } from "../graphql/queries"; import { REPORT_PROMPT_EXECUTION } from "../graphql/mutations"; import { GraphQLClient } from "graphql-request"; -// import { PromptExecution, PromptExecutionCreateInput } from "@pezzo/graphql"; --> This needs to be fixed import { IntegrationBaseSettings, PromptExecutionStatus } from "../types"; export interface PezzoClientOptions { @@ -23,16 +22,6 @@ export class Pezzo { }); } - async findPrompt(name: string) { - const result = await this.gqlClient.request(FIND_PROMPT, { - data: { - name, - }, - }); - - return result.findPromptWithApiKey; - } - async reportPromptExecution( data: unknown, autoParseJSON?: boolean @@ -65,16 +54,25 @@ export class Pezzo { return report; } - async getDeployedPromptVersion(promptId: string) { + async getDeployedPromptVersion(promptName: string) { const result = await this.gqlClient.request(GET_DEPLOYED_PROMPT_VERSION, { data: { - promptId, + name: promptName, + }, + deployedVersionData: { environmentSlug: this.options.environment, }, }); - const { settings, ...rest } = result.deployedPromptVersionWithApiKey; + const prompt = result.findPromptWithApiKey; - return { ...rest, settings: settings as IntegrationBaseSettings }; + return { + id: prompt.id, + deployedVersion: { + sha: prompt.deployedVersion.sha, + content: prompt.deployedVersion.content, + settings: prompt.deployedVersion.settings as IntegrationBaseSettings, + }, + }; } } diff --git a/libs/client/src/graphql/queries.ts b/libs/client/src/graphql/queries.ts index b1a407ef..c63d05c2 100644 --- a/libs/client/src/graphql/queries.ts +++ b/libs/client/src/graphql/queries.ts @@ -1,30 +1,18 @@ import { graphql } from "../@generated/graphql"; -export const GET_PROMPT_VERSION = graphql(/* GraphQL */ ` - query getPromptVersion($data: GetPromptVersionInput!) { - promptVersion(data: $data) { - sha - content - settings - } - } -`); - -export const FIND_PROMPT = graphql(/* GraphQL */ ` - query findPrompt($data: FindPromptByNameInput!) { +export const GET_DEPLOYED_PROMPT_VERSION = graphql(/* GraphQL */ ` + query findPromptWithDeployedVersion( + $data: FindPromptByNameInput! + $deployedVersionData: ResolveDeployedVersionInput! + ) { findPromptWithApiKey(data: $data) { id name - } - } -`); - -export const GET_DEPLOYED_PROMPT_VERSION = graphql(/* GraphQL */ ` - query deployedPromptVersion($data: GetDeployedPromptVersionInput!) { - deployedPromptVersionWithApiKey(data: $data) { - sha - content - settings + deployedVersion(data: $deployedVersionData) { + sha + content + settings + } } } `); diff --git a/libs/common/src/utils/index.ts b/libs/common/src/utils/index.ts index 8349277e..0b41fddb 100644 --- a/libs/common/src/utils/index.ts +++ b/libs/common/src/utils/index.ts @@ -1 +1,2 @@ export * from "./interpolate-variables"; +export * from "./slugify"; diff --git a/libs/common/src/utils/slugify.ts b/libs/common/src/utils/slugify.ts new file mode 100644 index 00000000..3954900d --- /dev/null +++ b/libs/common/src/utils/slugify.ts @@ -0,0 +1,9 @@ +export function slugify(str: string) { + str = str.replace(/^\s+|\s+$/g, ""); // trim leading/trailing white space + str = str.toLowerCase(); // convert string to lowercase + str = str + .replace(/[^a-z0-9 -]/g, "") // remove any non-alphanumeric characters + .replace(/\s+/g, "-") // replace spaces with hyphens + .replace(/-+/g, "-"); // remove consecutive hyphens + return str; +} diff --git a/libs/common/src/version.json b/libs/common/src/version.json index ad233e5e..36fa2c4f 100644 --- a/libs/common/src/version.json +++ b/libs/common/src/version.json @@ -1 +1 @@ -{"version":"0.0.16-alpha"} \ No newline at end of file +{"version":"0.0.17-alpha"} \ No newline at end of file diff --git a/libs/integrations/src/lib/integrations/base-executor.ts b/libs/integrations/src/lib/integrations/base-executor.ts index 692d4b6c..81869418 100644 --- a/libs/integrations/src/lib/integrations/base-executor.ts +++ b/libs/integrations/src/lib/integrations/base-executor.ts @@ -38,29 +38,13 @@ export abstract class BaseExecutor { abstract execute(props: ExecuteProps): Promise>; - private getPrompt(promptName: string) { - try { - return this.pezzoClient.findPrompt(promptName); - } catch (error) { - throw new PezzoClientError(error.message, error); - } - } - - private getPromptVersion(promptId: string) { - try { - return this.pezzoClient.getDeployedPromptVersion(promptId); - } catch (error) { - throw new PezzoClientError(error.message, error); - } - } - async run( promptName: string, variables: Record = {}, options: ExecuteOptions = {} ) { - const prompt = await this.getPrompt(promptName); - const promptVersion = await this.getPromptVersion(prompt.id); + const prompt = await this.pezzoClient.getDeployedPromptVersion(promptName); + const promptVersion = prompt.deployedVersion; const { settings, content } = promptVersion; const interpolatedContent = interpolateVariables(content, variables); const start = performance.now(); @@ -95,6 +79,7 @@ export abstract class BaseExecutor { status ); } + return this.pezzoClient.reportPromptExecution( { prompt: promptConnect, @@ -121,7 +106,7 @@ export abstract class BaseExecutor { options.autoParseJSON ); } catch (e) { - this.pezzoClient.reportPromptExecution({ + await this.pezzoClient.reportPromptExecution({ prompt: promptConnect, promptVersionSha: promptVersion.sha, status: "Error", diff --git a/package-lock.json b/package-lock.json index e75dd9d1..2da26230 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pezzo", - "version": "0.0.11-alpha", + "version": "0.0.17-alpha", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "pezzo", - "version": "0.0.11-alpha", + "version": "0.0.17-alpha", "license": "MIT", "dependencies": { "@ant-design/icons": "^5.0.1", @@ -17,6 +17,7 @@ "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", "@heroicons/react": "^2.0.17", + "@influxdata/influxdb-client": "^1.33.2", "@nestjs/apollo": "^11.0.5", "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.3.1", @@ -34,6 +35,8 @@ "apollo-server-express": "^3.12.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "date-fns": "^2.30.0", + "googleapis": "^118.0.0", "graphql-request": "^6.0.0", "graphql-type-json": "^0.3.2", "joi": "^17.9.1", @@ -4420,6 +4423,11 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "devOptional": true }, + "node_modules/@influxdata/influxdb-client": { + "version": "1.33.2", + "resolved": "https://registry.npmjs.org/@influxdata/influxdb-client/-/influxdb-client-1.33.2.tgz", + "integrity": "sha512-RT5SxH+grHAazo/YK3UTuWK/frPWRM0N7vkrCUyqVprDgQzlLP+bSK4ak2Jv3QVF/pazTnsxWjvtKZdwskV5Xw==" + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -10856,6 +10864,14 @@ "get-intrinsic": "^1.1.3" } }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "engines": { + "node": ">=8" + } + }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -11481,6 +11497,14 @@ "node": "*" } }, + "node_modules/bignumber.js": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", + "engines": { + "node": "*" + } + }, "node_modules/bin-check": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bin-check/-/bin-check-4.1.0.tgz", @@ -13384,6 +13408,21 @@ "integrity": "sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==", "dev": true }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/dayjs": { "version": "1.11.7", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", @@ -15137,8 +15176,7 @@ "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "node_modules/external-editor": { "version": "3.1.0", @@ -15263,6 +15301,11 @@ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, + "node_modules/fast-text-encoding": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==" + }, "node_modules/fast-url-parser": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", @@ -15889,6 +15932,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.0.tgz", + "integrity": "sha512-aezGIjb+/VfsJtIcHGcBSerNEDdfdHeMros+RbYbGpmonKWQCOVOes0LVZhn1lDtIgq55qq0HaxymIoae3Fl/A==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gcp-metadata": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.2.0.tgz", + "integrity": "sha512-aFhhvvNycky2QyhG+dcfEdHBF0FRbYcf39s6WNHUDysKSrbJ5vuFbjydxBcmewtXeV248GP8dWT3ByPNxsyHCw==", + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/generate-package-json-webpack-plugin": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/generate-package-json-webpack-plugin/-/generate-package-json-webpack-plugin-2.6.0.tgz", @@ -16085,6 +16154,110 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-auth-library": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.8.0.tgz", + "integrity": "sha512-0iJn7IDqObDG5Tu9Tn2WemmJ31ksEa96IyK0J0OZCpTh6CrC6FrattwKX87h3qKVuprCJpdOGKc1Xi8V0kMh8Q==", + "dependencies": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.2.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/google-auth-library/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-auth-library/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-auth-library/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/google-auth-library/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "dependencies": { + "node-forge": "^1.3.1" + }, + "bin": { + "gp12-pem": "build/src/bin/gp12-pem.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/googleapis": { + "version": "118.0.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-118.0.0.tgz", + "integrity": "sha512-Ny6zJOGn5P/YDT6GQbJU6K0lSzEu4Yuxnsn45ZgBIeSQ1RM0FolEjUToLXquZd89DU9wUfqA5XYHPEctk1TFWg==", + "dependencies": { + "google-auth-library": "^8.0.2", + "googleapis-common": "^6.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/googleapis-common": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-6.0.4.tgz", + "integrity": "sha512-m4ErxGE8unR1z0VajT6AYk3s6a9gIMM6EkDZfkPnES8joeOlEtFEJeF8IyZkb0tjPXkktUfYrE4b3Li1DNyOwA==", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^5.0.1", + "google-auth-library": "^8.0.2", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/googleapis-common/node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -16254,6 +16427,38 @@ "graphql": ">=0.11 <=16" } }, + "node_modules/gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "dependencies": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/gtoken/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/gtoken/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -21275,6 +21480,14 @@ "node": ">=4" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -27891,6 +28104,11 @@ "requires-port": "^1.0.0" } }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" + }, "node_modules/urlpattern-polyfill": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-7.0.0.tgz", @@ -31952,6 +32170,11 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "devOptional": true }, + "@influxdata/influxdb-client": { + "version": "1.33.2", + "resolved": "https://registry.npmjs.org/@influxdata/influxdb-client/-/influxdb-client-1.33.2.tgz", + "integrity": "sha512-RT5SxH+grHAazo/YK3UTuWK/frPWRM0N7vkrCUyqVprDgQzlLP+bSK4ak2Jv3QVF/pazTnsxWjvtKZdwskV5Xw==" + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -36836,6 +37059,11 @@ "get-intrinsic": "^1.1.3" } }, + "arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" + }, "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -37317,6 +37545,11 @@ "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" }, + "bignumber.js": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==" + }, "bin-check": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bin-check/-/bin-check-4.1.0.tgz", @@ -38777,6 +39010,14 @@ "integrity": "sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==", "dev": true }, + "date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "requires": { + "@babel/runtime": "^7.21.0" + } + }, "dayjs": { "version": "1.11.7", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", @@ -40119,8 +40360,7 @@ "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "external-editor": { "version": "3.1.0", @@ -40221,6 +40461,11 @@ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, + "fast-text-encoding": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", + "integrity": "sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==" + }, "fast-url-parser": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", @@ -40686,6 +40931,26 @@ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true }, + "gaxios": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.0.tgz", + "integrity": "sha512-aezGIjb+/VfsJtIcHGcBSerNEDdfdHeMros+RbYbGpmonKWQCOVOes0LVZhn1lDtIgq55qq0HaxymIoae3Fl/A==", + "requires": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.7" + } + }, + "gcp-metadata": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.2.0.tgz", + "integrity": "sha512-aFhhvvNycky2QyhG+dcfEdHBF0FRbYcf39s6WNHUDysKSrbJ5vuFbjydxBcmewtXeV248GP8dWT3ByPNxsyHCw==", + "requires": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + } + }, "generate-package-json-webpack-plugin": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/generate-package-json-webpack-plugin/-/generate-package-json-webpack-plugin-2.6.0.tgz", @@ -40828,6 +41093,93 @@ "slash": "^4.0.0" } }, + "google-auth-library": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-8.8.0.tgz", + "integrity": "sha512-0iJn7IDqObDG5Tu9Tn2WemmJ31ksEa96IyK0J0OZCpTh6CrC6FrattwKX87h3qKVuprCJpdOGKc1Xi8V0kMh8Q==", + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^5.0.0", + "gcp-metadata": "^5.2.0", + "gtoken": "^6.1.0", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + }, + "dependencies": { + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, + "google-p12-pem": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-4.0.1.tgz", + "integrity": "sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==", + "requires": { + "node-forge": "^1.3.1" + } + }, + "googleapis": { + "version": "118.0.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-118.0.0.tgz", + "integrity": "sha512-Ny6zJOGn5P/YDT6GQbJU6K0lSzEu4Yuxnsn45ZgBIeSQ1RM0FolEjUToLXquZd89DU9wUfqA5XYHPEctk1TFWg==", + "requires": { + "google-auth-library": "^8.0.2", + "googleapis-common": "^6.0.0" + } + }, + "googleapis-common": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-6.0.4.tgz", + "integrity": "sha512-m4ErxGE8unR1z0VajT6AYk3s6a9gIMM6EkDZfkPnES8joeOlEtFEJeF8IyZkb0tjPXkktUfYrE4b3Li1DNyOwA==", + "requires": { + "extend": "^3.0.2", + "gaxios": "^5.0.1", + "google-auth-library": "^8.0.2", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^9.0.0" + }, + "dependencies": { + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + } + } + }, "gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -40950,6 +41302,37 @@ "integrity": "sha512-umt4f5NnMK46ChM2coO36PTFhHouBrK9stWWBczERguwYrGnPNxJ9dimU6IyOBfOkC6Izhkg4H8+F51W/8CYDg==", "requires": {} }, + "gtoken": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-6.1.2.tgz", + "integrity": "sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==", + "requires": { + "gaxios": "^5.0.1", + "google-p12-pem": "^4.0.0", + "jws": "^4.0.0" + }, + "dependencies": { + "jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "requires": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + } + } + }, "handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -44821,6 +45204,14 @@ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, "json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -49595,6 +49986,11 @@ "requires-port": "^1.0.0" } }, + "url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" + }, "urlpattern-polyfill": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-7.0.0.tgz", diff --git a/package.json b/package.json index 7cbd2b86..def8dbd3 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", "@heroicons/react": "^2.0.17", + "@influxdata/influxdb-client": "^1.33.2", "@nestjs/apollo": "^11.0.5", "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.3.1", @@ -30,6 +31,8 @@ "apollo-server-express": "^3.12.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "date-fns": "^2.30.0", + "googleapis": "^118.0.0", "graphql-request": "^6.0.0", "graphql-type-json": "^0.3.2", "joi": "^17.9.1",