Add multi-tenant system

Signed-off-by: gearnode <bryan@frimin.fr>
This commit is contained in:
gearnode
2025-02-25 22:52:06 +01:00
parent 29ce55e89c
commit 0c176674a0
35 changed files with 1254 additions and 686 deletions

View File

@@ -12,6 +12,7 @@ import AuthLayout from "./layouts/AuthLayout";
import { RelayEnvironment } from "./RelayEnvironment";
import { AuthProvider } from "./contexts/AuthContext";
import { ProtectedRoute } from "./components/ProtectedRoute";
import { OrganizationProvider } from "./contexts/OrganizationContext";
posthog.init(process.env.POSTHOG_KEY!, {
api_host: process.env.POSTHOG_HOST,
@@ -48,55 +49,14 @@ function App() {
<HelmetProvider>
<BrowserRouter>
<AuthProvider>
<Routes>
{/* Authentication Routes - Accessible without login */}
<Route
path="/login"
element={
<ErrorBoundaryWithLocation>
<AuthLayout />
</ErrorBoundaryWithLocation>
}
>
<OrganizationProvider>
<Routes>
{/* Authentication Routes - Accessible without login */}
<Route
index
element={
<Suspense>
<ErrorBoundaryWithLocation>
<LoginPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
</Route>
<Route
path="/register"
element={
<ErrorBoundaryWithLocation>
<AuthLayout />
</ErrorBoundaryWithLocation>
}
>
<Route
index
element={
<Suspense>
<ErrorBoundaryWithLocation>
<RegisterPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
</Route>
{/* Protected Routes - Require authentication */}
<Route element={<ProtectedRoute />}>
<Route
path="/*"
path="/login"
element={
<ErrorBoundaryWithLocation>
<ConsoleLayout />
<AuthLayout />
</ErrorBoundaryWithLocation>
}
>
@@ -105,112 +65,155 @@ function App() {
element={
<Suspense>
<ErrorBoundaryWithLocation>
<HomePage />
<LoginPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route
path="peoples"
element={
<Suspense>
<ErrorBoundaryWithLocation>
<PeopleListPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route
path="peoples/create"
element={
<Suspense>
<ErrorBoundaryWithLocation>
<CreatePeoplePage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route
path="peoples/:peopleId"
element={
<Suspense>
<ErrorBoundaryWithLocation>
<PeopleOverviewPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route
path="vendors"
element={
<Suspense>
<ErrorBoundaryWithLocation>
<VendorListPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route
path="frameworks"
element={
<Suspense>
<ErrorBoundaryWithLocation>
<FrameworkListPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route
path="frameworks/:frameworkId"
element={
<Suspense>
<ErrorBoundaryWithLocation>
<FrameworkOverviewPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route
path="frameworks/:frameworkId/controls/:controlId"
element={
<Suspense>
<ErrorBoundaryWithLocation>
<ControlOverviewPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route
path="vendors/:vendorId"
element={
<Suspense>
<ErrorBoundaryWithLocation>
<VendorOverviewPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route
path="settings"
element={
<Suspense>
<ErrorBoundaryWithLocation>
<SettingsPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route
path="*"
element={
<Suspense>
<NotFoundPage />
</Suspense>
}
/>
</Route>
</Route>
</Routes>
<Route
path="/register"
element={
<ErrorBoundaryWithLocation>
<AuthLayout />
</ErrorBoundaryWithLocation>
}
>
<Route
index
element={
<Suspense>
<ErrorBoundaryWithLocation>
<RegisterPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
</Route>
{/* Protected Routes - Require authentication */}
<Route element={<ProtectedRoute />}>
<Route
path="/*"
element={
<ErrorBoundaryWithLocation>
<ConsoleLayout />
</ErrorBoundaryWithLocation>
}
>
<Route
index
element={
<Suspense>
<ErrorBoundaryWithLocation>
<HomePage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route
path="peoples"
element={
<Suspense>
<ErrorBoundaryWithLocation>
<PeopleListPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route
path="peoples/create"
element={
<Suspense>
<ErrorBoundaryWithLocation>
<CreatePeoplePage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route
path="peoples/:peopleId"
element={
<Suspense>
<ErrorBoundaryWithLocation>
<PeopleOverviewPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route
path="vendors"
element={
<Suspense>
<ErrorBoundaryWithLocation>
<VendorListPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route
path="frameworks"
element={
<Suspense>
<ErrorBoundaryWithLocation>
<FrameworkListPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route
path="frameworks/:frameworkId"
element={
<Suspense>
<ErrorBoundaryWithLocation>
<FrameworkOverviewPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route
path="frameworks/:frameworkId/controls/:controlId"
element={
<Suspense>
<ErrorBoundaryWithLocation>
<ControlOverviewPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route
path="vendors/:vendorId"
element={
<Suspense>
<ErrorBoundaryWithLocation>
<VendorOverviewPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route
path="settings"
element={
<Suspense>
<ErrorBoundaryWithLocation>
<SettingsPage />
</ErrorBoundaryWithLocation>
</Suspense>
}
/>
<Route
path="*"
element={
<Suspense>
<NotFoundPage />
</Suspense>
}
/>
</Route>
</Route>
</Routes>
</OrganizationProvider>
</AuthProvider>
</BrowserRouter>
</HelmetProvider>

View File

@@ -30,7 +30,7 @@ import {
SidebarMenuItem,
} from "@/components/ui/sidebar";
import type { AppSidebarQuery as AppSidebarQueryType } from "./__generated__/AppSidebarQuery.graphql";
import { TeamSwitcher } from "@/components/TeamSwitcher";
const staticData = {
user: {
name: "shadcn",
@@ -84,12 +84,8 @@ const AppSidebarQuery = graphql`
query AppSidebarQuery {
viewer {
id
organization {
name
logoUrl
createdAt
updatedAt
}
...TeamSwitcher_organizations
...NavUser_viewer
}
}
`;
@@ -101,37 +97,18 @@ function AppSidebarContent({
queryRef: PreloadedQuery<AppSidebarQueryType>;
}) {
const data = usePreloadedQuery(AppSidebarQuery, queryRef);
const organization = data.viewer.organization;
return (
<Sidebar variant="inset" {...props}>
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" asChild>
<a href="#">
<img
src={organization.logoUrl}
alt={organization.name}
className="size-8 rounded-lg object-cover"
/>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">
{organization.name}
</span>
<span className="truncate text-xs">Enterprise</span>
</div>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
<TeamSwitcher organizations={data.viewer} />
</SidebarHeader>
<SidebarContent>
<NavMain items={staticData.navMain} />
<NavSecondary items={staticData.navSecondary} className="mt-auto" />
</SidebarContent>
<SidebarFooter>
<NavUser user={staticData.user} />
<NavUser viewer={data.viewer} />
</SidebarFooter>
</Sidebar>
);
@@ -162,7 +139,23 @@ function AppSidebarFallback(props: React.ComponentProps<typeof Sidebar>) {
<NavSecondary items={staticData.navSecondary} className="mt-auto" />
</SidebarContent>
<SidebarFooter>
<NavUser user={staticData.user} />
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" asChild>
<a href="#">
<div className="h-8 w-8 rounded-lg bg-muted animate-pulse" />
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">
<div className="h-4 w-24 animate-pulse rounded bg-muted" />
</span>
<span className="truncate text-xs">
<div className="h-3 w-32 animate-pulse rounded bg-muted" />
</span>
</div>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
</Sidebar>
);

View File

@@ -8,6 +8,7 @@ import {
LogOut,
Sparkles,
} from "lucide-react";
import { graphql, useFragment } from "react-relay";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
@@ -25,17 +26,22 @@ import {
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
import {
NavUser_viewer$key,
NavUser_viewer$data,
} from "./__generated__/NavUser_viewer.graphql";
export function NavUser({
user,
}: {
user: {
name: string;
email: string;
avatar: string;
};
}) {
export const navUserFragment = graphql`
fragment NavUser_viewer on User {
id
fullName
email
}
`;
export function NavUser({ viewer }: { viewer: NavUser_viewer$key }) {
const { isMobile } = useSidebar();
const currentUser = useFragment(navUserFragment, viewer);
return (
<SidebarMenu>
@@ -47,12 +53,18 @@ export function NavUser({
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
<AvatarFallback className="rounded-lg">
{currentUser.fullName.substring(0, 2).toUpperCase()}
</AvatarFallback>
<AvatarFallback className="rounded-lg">
{currentUser.fullName.substring(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
<span className="truncate font-semibold">
{currentUser.fullName}
</span>
<span className="truncate text-xs">{currentUser.email}</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</SidebarMenuButton>
@@ -66,12 +78,18 @@ export function NavUser({
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
<AvatarFallback className="rounded-lg">
{currentUser.fullName.substring(0, 2).toUpperCase()}
</AvatarFallback>
<AvatarFallback className="rounded-lg">
{currentUser.fullName.substring(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
<span className="truncate font-semibold">
{currentUser.fullName}
</span>
<span className="truncate text-xs">{currentUser.email}</span>
</div>
</div>
</DropdownMenuLabel>

View File

@@ -0,0 +1,139 @@
"use client";
import { useState, useEffect } from "react";
import { ChevronsUpDown, Plus } from "lucide-react";
import { graphql, useFragment } from "react-relay";
import { useOrganization } from "@/contexts/OrganizationContext";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar";
import {
TeamSwitcher_organizations$key,
TeamSwitcher_organizations$data,
} from "./__generated__/TeamSwitcher_organizations.graphql";
export const teamSwitcherFragment = graphql`
fragment TeamSwitcher_organizations on User {
organizations {
id
name
logoUrl
}
}
`;
// Extended type to include plan field until Relay compiler generates the types
type Organization = TeamSwitcher_organizations$data["organizations"][0] & {
plan?: string;
};
export function TeamSwitcher({
organizations,
}: {
organizations: TeamSwitcher_organizations$key;
}) {
const { isMobile } = useSidebar();
const data = useFragment(teamSwitcherFragment, organizations);
const { currentOrganization, setCurrentOrganization } = useOrganization();
useEffect(() => {
if (
data.organizations &&
data.organizations.length > 0 &&
!currentOrganization
) {
// Type assertion to include plan field
setCurrentOrganization(data.organizations[0] as Organization);
}
}, [data.organizations, currentOrganization, setCurrentOrganization]);
// If no organizations or data is still loading
if (!currentOrganization) {
return null;
}
// Convert logoUrl to a React component
const LogoComponent = ({
org,
className,
}: {
org: Organization;
className?: string;
}) => {
if (org.logoUrl) {
return <img src={org.logoUrl} alt={org.name} className={className} />;
}
return null;
};
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<LogoComponent org={currentOrganization} className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">
{currentOrganization.name}
</span>
<span className="truncate text-xs">Free</span>
</div>
<ChevronsUpDown className="ml-auto" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
align="start"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenuLabel className="text-xs text-muted-foreground">
Organizations
</DropdownMenuLabel>
{data.organizations.map((org: Organization, index: number) => (
<DropdownMenuItem
key={org.id}
onClick={() => setCurrentOrganization(org)}
className="gap-2 p-2"
>
<div className="flex size-6 items-center justify-center rounded-sm border">
<LogoComponent org={org} className="size-4 shrink-0" />
</div>
{org.name}
<DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem className="gap-2 p-2">
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
<Plus className="size-4" />
</div>
<div className="font-medium text-muted-foreground">
Add organization
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
);
}

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<717a5c6c939c65b7e0790f84e2c41bcd>>
* @generated SignedSource<<d060c85c907876f442133c643b917eee>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -9,16 +9,12 @@
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type AppSidebarQuery$variables = Record<PropertyKey, never>;
export type AppSidebarQuery$data = {
readonly viewer: {
readonly id: string;
readonly organization: {
readonly createdAt: any;
readonly logoUrl: string;
readonly name: string;
readonly updatedAt: any;
};
readonly " $fragmentSpreads": FragmentRefs<"NavUser_viewer" | "TeamSwitcher_organizations">;
};
};
export type AppSidebarQuery = {
@@ -33,34 +29,6 @@ var v0 = {
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v1 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoUrl",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
};
return {
"fragment": {
@@ -79,19 +47,14 @@ return {
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "organization",
"plural": false,
"selections": [
(v1/*: any*/),
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/)
],
"storageKey": null
"kind": "FragmentSpread",
"name": "TeamSwitcher_organizations"
},
{
"args": null,
"kind": "FragmentSpread",
"name": "NavUser_viewer"
}
],
"storageKey": null
@@ -120,16 +83,40 @@ return {
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "organization",
"plural": false,
"name": "organizations",
"plural": true,
"selections": [
(v1/*: any*/),
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
(v0/*: any*/)
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoUrl",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"storageKey": null
}
],
"storageKey": null
@@ -137,16 +124,16 @@ return {
]
},
"params": {
"cacheID": "e50b9b44daf91d53b07f8912e6c2088f",
"cacheID": "48137f101a20a5ce116cbf46c2a8cd63",
"id": null,
"metadata": {},
"name": "AppSidebarQuery",
"operationKind": "query",
"text": "query AppSidebarQuery {\n viewer {\n id\n organization {\n name\n logoUrl\n createdAt\n updatedAt\n id\n }\n }\n}\n"
"text": "query AppSidebarQuery {\n viewer {\n id\n ...TeamSwitcher_organizations\n ...NavUser_viewer\n }\n}\n\nfragment NavUser_viewer on User {\n id\n fullName\n email\n}\n\nfragment TeamSwitcher_organizations on User {\n organizations {\n id\n name\n logoUrl\n }\n}\n"
}
};
})();
(node as any).hash = "d450e65862def2f7ebd6f4b8d0475be0";
(node as any).hash = "d5e28d934b5913169dd822042f4eca07";
export default node;

View File

@@ -0,0 +1,58 @@
/**
* @generated SignedSource<<0965f25c8dedb59a9316e4df4d773af6>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type NavUser_viewer$data = {
readonly email: string;
readonly fullName: string;
readonly id: string;
readonly " $fragmentType": "NavUser_viewer";
};
export type NavUser_viewer$key = {
readonly " $data"?: NavUser_viewer$data;
readonly " $fragmentSpreads": FragmentRefs<"NavUser_viewer">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "NavUser_viewer",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "fullName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"storageKey": null
}
],
"type": "User",
"abstractKey": null
};
(node as any).hash = "7ee85a272f543b514c5a6a3148fd1613";
export default node;

View File

@@ -0,0 +1,71 @@
/**
* @generated SignedSource<<8ecb9304d0a498d483d1a8c251f8c49a>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type TeamSwitcher_organizations$data = {
readonly organizations: ReadonlyArray<{
readonly id: string;
readonly logoUrl: string;
readonly name: string;
}>;
readonly " $fragmentType": "TeamSwitcher_organizations";
};
export type TeamSwitcher_organizations$key = {
readonly " $data"?: TeamSwitcher_organizations$data;
readonly " $fragmentSpreads": FragmentRefs<"TeamSwitcher_organizations">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "TeamSwitcher_organizations",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "organizations",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoUrl",
"storageKey": null
}
],
"storageKey": null
}
],
"type": "User",
"abstractKey": null
};
(node as any).hash = "318f68b82b5b87500ec99ffcdd2929ab";
export default node;

View File

@@ -9,6 +9,8 @@ interface AuthContextType {
logout: () => Promise<void>;
}
// TODO: should be remove when backend returns proper authenticated error
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: React.ReactNode }) {

View File

@@ -0,0 +1,43 @@
import React, { createContext, useContext, useState, ReactNode } from "react";
interface Organization {
id: string;
name: string;
logoUrl: string;
}
interface OrganizationContextType {
currentOrganization: Organization | null;
setCurrentOrganization: (organization: Organization) => void;
}
const OrganizationContext = createContext<OrganizationContextType | undefined>(
undefined
);
export function useOrganization() {
const context = useContext(OrganizationContext);
if (context === undefined) {
throw new Error(
"useOrganization must be used within an OrganizationProvider"
);
}
return context;
}
interface OrganizationProviderProps {
children: ReactNode;
}
export function OrganizationProvider({ children }: OrganizationProviderProps) {
const [currentOrganization, setCurrentOrganization] =
useState<Organization | null>(null);
return (
<OrganizationContext.Provider
value={{ currentOrganization, setCurrentOrganization }}
>
{children}
</OrganizationContext.Provider>
);
}

View File

@@ -24,8 +24,7 @@ const createPeoplePageQuery = graphql`
query CreatePeoplePageQuery {
viewer {
id
organization {
organizations {
id
name
}

View File

@@ -9,12 +9,12 @@ import { Card, CardContent } from "@/components/ui/card";
import { Link } from "react-router";
import type { FrameworkListPageQuery as FrameworkListPageQueryType } from "./__generated__/FrameworkListPageQuery.graphql";
import { Helmet } from "react-helmet-async";
import { useOrganization } from "@/contexts/OrganizationContext";
const FrameworkListPageQuery = graphql`
query FrameworkListPageQuery {
viewer {
id
organization {
query FrameworkListPageQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
... on Organization {
frameworks {
edges {
node {
@@ -87,7 +87,7 @@ function FrameworkListPageContent({
}) {
const data = usePreloadedQuery(FrameworkListPageQuery, queryRef);
const frameworks =
data.viewer.organization.frameworks?.edges.map((edge) => edge?.node) ?? [];
data.organization.frameworks?.edges.map((edge) => edge?.node) ?? [];
return (
<div className="space-y-6">
@@ -164,9 +164,11 @@ export default function FrameworkListPage() {
FrameworkListPageQuery
);
const { currentOrganization } = useOrganization();
useEffect(() => {
loadQuery({});
}, [loadQuery]);
loadQuery({ organizationId: currentOrganization!.id });
}, [loadQuery, currentOrganization]);
return (
<>

View File

@@ -12,7 +12,7 @@ export const HomePageQuery = graphql`
query HomePageQuery {
viewer {
id
organization {
organizations {
name
createdAt
updatedAt

View File

@@ -18,28 +18,34 @@ import type { PeopleListPageQuery as PeopleListPageQueryType } from "./__generat
import type { PeopleListPageDeletePeopleMutation } from "./__generated__/PeopleListPageDeletePeopleMutation.graphql";
import { PeopleListPagePaginationQuery } from "./__generated__/PeopleListPagePaginationQuery.graphql";
import { PeopleListPage_peoples$key } from "./__generated__/PeopleListPage_peoples.graphql";
import { useOrganization } from "@/contexts/OrganizationContext";
const ITEMS_PER_PAGE = 25;
const peopleListPageQuery = graphql`
query PeopleListPageQuery(
$organizationId: ID!
$first: Int
$after: CursorKey
$last: Int
$before: CursorKey
) {
viewer {
id
organization {
...PeopleListPage_peoples
}
organization: node(id: $organizationId) {
...PeopleListPage_peoples
@arguments(first: $first, after: $after, last: $last, before: $before)
}
}
`;
const peopleListFragment = graphql`
fragment PeopleListPage_peoples on Organization
@refetchable(queryName: "PeopleListPagePaginationQuery") {
@refetchable(queryName: "PeopleListPagePaginationQuery")
@argumentDefinitions(
first: { type: "Int" }
after: { type: "CursorKey" }
last: { type: "Int" }
before: { type: "CursorKey" }
) {
id
peoples(first: $first, after: $after, last: $last, before: $before)
@connection(key: "PeopleListPage_peoples") {
@@ -147,7 +153,7 @@ function PeopleListContent({
} = usePaginationFragment<
PeopleListPagePaginationQuery,
PeopleListPage_peoples$key
>(peopleListFragment, data.viewer.organization);
>(peopleListFragment, data.organization);
const peoples =
peoplesConnection.peoples.edges.map((edge) => edge.node) ?? [];
@@ -301,17 +307,20 @@ export default function PeopleListPage() {
const [queryRef, loadQuery] =
useQueryLoader<PeopleListPageQueryType>(peopleListPageQuery);
const { currentOrganization } = useOrganization();
useEffect(() => {
const after = searchParams.get("after");
const before = searchParams.get("before");
loadQuery({
organizationId: currentOrganization!.id,
first: before ? undefined : ITEMS_PER_PAGE,
after: after || undefined,
last: before ? ITEMS_PER_PAGE : undefined,
before: before || undefined,
});
}, [loadQuery]);
}, [loadQuery, currentOrganization]);
if (!queryRef) {
return <PeopleListPageFallback />;

View File

@@ -28,7 +28,7 @@ const settingsPageQuery = graphql`
query SettingsPageQuery {
viewer {
id
organization {
organizations {
name
logoUrl
}
@@ -49,7 +49,7 @@ function SettingsPageContent({
queryRef: PreloadedQuery<SettingsPageQueryType>;
}) {
const data = usePreloadedQuery(settingsPageQuery, queryRef);
const organization = data.viewer.organization;
const organization = data.viewer.organizations[0];
const members: Member[] = [];
return (

View File

@@ -22,28 +22,35 @@ import { VendorListPageDeleteVendorMutation } from "./__generated__/VendorListPa
import { toast } from "@/hooks/use-toast";
import { VendorListPagePaginationQuery } from "./__generated__/VendorListPagePaginationQuery.graphql";
import { VendorListPage_vendors$key } from "./__generated__/VendorListPage_vendors.graphql";
import { useToast } from "@/hooks/use-toast";
import { useOrganization } from "@/contexts/OrganizationContext";
const ITEMS_PER_PAGE = 25;
const vendorListPageQuery = graphql`
query VendorListPageQuery(
$organizationId: ID!
$first: Int
$after: CursorKey
$last: Int
$before: CursorKey
) {
viewer {
id
organization {
...VendorListPage_vendors
}
organization: node(id: $organizationId) {
...VendorListPage_vendors
@arguments(first: $first, after: $after, last: $last, before: $before)
}
}
`;
const vendorListFragment = graphql`
fragment VendorListPage_vendors on Organization
@refetchable(queryName: "VendorListPagePaginationQuery") {
@refetchable(queryName: "VendorListPagePaginationQuery")
@argumentDefinitions(
first: { type: "Int" }
after: { type: "CursorKey" }
last: { type: "Int" }
before: { type: "CursorKey" }
) {
id
vendors(first: $first, after: $after, last: $last, before: $before)
@connection(key: "VendorListPage_vendors") {
@@ -96,8 +103,15 @@ const deleteVendorMutation = graphql`
}
`;
// Define a proper type for the vendor items
interface VendorItem {
id: string;
name: string;
createdAt: string;
}
// TODO: Remove this once we have a real list of vendors
const vendorsList = [
const vendorsList: VendorItem[] = [
{ id: "1", name: "Amazon Web Services", createdAt: new Date().toISOString() },
{
id: "2",
@@ -172,6 +186,7 @@ function VendorListContent({
}: {
queryRef: PreloadedQuery<VendorListPageQueryType>;
}) {
const { toast } = useToast();
const data = usePreloadedQuery<VendorListPageQueryType>(
vendorListPageQuery,
queryRef
@@ -179,7 +194,7 @@ function VendorListContent({
const [searchParams, setSearchParams] = useSearchParams();
const [isPending, startTransition] = useTransition();
const [searchTerm, setSearchTerm] = useState("");
const [filteredVendors, setFilteredVendors] = useState<Array<any>>([]);
const [filteredVendors, setFilteredVendors] = useState<VendorItem[]>([]);
const [createVendor] =
useMutation<VendorListPageCreateVendorMutation>(createVendorMutation);
const [deleteVendor] =
@@ -196,7 +211,7 @@ function VendorListContent({
} = usePaginationFragment<
VendorListPagePaginationQuery,
VendorListPage_vendors$key
>(vendorListFragment, data.viewer.organization);
>(vendorListFragment, data.organization);
const vendors =
vendorsConnection.vendors.edges.map((edge) => edge.node) ?? [];
@@ -245,7 +260,7 @@ function VendorListContent({
style={{ borderRadius: "0.3rem" }}
className="absolute top-full left-0 mt-1 w-[calc(100%-100px)] max-h-48 overflow-y-auto border bg-popover shadow-md z-10"
>
{filteredVendors.map((vendor) => (
{filteredVendors.map((vendor: VendorItem) => (
<button
key={vendor.id}
className="w-full px-3 py-2 text-left hover:bg-accent"
@@ -254,7 +269,7 @@ function VendorListContent({
variables: {
connections: [vendorsConnection.vendors.__id],
input: {
organizationId: data.viewer.organization.id,
organizationId: organization.id,
name: vendor.name,
description: "",
serviceStartAt: new Date().toISOString(),
@@ -410,17 +425,20 @@ export default function VendorListPage() {
const [queryRef, loadQuery] =
useQueryLoader<VendorListPageQueryType>(vendorListPageQuery);
const { currentOrganization } = useOrganization();
useEffect(() => {
const after = searchParams.get("after");
const before = searchParams.get("before");
loadQuery({
organizationId: currentOrganization!.id,
first: before ? undefined : ITEMS_PER_PAGE,
after: after || undefined,
last: before ? ITEMS_PER_PAGE : undefined,
before: before || undefined,
});
}, [loadQuery]);
}, [loadQuery, currentOrganization]);
if (!queryRef) {
return <VendorListFallback />;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<bcc0482212bee6fc9682545cc8255139>>
* @generated SignedSource<<9453bfb4e947c003e812ae63a4f1ed0a>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -13,10 +13,10 @@ export type CreatePeoplePageQuery$variables = Record<PropertyKey, never>;
export type CreatePeoplePageQuery$data = {
readonly viewer: {
readonly id: string;
readonly organization: {
readonly organizations: ReadonlyArray<{
readonly id: string;
readonly name: string;
};
}>;
};
};
export type CreatePeoplePageQuery = {
@@ -47,8 +47,8 @@ v1 = [
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "organization",
"plural": false,
"name": "organizations",
"plural": true,
"selections": [
(v0/*: any*/),
{
@@ -83,16 +83,16 @@ return {
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "e0b72d4317b700564caa46c609276aed",
"cacheID": "74b036184be646c06a001772776c445b",
"id": null,
"metadata": {},
"name": "CreatePeoplePageQuery",
"operationKind": "query",
"text": "query CreatePeoplePageQuery {\n viewer {\n id\n organization {\n id\n name\n }\n }\n}\n"
"text": "query CreatePeoplePageQuery {\n viewer {\n id\n organizations {\n id\n name\n }\n }\n}\n"
}
};
})();
(node as any).hash = "89be7e6fd93829bdc8ac374983ca0a9b";
(node as any).hash = "751fa313df4e5ed06f8100d1e7e2d4ea";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<903f19e5b3c193151107de5969fe5259>>
* @generated SignedSource<<ea02ef2ebc2c52a72d443c130a44087c>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -10,30 +10,29 @@
import { ConcreteRequest } from 'relay-runtime';
export type ControlState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED";
export type FrameworkListPageQuery$variables = Record<PropertyKey, never>;
export type FrameworkListPageQuery$variables = {
organizationId: string;
};
export type FrameworkListPageQuery$data = {
readonly viewer: {
readonly id: string;
readonly organization: {
readonly frameworks: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly controls: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly state: ControlState;
};
}>;
};
readonly createdAt: any;
readonly description: string;
readonly id: string;
readonly name: string;
readonly updatedAt: any;
readonly organization: {
readonly frameworks?: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly controls: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly state: ControlState;
};
}>;
};
}>;
};
readonly createdAt: any;
readonly description: string;
readonly id: string;
readonly name: string;
readonly updatedAt: any;
};
}>;
};
};
};
@@ -43,82 +42,102 @@ export type FrameworkListPageQuery = {
};
const node: ConcreteRequest = (function(){
var v0 = {
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v1 = {
"alias": null,
"args": null,
"concreteType": "FrameworkConnection",
"kind": "LinkedField",
"name": "frameworks",
"plural": false,
v3 = {
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "FrameworkEdge",
"concreteType": "FrameworkConnection",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"name": "frameworks",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Framework",
"concreteType": "FrameworkEdge",
"kind": "LinkedField",
"name": "node",
"plural": false,
"name": "edges",
"plural": true,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "ControlConnection",
"concreteType": "Framework",
"kind": "LinkedField",
"name": "controls",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "ControlEdge",
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "ControlConnection",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"name": "controls",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Control",
"concreteType": "ControlEdge",
"kind": "LinkedField",
"name": "node",
"plural": false,
"name": "edges",
"plural": true,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"concreteType": "Control",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "state",
"storageKey": null
}
],
"storageKey": null
}
],
@@ -126,23 +145,23 @@ v1 = {
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
}
],
"storageKey": null
@@ -151,36 +170,25 @@ v1 = {
"storageKey": null
}
],
"storageKey": null
"type": "Organization",
"abstractKey": null
};
return {
"fragment": {
"argumentDefinitions": [],
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "FrameworkListPageQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "User",
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "viewer",
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "organization",
"plural": false,
"selections": [
(v1/*: any*/)
],
"storageKey": null
}
(v3/*: any*/)
],
"storageKey": null
}
@@ -190,48 +198,43 @@ return {
},
"kind": "Request",
"operation": {
"argumentDefinitions": [],
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "FrameworkListPageQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "User",
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "viewer",
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "organization",
"plural": false,
"selections": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
}
},
(v3/*: any*/),
(v2/*: any*/)
],
"storageKey": null
}
]
},
"params": {
"cacheID": "8f573b1a89e302e61a765711c1c6e73a",
"cacheID": "e45d4e64a00b3e27cd6dc52179f47d52",
"id": null,
"metadata": {},
"name": "FrameworkListPageQuery",
"operationKind": "query",
"text": "query FrameworkListPageQuery {\n viewer {\n id\n organization {\n frameworks {\n edges {\n node {\n id\n name\n description\n controls {\n edges {\n node {\n id\n state\n }\n }\n }\n createdAt\n updatedAt\n }\n }\n }\n id\n }\n }\n}\n"
"text": "query FrameworkListPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n frameworks {\n edges {\n node {\n id\n name\n description\n controls {\n edges {\n node {\n id\n state\n }\n }\n }\n createdAt\n updatedAt\n }\n }\n }\n }\n id\n }\n}\n"
}
};
})();
(node as any).hash = "ac0337411177ab9b1c8ef81e6bb47a06";
(node as any).hash = "5cd314faffe31e716de2336e70cca4ee";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<7ca2f2186b657853d9fce4c7a21a7db1>>
* @generated SignedSource<<17467071cb25b3e291532a4fb9074d67>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -16,7 +16,7 @@ export type HomePageQuery$variables = Record<PropertyKey, never>;
export type HomePageQuery$data = {
readonly viewer: {
readonly id: string;
readonly organization: {
readonly organizations: ReadonlyArray<{
readonly createdAt: any;
readonly frameworks: {
readonly edges: ReadonlyArray<{
@@ -124,7 +124,7 @@ export type HomePageQuery$data = {
};
}>;
};
};
}>;
};
};
export type HomePageQuery = {
@@ -626,8 +626,8 @@ return {
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "organization",
"plural": false,
"name": "organizations",
"plural": true,
"selections": [
(v1/*: any*/),
(v2/*: any*/),
@@ -665,8 +665,8 @@ return {
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "organization",
"plural": false,
"name": "organizations",
"plural": true,
"selections": [
(v1/*: any*/),
(v2/*: any*/),
@@ -684,16 +684,16 @@ return {
]
},
"params": {
"cacheID": "9f8dd0b1a2698dd2d944e75de5d32087",
"cacheID": "d40cf8a4787ed129284c566abd99fdaf",
"id": null,
"metadata": {},
"name": "HomePageQuery",
"operationKind": "query",
"text": "query HomePageQuery {\n viewer {\n id\n organization {\n name\n createdAt\n updatedAt\n vendors {\n edges {\n node {\n id\n name\n createdAt\n updatedAt\n }\n }\n }\n peoples {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n additionalEmailAddresses\n createdAt\n updatedAt\n }\n }\n }\n frameworks {\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n edges {\n cursor\n node {\n id\n name\n description\n controls {\n edges {\n node {\n id\n name\n state\n stateTransisions {\n edges {\n node {\n id\n toState\n fromState\n createdAt\n updatedAt\n }\n }\n }\n tasks {\n edges {\n node {\n id\n name\n state\n evidences {\n edges {\n node {\n id\n state\n fileUrl\n stateTransisions {\n edges {\n node {\n id\n fromState\n toState\n reason\n createdAt\n updatedAt\n }\n }\n }\n createdAt\n updatedAt\n }\n }\n }\n stateTransisions {\n edges {\n node {\n id\n toState\n fromState\n reason\n createdAt\n updatedAt\n }\n }\n }\n createdAt\n updatedAt\n }\n }\n }\n }\n }\n }\n }\n }\n }\n id\n }\n }\n}\n"
"text": "query HomePageQuery {\n viewer {\n id\n organizations {\n name\n createdAt\n updatedAt\n vendors {\n edges {\n node {\n id\n name\n createdAt\n updatedAt\n }\n }\n }\n peoples {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n additionalEmailAddresses\n createdAt\n updatedAt\n }\n }\n }\n frameworks {\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n edges {\n cursor\n node {\n id\n name\n description\n controls {\n edges {\n node {\n id\n name\n state\n stateTransisions {\n edges {\n node {\n id\n toState\n fromState\n createdAt\n updatedAt\n }\n }\n }\n tasks {\n edges {\n node {\n id\n name\n state\n evidences {\n edges {\n node {\n id\n state\n fileUrl\n stateTransisions {\n edges {\n node {\n id\n fromState\n toState\n reason\n createdAt\n updatedAt\n }\n }\n }\n createdAt\n updatedAt\n }\n }\n }\n stateTransisions {\n edges {\n node {\n id\n toState\n fromState\n reason\n createdAt\n updatedAt\n }\n }\n }\n createdAt\n updatedAt\n }\n }\n }\n }\n }\n }\n }\n }\n }\n id\n }\n }\n}\n"
}
};
})();
(node as any).hash = "875e681cb7e936d3f1a543e88c4cb2d0";
(node as any).hash = "e9da69d4e7095f003eb28bf51e7c399d";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<9ff37ab5131d3343de8f2d0c843377d1>>
* @generated SignedSource<<48cb6bd6a591f9b4a0c44d3ac0b54ee4>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -60,21 +60,7 @@ v5 = [
"variableName": "id"
}
],
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v8 = [
v6 = [
{
"kind": "Variable",
"name": "after",
@@ -95,7 +81,21 @@ v8 = [
"name": "last",
"variableName": "last"
}
];
],
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v8 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
@@ -118,7 +118,7 @@ return {
"plural": false,
"selections": [
{
"args": null,
"args": (v6/*: any*/),
"kind": "FragmentSpread",
"name": "PeopleListPage_peoples"
}
@@ -149,14 +149,14 @@ return {
"name": "node",
"plural": false,
"selections": [
(v6/*: any*/),
(v7/*: any*/),
(v8/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": (v8/*: any*/),
"args": (v6/*: any*/),
"concreteType": "PeopleConnection",
"kind": "LinkedField",
"name": "peoples",
@@ -178,7 +178,7 @@ return {
"name": "node",
"plural": false,
"selections": [
(v7/*: any*/),
(v8/*: any*/),
{
"alias": null,
"args": null,
@@ -221,7 +221,7 @@ return {
"name": "updatedAt",
"storageKey": null
},
(v6/*: any*/)
(v7/*: any*/)
],
"storageKey": null
},
@@ -291,7 +291,7 @@ return {
},
{
"alias": null,
"args": (v8/*: any*/),
"args": (v6/*: any*/),
"filters": null,
"handle": "connection",
"key": "PeopleListPage_peoples",
@@ -308,16 +308,16 @@ return {
]
},
"params": {
"cacheID": "17a418660f5fd222c0400f2825634ed4",
"cacheID": "f20190ee884fd80e3f9157895667cb9e",
"id": null,
"metadata": {},
"name": "PeopleListPagePaginationQuery",
"operationKind": "query",
"text": "query PeopleListPagePaginationQuery(\n $after: CursorKey\n $before: CursorKey\n $first: Int\n $last: Int\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...PeopleListPage_peoples\n id\n }\n}\n\nfragment PeopleListPage_peoples on Organization {\n id\n peoples(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n additionalEmailAddresses\n kind\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n}\n"
"text": "query PeopleListPagePaginationQuery(\n $after: CursorKey\n $before: CursorKey\n $first: Int\n $last: Int\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...PeopleListPage_peoples_pbnwq\n id\n }\n}\n\nfragment PeopleListPage_peoples_pbnwq on Organization {\n id\n peoples(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n additionalEmailAddresses\n kind\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n}\n"
}
};
})();
(node as any).hash = "6602dd60c3adf0db89580e752495c910";
(node as any).hash = "3d7335649a48a459415be45828b7e265";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<fd54290261cf29d9b3cf42b70a68f398>>
* @generated SignedSource<<7b8897ff225dacac2f05e25136520b59>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -15,13 +15,11 @@ export type PeopleListPageQuery$variables = {
before?: any | null | undefined;
first?: number | null | undefined;
last?: number | null | undefined;
organizationId: string;
};
export type PeopleListPageQuery$data = {
readonly viewer: {
readonly id: string;
readonly organization: {
readonly " $fragmentSpreads": FragmentRefs<"PeopleListPage_peoples">;
};
readonly organization: {
readonly " $fragmentSpreads": FragmentRefs<"PeopleListPage_peoples">;
};
};
export type PeopleListPageQuery = {
@@ -51,13 +49,18 @@ v3 = {
"name": "last"
},
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
},
v5 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v6 = [
{
"kind": "Variable",
"name": "after",
@@ -78,43 +81,46 @@ v5 = [
"name": "last",
"variableName": "last"
}
];
],
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v8 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/),
(v2/*: any*/),
(v3/*: any*/)
(v3/*: any*/),
(v4/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "PeopleListPageQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "User",
"alias": "organization",
"args": (v5/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "viewer",
"name": "node",
"plural": false,
"selections": [
(v4/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "organization",
"plural": false,
"selections": [
{
"args": null,
"kind": "FragmentSpread",
"name": "PeopleListPage_peoples"
}
],
"storageKey": null
"args": (v6/*: any*/),
"kind": "FragmentSpread",
"name": "PeopleListPage_peoples"
}
],
"storageKey": null
@@ -126,6 +132,7 @@ return {
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v4/*: any*/),
(v2/*: any*/),
(v0/*: any*/),
(v3/*: any*/),
@@ -135,26 +142,21 @@ return {
"name": "PeopleListPageQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "User",
"alias": "organization",
"args": (v5/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "viewer",
"name": "node",
"plural": false,
"selections": [
(v4/*: any*/),
(v7/*: any*/),
(v8/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "organization",
"plural": false,
"kind": "InlineFragment",
"selections": [
(v4/*: any*/),
{
"alias": null,
"args": (v5/*: any*/),
"args": (v6/*: any*/),
"concreteType": "PeopleConnection",
"kind": "LinkedField",
"name": "peoples",
@@ -176,7 +178,7 @@ return {
"name": "node",
"plural": false,
"selections": [
(v4/*: any*/),
(v8/*: any*/),
{
"alias": null,
"args": null,
@@ -219,13 +221,7 @@ return {
"name": "updatedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
}
(v7/*: any*/)
],
"storageKey": null
},
@@ -295,7 +291,7 @@ return {
},
{
"alias": null,
"args": (v5/*: any*/),
"args": (v6/*: any*/),
"filters": null,
"handle": "connection",
"key": "PeopleListPage_peoples",
@@ -303,7 +299,8 @@ return {
"name": "peoples"
}
],
"storageKey": null
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
@@ -311,16 +308,16 @@ return {
]
},
"params": {
"cacheID": "d9b0af5656fcb50e2cedf0f831c0394d",
"cacheID": "ceb15735cc88f1993e35ac9a539a7ad0",
"id": null,
"metadata": {},
"name": "PeopleListPageQuery",
"operationKind": "query",
"text": "query PeopleListPageQuery(\n $first: Int\n $after: CursorKey\n $last: Int\n $before: CursorKey\n) {\n viewer {\n id\n organization {\n ...PeopleListPage_peoples\n id\n }\n }\n}\n\nfragment PeopleListPage_peoples on Organization {\n id\n peoples(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n additionalEmailAddresses\n kind\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n}\n"
"text": "query PeopleListPageQuery(\n $organizationId: ID!\n $first: Int\n $after: CursorKey\n $last: Int\n $before: CursorKey\n) {\n organization: node(id: $organizationId) {\n __typename\n ...PeopleListPage_peoples_pbnwq\n id\n }\n}\n\nfragment PeopleListPage_peoples_pbnwq on Organization {\n id\n peoples(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n fullName\n primaryEmailAddress\n additionalEmailAddresses\n kind\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n}\n"
}
};
})();
(node as any).hash = "9646de6e2d6b844cb87945b8a26c0999";
(node as any).hash = "0a1918698d276906f5e17baa40f756e3";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<5e071fe96262050a43c9471c27ff667e>>
* @generated SignedSource<<4291ff1f33eb226774e68544740b4331>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -54,19 +54,23 @@ v1 = {
return {
"argumentDefinitions": [
{
"kind": "RootArgument",
"defaultValue": null,
"kind": "LocalArgument",
"name": "after"
},
{
"kind": "RootArgument",
"defaultValue": null,
"kind": "LocalArgument",
"name": "before"
},
{
"kind": "RootArgument",
"defaultValue": null,
"kind": "LocalArgument",
"name": "first"
},
{
"kind": "RootArgument",
"defaultValue": null,
"kind": "LocalArgument",
"name": "last"
}
],
@@ -252,6 +256,6 @@ return {
};
})();
(node as any).hash = "6602dd60c3adf0db89580e752495c910";
(node as any).hash = "3d7335649a48a459415be45828b7e265";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<0b5e0bcf4f6519e1ccb1d7ffa3e488cb>>
* @generated SignedSource<<f2e46ea14241587a319e9a6af9072bb9>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -13,10 +13,10 @@ export type SettingsPageQuery$variables = Record<PropertyKey, never>;
export type SettingsPageQuery$data = {
readonly viewer: {
readonly id: string;
readonly organization: {
readonly organizations: ReadonlyArray<{
readonly logoUrl: string;
readonly name: string;
};
}>;
};
};
export type SettingsPageQuery = {
@@ -67,8 +67,8 @@ return {
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "organization",
"plural": false,
"name": "organizations",
"plural": true,
"selections": [
(v1/*: any*/),
(v2/*: any*/)
@@ -102,8 +102,8 @@ return {
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "organization",
"plural": false,
"name": "organizations",
"plural": true,
"selections": [
(v1/*: any*/),
(v2/*: any*/),
@@ -117,16 +117,16 @@ return {
]
},
"params": {
"cacheID": "77d3f8660df07b02aa515461c9ec1449",
"cacheID": "5a3f5c431e1f886a330d220fd33a33f3",
"id": null,
"metadata": {},
"name": "SettingsPageQuery",
"operationKind": "query",
"text": "query SettingsPageQuery {\n viewer {\n id\n organization {\n name\n logoUrl\n id\n }\n }\n}\n"
"text": "query SettingsPageQuery {\n viewer {\n id\n organizations {\n name\n logoUrl\n id\n }\n }\n}\n"
}
};
})();
(node as any).hash = "c639e9c45b175beec64550bb72fe5111";
(node as any).hash = "cef332dd829051516fa47315c3a60d1f";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<035db05f931b88ee09b1bbdd2c39a7e7>>
* @generated SignedSource<<8a3c352a16aeb823ac25ee61446cc573>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -60,21 +60,7 @@ v5 = [
"variableName": "id"
}
],
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v8 = [
v6 = [
{
"kind": "Variable",
"name": "after",
@@ -95,7 +81,21 @@ v8 = [
"name": "last",
"variableName": "last"
}
];
],
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v8 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
@@ -118,7 +118,7 @@ return {
"plural": false,
"selections": [
{
"args": null,
"args": (v6/*: any*/),
"kind": "FragmentSpread",
"name": "VendorListPage_vendors"
}
@@ -149,14 +149,14 @@ return {
"name": "node",
"plural": false,
"selections": [
(v6/*: any*/),
(v7/*: any*/),
(v8/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": (v8/*: any*/),
"args": (v6/*: any*/),
"concreteType": "VendorConnection",
"kind": "LinkedField",
"name": "vendors",
@@ -178,7 +178,7 @@ return {
"name": "node",
"plural": false,
"selections": [
(v7/*: any*/),
(v8/*: any*/),
{
"alias": null,
"args": null,
@@ -207,7 +207,7 @@ return {
"name": "updatedAt",
"storageKey": null
},
(v6/*: any*/)
(v7/*: any*/)
],
"storageKey": null
},
@@ -277,7 +277,7 @@ return {
},
{
"alias": null,
"args": (v8/*: any*/),
"args": (v6/*: any*/),
"filters": null,
"handle": "connection",
"key": "VendorListPage_vendors",
@@ -294,16 +294,16 @@ return {
]
},
"params": {
"cacheID": "61e9346ec50a3f4b1397fa2e6165ab3e",
"cacheID": "8fa8530a89995a836fec656b1d141b89",
"id": null,
"metadata": {},
"name": "VendorListPagePaginationQuery",
"operationKind": "query",
"text": "query VendorListPagePaginationQuery(\n $after: CursorKey\n $before: CursorKey\n $first: Int\n $last: Int\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...VendorListPage_vendors\n id\n }\n}\n\nfragment VendorListPage_vendors on Organization {\n id\n vendors(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n name\n description\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n}\n"
"text": "query VendorListPagePaginationQuery(\n $after: CursorKey\n $before: CursorKey\n $first: Int\n $last: Int\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...VendorListPage_vendors_pbnwq\n id\n }\n}\n\nfragment VendorListPage_vendors_pbnwq on Organization {\n id\n vendors(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n name\n description\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n}\n"
}
};
})();
(node as any).hash = "b5d797660f73e909467c3ab4e988001f";
(node as any).hash = "7ab226392b5cd58f332515753f1099ae";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<42f5a79d59c05d56dc664163fd507fed>>
* @generated SignedSource<<15bedd2878042e53e79e430f79d767cb>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -15,13 +15,11 @@ export type VendorListPageQuery$variables = {
before?: any | null | undefined;
first?: number | null | undefined;
last?: number | null | undefined;
organizationId: string;
};
export type VendorListPageQuery$data = {
readonly viewer: {
readonly id: string;
readonly organization: {
readonly " $fragmentSpreads": FragmentRefs<"VendorListPage_vendors">;
};
readonly organization: {
readonly " $fragmentSpreads": FragmentRefs<"VendorListPage_vendors">;
};
};
export type VendorListPageQuery = {
@@ -51,13 +49,18 @@ v3 = {
"name": "last"
},
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
},
v5 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v6 = [
{
"kind": "Variable",
"name": "after",
@@ -78,43 +81,46 @@ v5 = [
"name": "last",
"variableName": "last"
}
];
],
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v8 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/),
(v2/*: any*/),
(v3/*: any*/)
(v3/*: any*/),
(v4/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "VendorListPageQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "User",
"alias": "organization",
"args": (v5/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "viewer",
"name": "node",
"plural": false,
"selections": [
(v4/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "organization",
"plural": false,
"selections": [
{
"args": null,
"kind": "FragmentSpread",
"name": "VendorListPage_vendors"
}
],
"storageKey": null
"args": (v6/*: any*/),
"kind": "FragmentSpread",
"name": "VendorListPage_vendors"
}
],
"storageKey": null
@@ -126,6 +132,7 @@ return {
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v4/*: any*/),
(v2/*: any*/),
(v0/*: any*/),
(v3/*: any*/),
@@ -135,26 +142,21 @@ return {
"name": "VendorListPageQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "User",
"alias": "organization",
"args": (v5/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "viewer",
"name": "node",
"plural": false,
"selections": [
(v4/*: any*/),
(v7/*: any*/),
(v8/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "organization",
"plural": false,
"kind": "InlineFragment",
"selections": [
(v4/*: any*/),
{
"alias": null,
"args": (v5/*: any*/),
"args": (v6/*: any*/),
"concreteType": "VendorConnection",
"kind": "LinkedField",
"name": "vendors",
@@ -176,7 +178,7 @@ return {
"name": "node",
"plural": false,
"selections": [
(v4/*: any*/),
(v8/*: any*/),
{
"alias": null,
"args": null,
@@ -205,13 +207,7 @@ return {
"name": "updatedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
}
(v7/*: any*/)
],
"storageKey": null
},
@@ -281,7 +277,7 @@ return {
},
{
"alias": null,
"args": (v5/*: any*/),
"args": (v6/*: any*/),
"filters": null,
"handle": "connection",
"key": "VendorListPage_vendors",
@@ -289,7 +285,8 @@ return {
"name": "vendors"
}
],
"storageKey": null
"type": "Organization",
"abstractKey": null
}
],
"storageKey": null
@@ -297,16 +294,16 @@ return {
]
},
"params": {
"cacheID": "0c3f0f24d810d8a510fa30ca3f3f5890",
"cacheID": "4f036b643119ac6840eede9bc148c6ac",
"id": null,
"metadata": {},
"name": "VendorListPageQuery",
"operationKind": "query",
"text": "query VendorListPageQuery(\n $first: Int\n $after: CursorKey\n $last: Int\n $before: CursorKey\n) {\n viewer {\n id\n organization {\n ...VendorListPage_vendors\n id\n }\n }\n}\n\nfragment VendorListPage_vendors on Organization {\n id\n vendors(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n name\n description\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n}\n"
"text": "query VendorListPageQuery(\n $organizationId: ID!\n $first: Int\n $after: CursorKey\n $last: Int\n $before: CursorKey\n) {\n organization: node(id: $organizationId) {\n __typename\n ...VendorListPage_vendors_pbnwq\n id\n }\n}\n\nfragment VendorListPage_vendors_pbnwq on Organization {\n id\n vendors(first: $first, after: $after, last: $last, before: $before) {\n edges {\n node {\n id\n name\n description\n createdAt\n updatedAt\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n}\n"
}
};
})();
(node as any).hash = "ffd0aaecc51a29f19ff874adbb38dab4";
(node as any).hash = "a0a4881ec7e60051fa196830f5c62f45";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<aa140c75b78a370fac81e7180d3537fc>>
* @generated SignedSource<<d8da23ca67cc47188e916e129dac0be0>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -51,19 +51,23 @@ v1 = {
return {
"argumentDefinitions": [
{
"kind": "RootArgument",
"defaultValue": null,
"kind": "LocalArgument",
"name": "after"
},
{
"kind": "RootArgument",
"defaultValue": null,
"kind": "LocalArgument",
"name": "before"
},
{
"kind": "RootArgument",
"defaultValue": null,
"kind": "LocalArgument",
"name": "first"
},
{
"kind": "RootArgument",
"defaultValue": null,
"kind": "LocalArgument",
"name": "last"
}
],
@@ -235,6 +239,6 @@ return {
};
})();
(node as any).hash = "b5d797660f73e909467c3ab4e988001f";
(node as any).hash = "7ab226392b5cd58f332515753f1099ae";
export default node;

View File

@@ -345,9 +345,10 @@ type EvidenceStateTransition {
# Authentication types
type User {
id: ID!
fullName: String!
email: String!
organization: Organization! @goField(forceResolver: true)
organizations: [Organization!]! @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!

View File

@@ -271,11 +271,12 @@ type ComplexityRoot struct {
}
User struct {
CreatedAt func(childComplexity int) int
Email func(childComplexity int) int
ID func(childComplexity int) int
Organization func(childComplexity int) int
UpdatedAt func(childComplexity int) int
CreatedAt func(childComplexity int) int
Email func(childComplexity int) int
FullName func(childComplexity int) int
ID func(childComplexity int) int
Organizations func(childComplexity int) int
UpdatedAt func(childComplexity int) int
}
Vendor struct {
@@ -337,7 +338,7 @@ type TaskResolver interface {
Evidences(ctx context.Context, obj *types.Task, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.EvidenceConnection, error)
}
type UserResolver interface {
Organization(ctx context.Context, obj *types.User) (*types.Organization, error)
Organizations(ctx context.Context, obj *types.User) ([]*types.Organization, error)
}
type executableSchema struct {
@@ -1258,6 +1259,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.User.Email(childComplexity), true
case "User.fullName":
if e.complexity.User.FullName == nil {
break
}
return e.complexity.User.FullName(childComplexity), true
case "User.id":
if e.complexity.User.ID == nil {
break
@@ -1265,12 +1273,12 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.User.ID(childComplexity), true
case "User.organization":
if e.complexity.User.Organization == nil {
case "User.organizations":
if e.complexity.User.Organizations == nil {
break
}
return e.complexity.User.Organization(childComplexity), true
return e.complexity.User.Organizations(childComplexity), true
case "User.updatedAt":
if e.complexity.User.UpdatedAt == nil {
@@ -1856,9 +1864,10 @@ type EvidenceStateTransition {
# Authentication types
type User {
id: ID!
fullName: String!
email: String!
organization: Organization! @goField(forceResolver: true)
organizations: [Organization!]! @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
@@ -6740,10 +6749,12 @@ func (ec *executionContext) fieldContext_Query_viewer(_ context.Context, field g
switch field.Name {
case "id":
return ec.fieldContext_User_id(ctx, field)
case "fullName":
return ec.fieldContext_User_fullName(ctx, field)
case "email":
return ec.fieldContext_User_email(ctx, field)
case "organization":
return ec.fieldContext_User_organization(ctx, field)
case "organizations":
return ec.fieldContext_User_organizations(ctx, field)
case "createdAt":
return ec.fieldContext_User_createdAt(ctx, field)
case "updatedAt":
@@ -7896,6 +7907,44 @@ func (ec *executionContext) fieldContext_User_id(_ context.Context, field graphq
return fc, nil
}
func (ec *executionContext) _User_fullName(ctx context.Context, field graphql.CollectedField, obj *types.User) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_User_fullName(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.FullName, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_User_fullName(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "User",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type String does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _User_email(ctx context.Context, field graphql.CollectedField, obj *types.User) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_User_email(ctx, field)
if err != nil {
@@ -7934,15 +7983,15 @@ func (ec *executionContext) fieldContext_User_email(_ context.Context, field gra
return fc, nil
}
func (ec *executionContext) _User_organization(ctx context.Context, field graphql.CollectedField, obj *types.User) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_User_organization(ctx, field)
func (ec *executionContext) _User_organizations(ctx context.Context, field graphql.CollectedField, obj *types.User) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_User_organizations(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.User().Organization(rctx, obj)
return ec.resolvers.User().Organizations(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
@@ -7954,12 +8003,12 @@ func (ec *executionContext) _User_organization(ctx context.Context, field graphq
}
return graphql.Null
}
res := resTmp.(*types.Organization)
res := resTmp.([]*types.Organization)
fc.Result = res
return ec.marshalNOrganization2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐOrganization(ctx, field.Selections, res)
return ec.marshalNOrganization2ᚕᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐOrganizationᚄ(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_User_organization(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
func (ec *executionContext) fieldContext_User_organizations(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "User",
Field: field,
@@ -12702,16 +12751,21 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
case "fullName":
out.Values[i] = ec._User_fullName(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
case "email":
out.Values[i] = ec._User_email(ctx, field, obj)
if out.Values[i] == graphql.Null {
atomic.AddUint32(&out.Invalids, 1)
}
case "organization":
case "organizations":
field := field
innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
res = ec._User_organization(ctx, field, obj)
res = ec._User_organizations(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&fs.Invalids, 1)
}
@@ -13853,8 +13907,42 @@ func (ec *executionContext) marshalNNode2githubᚗcomᚋgetproboᚋproboᚋpkg
return ec._Node(ctx, sel, v)
}
func (ec *executionContext) marshalNOrganization2githubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐOrganization(ctx context.Context, sel ast.SelectionSet, v types.Organization) graphql.Marshaler {
return ec._Organization(ctx, sel, &v)
func (ec *executionContext) marshalNOrganization2ᚕᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐOrganizationᚄ(ctx context.Context, sel ast.SelectionSet, v []*types.Organization) graphql.Marshaler {
ret := make(graphql.Array, len(v))
var wg sync.WaitGroup
isLen1 := len(v) == 1
if !isLen1 {
wg.Add(len(v))
}
for i := range v {
i := i
fc := &graphql.FieldContext{
Index: &i,
Result: &v[i],
}
ctx := graphql.WithFieldContext(ctx, fc)
f := func(i int) {
if !isLen1 {
defer wg.Done()
}
ret[i] = ec.marshalNOrganization2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐOrganization(ctx, sel, v[i])
}
if isLen1 {
f(i)
} else {
go f(i)
}
}
wg.Wait()
for _, e := range ret {
if e == graphql.Null {
return graphql.Null
}
}
return ret
}
func (ec *executionContext) marshalNOrganization2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋapiᚋconsoleᚋv1ᚋtypesᚐOrganization(ctx context.Context, sel ast.SelectionSet, v *types.Organization) graphql.Marshaler {

View File

@@ -292,11 +292,12 @@ type UpdateVendorInput struct {
}
type User struct {
ID gid.GID `json:"id"`
Email string `json:"email"`
Organization *Organization `json:"organization"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ID gid.GID `json:"id"`
FullName string `json:"fullName"`
Email string `json:"email"`
Organizations []*Organization `json:"organizations"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type Vendor struct {

View File

@@ -0,0 +1,49 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package types
import (
"time"
"github.com/getprobo/probo/pkg/usrmgr/coredata"
)
type UserType struct {
ID string
Email string
CreatedAt time.Time
UpdatedAt time.Time
}
// NewUser creates a new UserType from a coredata.User
func NewUser(u *coredata.User) *UserType {
return &UserType{
ID: u.ID.String(),
Email: u.EmailAddress,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
}
// AddUserToOrganizationInput is the input for adding a user to an organization
type AddUserToOrganizationInput struct {
UserID string
OrganizationID string
}
// AddUserToOrganizationPayload is the payload for adding a user to an organization
type AddUserToOrganizationPayload struct {
User *UserType
}

View File

@@ -269,6 +269,7 @@ func (r *queryResolver) Viewer(ctx context.Context) (*types.User, error) {
return &types.User{
ID: user.ID,
Email: user.EmailAddress,
FullName: user.FullName,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}, nil
@@ -298,26 +299,30 @@ func (r *taskResolver) Evidences(ctx context.Context, obj *types.Task, first *in
return types.NewEvidenceConnection(page), nil
}
// Organization is the resolver for the organization field.
func (r *userResolver) Organization(ctx context.Context, obj *types.User) (*types.Organization, error) {
// Get the user's organization ID
organizationID, err := r.usrmgrSvc.GetUserOrganization(ctx, obj.ID)
// Organizations is the resolver for the organizations field.
func (r *userResolver) Organizations(ctx context.Context, obj *types.User) ([]*types.Organization, error) {
// Get the user's organization IDs
organizationIDs, err := r.usrmgrSvc.GetUserOrganizations(ctx, obj.ID)
if err != nil {
return nil, fmt.Errorf("failed to get user organization: %w", err)
return nil, fmt.Errorf("failed to get user organizations: %w", err)
}
// If the user doesn't have an organization, return nil
if organizationID == gid.Nil {
return nil, nil
// If the user doesn't have any organizations, return an empty slice
if len(organizationIDs) == 0 {
return []*types.Organization{}, nil
}
// Get the organization details
organization, err := r.proboSvc.GetOrganization(ctx, organizationID)
if err != nil {
return nil, fmt.Errorf("failed to get organization details: %w", err)
// Get the organization details for each organization ID
var organizations []*types.Organization
for _, organizationID := range organizationIDs {
organization, err := r.proboSvc.GetOrganization(ctx, organizationID)
if err != nil {
return nil, fmt.Errorf("failed to get organization details: %w", err)
}
organizations = append(organizations, types.NewOrganization(organization))
}
return types.NewOrganization(organization), nil
return organizations, nil
}
// Control returns schema.ControlResolver implementation.

View File

@@ -0,0 +1,11 @@
CREATE TABLE usrmgr_user_organizations (
user_id TEXT REFERENCES usrmgr_users(id) NOT NULL,
organization_id TEXT REFERENCES organizations(id) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
PRIMARY KEY (user_id, organization_id)
);
INSERT INTO usrmgr_user_organizations (user_id, organization_id, created_at)
SELECT id, organization_id, NOW()
FROM usrmgr_users
WHERE organization_id IS NOT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE usrmgr_users ADD COLUMN fullname TEXT;

View File

@@ -0,0 +1 @@
ALTER TABLE usrmgr_users ALTER COLUMN fullname SET NOT NULL;

View File

@@ -29,6 +29,7 @@ type (
ID gid.GID
EmailAddress string
HashedPassword []byte
FullName string
OrganizationID gid.GID
CreatedAt time.Time
UpdatedAt time.Time
@@ -44,6 +45,7 @@ func (u *User) scan(r pgx.Row) error {
&u.ID,
&u.EmailAddress,
&u.HashedPassword,
&u.FullName,
&u.OrganizationID,
&u.CreatedAt,
&u.UpdatedAt,
@@ -60,6 +62,7 @@ SELECT
id,
email_address,
hashed_password,
fullname,
organization_id,
created_at,
updated_at
@@ -94,6 +97,7 @@ SELECT
id,
email_address,
hashed_password,
fullname,
organization_id,
created_at,
updated_at
@@ -124,11 +128,12 @@ func (u *User) Insert(
) error {
q := `
INSERT INTO
usrmgr_users (id, email_address, hashed_password, organization_id, created_at, updated_at)
usrmgr_users (id, email_address, hashed_password, fullname, organization_id, created_at, updated_at)
VALUES (
@user_id,
@email_address,
@hashed_password,
@fullname,
@organization_id,
@created_at,
@updated_at
@@ -139,6 +144,7 @@ VALUES (
"user_id": u.ID,
"email_address": u.EmailAddress,
"hashed_password": u.HashedPassword,
"fullname": u.FullName,
"organization_id": "AZSfP_xAcAC5IAAAAAAltA",
"created_at": u.CreatedAt,
"updated_at": u.UpdatedAt,

View File

@@ -308,46 +308,6 @@ func (s Service) GetUserBySession(
return s.GetUserByID(ctx, session.UserID)
}
// SetUserOrganization sets the organization for a user
func (s Service) SetUserOrganization(
ctx context.Context,
userID gid.GID,
organizationID gid.GID,
) error {
return s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
user := &coredata.User{}
if err := user.LoadByID(ctx, tx, userID); err != nil {
return fmt.Errorf("user not found: %w", err)
}
// Update the organization ID
user.OrganizationID = organizationID
user.UpdatedAt = time.Now()
// Update the user in the database
q := `
UPDATE usrmgr_users
SET organization_id = @organization_id, updated_at = @updated_at
WHERE id = @user_id
`
args := pgx.NamedArgs{
"user_id": user.ID,
"organization_id": user.OrganizationID,
"updated_at": user.UpdatedAt,
}
_, err := tx.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("cannot update user organization: %w", err)
}
return nil
},
)
}
// GetUserOrganization gets the organization ID for a user
func (s Service) GetUserOrganization(
ctx context.Context,
@@ -360,3 +320,101 @@ func (s Service) GetUserOrganization(
return user.OrganizationID, nil
}
// GetUserOrganizations gets all organizations for a user
func (s Service) GetUserOrganizations(
ctx context.Context,
userID gid.GID,
) ([]gid.GID, error) {
q := `
SELECT
organization_id
FROM
usrmgr_user_organizations
WHERE
user_id = @user_id;
`
args := pgx.NamedArgs{"user_id": userID}
var organizationIDs []gid.GID
err := s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
rows, err := tx.Query(ctx, q, args)
if err != nil {
return fmt.Errorf("failed to query user organizations: %w", err)
}
defer rows.Close()
for rows.Next() {
var organizationID gid.GID
if err := rows.Scan(&organizationID); err != nil {
return fmt.Errorf("failed to scan organization ID: %w", err)
}
organizationIDs = append(organizationIDs, organizationID)
}
if err := rows.Err(); err != nil {
return fmt.Errorf("error iterating over rows: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return organizationIDs, nil
}
// AddUserToOrganization adds a user to an organization
func (s Service) AddUserToOrganization(
ctx context.Context,
userID gid.GID,
organizationID gid.GID,
) error {
q := `
INSERT INTO
usrmgr_user_organizations (user_id, organization_id, created_at)
VALUES
(@user_id, @organization_id, NOW())
ON CONFLICT (user_id, organization_id) DO NOTHING;
`
args := pgx.NamedArgs{
"user_id": userID,
"organization_id": organizationID,
}
return s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
_, err := tx.Exec(ctx, q, args)
if err != nil {
return fmt.Errorf("failed to add user to organization: %w", err)
}
return nil
},
)
}
// GetUserIDFromContext gets the user ID from the context
func (s Service) GetUserIDFromContext(ctx context.Context) (gid.GID, error) {
// Get the session ID from the context
sessionID, ok := ctx.Value("session_id").(gid.GID)
if !ok {
return gid.GID{}, fmt.Errorf("no session ID in context")
}
// Get the session
session, err := s.GetSession(ctx, sessionID)
if err != nil {
return gid.GID{}, fmt.Errorf("failed to get session: %w", err)
}
return session.UserID, nil
}