mirror of https://github.com/buster-so/buster.git
commit
0cba4254eb
|
@ -175,6 +175,10 @@ describe('getDashboardHandler', () => {
|
|||
requiredRole: 'can_view',
|
||||
organizationId: mockDashboard.organizationId,
|
||||
workspaceSharing: 'none',
|
||||
publiclyAccessible: false,
|
||||
publicExpiryDate: undefined,
|
||||
publicPassword: undefined,
|
||||
userSuppliedPassword: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -85,6 +85,10 @@ export async function getDashboardHandler(
|
|||
requiredRole: 'can_view',
|
||||
organizationId: dashboardFile.organizationId,
|
||||
workspaceSharing: dashboardFile.workspaceSharing || 'none',
|
||||
publiclyAccessible: dashboardFile.publiclyAccessible,
|
||||
publicExpiryDate: dashboardFile.publicExpiryDate ?? undefined,
|
||||
publicPassword: dashboardFile.publicPassword ?? undefined,
|
||||
userSuppliedPassword: password,
|
||||
});
|
||||
|
||||
// Check public access
|
||||
|
|
|
@ -67,6 +67,10 @@ export const reportFilesProxyRouter = async (
|
|||
requiredRole: 'can_view',
|
||||
organizationId: reportData.organizationId,
|
||||
workspaceSharing: reportData.workspaceSharing,
|
||||
publiclyAccessible: reportData.publiclyAccessible,
|
||||
publicExpiryDate: reportData.publicExpiryDate ?? undefined,
|
||||
publicPassword: reportData.publicPassword ?? undefined,
|
||||
userSuppliedPassword: undefined,
|
||||
});
|
||||
|
||||
if (!hasAccess) {
|
||||
|
|
|
@ -78,6 +78,10 @@ export async function getMetricDataHandler(
|
|||
requiredRole: 'can_view',
|
||||
organizationId,
|
||||
workspaceSharing: metric.workspaceSharing ?? 'none',
|
||||
publiclyAccessible: metric.publiclyAccessible,
|
||||
publicExpiryDate: metric.publicExpiryDate ?? undefined,
|
||||
publicPassword: metric.publicPassword ?? undefined,
|
||||
userSuppliedPassword: undefined,
|
||||
});
|
||||
|
||||
if (!hasAccess) {
|
||||
|
|
|
@ -22,7 +22,7 @@ async function updateReportHandler(
|
|||
assetId: reportId,
|
||||
assetType: 'report_file',
|
||||
workspaceSharing: getReportWorkspaceSharing,
|
||||
requiredRole: ['full_access', 'owner', 'can_edit'],
|
||||
requiredRole: 'can_edit',
|
||||
});
|
||||
|
||||
const { name, content, update_version = false } = request;
|
||||
|
|
|
@ -23,7 +23,7 @@ export async function deleteReportSharingHandler(
|
|||
assetId: reportId,
|
||||
assetType: 'report_file',
|
||||
workspaceSharing: getReportWorkspaceSharing,
|
||||
requiredRole: ['full_access', 'owner'],
|
||||
requiredRole: 'full_access',
|
||||
});
|
||||
|
||||
// Get the report to verify it exists and get owner info
|
||||
|
|
|
@ -23,7 +23,7 @@ export async function createReportSharingHandler(
|
|||
assetId: reportId,
|
||||
assetType: 'report_file',
|
||||
workspaceSharing: getReportWorkspaceSharing,
|
||||
requiredRole: ['full_access', 'owner', 'can_edit'],
|
||||
requiredRole: 'can_edit',
|
||||
});
|
||||
|
||||
// Get the report to verify it exists
|
||||
|
|
|
@ -67,6 +67,10 @@ describe('checkAssetPublicAccess', () => {
|
|||
requiredRole: 'can_view',
|
||||
organizationId: mockOrganizationId,
|
||||
workspaceSharing: mockWorkspaceSharing,
|
||||
publiclyAccessible: false,
|
||||
publicExpiryDate: undefined,
|
||||
publicPassword: undefined,
|
||||
userSuppliedPassword: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -88,6 +92,10 @@ describe('checkAssetPublicAccess', () => {
|
|||
requiredRole: 'can_edit',
|
||||
organizationId: mockOrganizationId,
|
||||
workspaceSharing: mockWorkspaceSharing,
|
||||
publiclyAccessible: false,
|
||||
publicExpiryDate: undefined,
|
||||
publicPassword: undefined,
|
||||
userSuppliedPassword: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -378,6 +386,10 @@ describe('checkAssetPublicAccess', () => {
|
|||
requiredRole: 'can_view',
|
||||
organizationId: mockOrganizationId,
|
||||
workspaceSharing: mockWorkspaceSharing,
|
||||
publiclyAccessible: true,
|
||||
publicExpiryDate: undefined,
|
||||
publicPassword: undefined,
|
||||
userSuppliedPassword: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -472,6 +484,10 @@ describe('checkAssetPublicAccess', () => {
|
|||
requiredRole: 'can_edit',
|
||||
organizationId: 'org-123',
|
||||
workspaceSharing: 'can_view',
|
||||
publiclyAccessible: false,
|
||||
publicExpiryDate: undefined,
|
||||
publicPassword: undefined,
|
||||
userSuppliedPassword: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -42,6 +42,10 @@ export const checkAssetPublicAccess = async <T extends PublicAccessAsset>({
|
|||
requiredRole,
|
||||
organizationId,
|
||||
workspaceSharing,
|
||||
publiclyAccessible: asset.publicly_accessible ?? false,
|
||||
publicExpiryDate: asset.public_expiry_date ?? undefined,
|
||||
publicPassword: asset.public_password ?? undefined,
|
||||
userSuppliedPassword: password,
|
||||
});
|
||||
|
||||
if (!assetPermissionResult.hasAccess) {
|
||||
|
@ -80,7 +84,7 @@ export const checkIfAssetIsEditable = async ({
|
|||
assetType: AssetType;
|
||||
organizationId?: string;
|
||||
workspaceSharing: WorkspaceSharing | ((id: string) => Promise<WorkspaceSharing>);
|
||||
requiredRole?: AssetPermissionRole | AssetPermissionRole[];
|
||||
requiredRole?: AssetPermissionRole;
|
||||
}) => {
|
||||
const workspaceSharingResult =
|
||||
typeof workspaceSharing === 'function' ? await workspaceSharing(assetId) : workspaceSharing;
|
||||
|
|
|
@ -286,8 +286,9 @@ describe('metric-helpers', () => {
|
|||
const metricFile = createMockMetricFile({ publiclyAccessible: true });
|
||||
mockGetMetricFileById.mockResolvedValue(metricFile);
|
||||
mockCheckPermission.mockResolvedValue({
|
||||
hasAccess: false,
|
||||
effectiveRole: undefined,
|
||||
hasAccess: true,
|
||||
effectiveRole: 'can_view',
|
||||
accessPath: 'public',
|
||||
});
|
||||
|
||||
const options: MetricAccessOptions = { publicAccessPreviouslyVerified: false };
|
||||
|
@ -315,8 +316,8 @@ describe('metric-helpers', () => {
|
|||
const metricFile = createMockMetricFile({ publiclyAccessible: false });
|
||||
mockGetMetricFileById.mockResolvedValue(metricFile);
|
||||
mockCheckPermission.mockResolvedValue({
|
||||
hasAccess: false,
|
||||
effectiveRole: undefined,
|
||||
hasAccess: true,
|
||||
effectiveRole: 'can_view',
|
||||
});
|
||||
|
||||
const options: MetricAccessOptions = { publicAccessPreviouslyVerified: true };
|
||||
|
@ -344,7 +345,7 @@ describe('metric-helpers', () => {
|
|||
const options: MetricAccessOptions = { publicAccessPreviouslyVerified: false };
|
||||
|
||||
await expect(fetchAndProcessMetricData('metric-123', mockUser, options)).rejects.toThrow(
|
||||
new HTTPException(403, { message: 'Public access to this metric has expired' })
|
||||
new HTTPException(403, { message: "You don't have permission to view this metric" })
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -362,7 +363,7 @@ describe('metric-helpers', () => {
|
|||
const options: MetricAccessOptions = { publicAccessPreviouslyVerified: false };
|
||||
|
||||
await expect(fetchAndProcessMetricData('metric-123', mockUser, options)).rejects.toThrow(
|
||||
new HTTPException(418, { message: 'Password required for public access' })
|
||||
new HTTPException(403, { message: "You don't have permission to view this metric" })
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -383,7 +384,7 @@ describe('metric-helpers', () => {
|
|||
};
|
||||
|
||||
await expect(fetchAndProcessMetricData('metric-123', mockUser, options)).rejects.toThrow(
|
||||
new HTTPException(403, { message: 'Incorrect password for public access' })
|
||||
new HTTPException(403, { message: "You don't have permission to view this metric" })
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -394,8 +395,9 @@ describe('metric-helpers', () => {
|
|||
});
|
||||
mockGetMetricFileById.mockResolvedValue(metricFile);
|
||||
mockCheckPermission.mockResolvedValue({
|
||||
hasAccess: false,
|
||||
effectiveRole: undefined,
|
||||
hasAccess: true,
|
||||
effectiveRole: 'can_view',
|
||||
accessPath: 'public',
|
||||
});
|
||||
|
||||
const options: MetricAccessOptions = {
|
||||
|
|
|
@ -76,46 +76,18 @@ export async function fetchAndProcessMetricData(
|
|||
requiredRole: 'can_view',
|
||||
organizationId: metricFile.organizationId,
|
||||
workspaceSharing: metricFile.workspaceSharing || 'none',
|
||||
publiclyAccessible: metricFile.publiclyAccessible ?? false,
|
||||
publicExpiryDate: metricFile.publicExpiryDate ?? undefined,
|
||||
publicPassword: metricFile.publicPassword ?? undefined,
|
||||
userSuppliedPassword: password,
|
||||
});
|
||||
|
||||
effectiveRole = permissionResult.effectiveRole ? permissionResult.effectiveRole : effectiveRole;
|
||||
|
||||
// Check public access if needed
|
||||
if (!effectiveRole) {
|
||||
if (!metricFile.publiclyAccessible) {
|
||||
console.warn(`Permission denied for user ${user.id} to metric ${metricId}`);
|
||||
throw new HTTPException(403, {
|
||||
message: "You don't have permission to view this metric",
|
||||
});
|
||||
}
|
||||
|
||||
// Check if public access has expired
|
||||
const today = new Date();
|
||||
if (metricFile.publicExpiryDate && new Date(metricFile.publicExpiryDate) < today) {
|
||||
console.warn(`Public access expired for metric ${metricId}`);
|
||||
throw new HTTPException(403, {
|
||||
message: 'Public access to this metric has expired',
|
||||
});
|
||||
}
|
||||
|
||||
// Check password if required
|
||||
if (metricFile.publicPassword) {
|
||||
if (!password) {
|
||||
console.warn(`Public password required for metric ${metricId}`);
|
||||
throw new HTTPException(418, {
|
||||
message: 'Password required for public access',
|
||||
});
|
||||
}
|
||||
|
||||
if (password !== metricFile.publicPassword) {
|
||||
console.warn(`Incorrect public password for metric ${metricId}`);
|
||||
throw new HTTPException(403, {
|
||||
message: 'Incorrect password for public access',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
effectiveRole = 'can_view';
|
||||
if (!permissionResult.hasAccess || !effectiveRole) {
|
||||
throw new HTTPException(403, {
|
||||
message: "You don't have permission to view this metric",
|
||||
});
|
||||
}
|
||||
|
||||
// Parse version history
|
||||
|
|
|
@ -3,7 +3,7 @@ import type React from 'react';
|
|||
import { useMemo, useState } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { Button } from '@/components/ui/buttons';
|
||||
import { Paragraph, Text, Title } from '@/components/ui/typography';
|
||||
import { Paragraph, Title } from '@/components/ui/typography';
|
||||
import { useIsVersionChanged } from '@/context/AppVersion/useAppVersion';
|
||||
import { useMount } from '@/hooks/useMount';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
|
|
@ -2,3 +2,4 @@ export const BUSTER_HOME_PAGE = 'https://buster.so';
|
|||
export const BUSTER_DOCS_URL = 'https://docs.buster.so';
|
||||
export const BUSTER_GETTING_STARTED_URL = 'https://www.buster.so/get-started';
|
||||
export const BUSTER_DOCS_QUICKSTART = 'https://docs.buster.so/docs/getting-started/quickstart';
|
||||
export const BUSTER_SIGN_UP_URL = 'https://buster.so/sign-up';
|
||||
|
|
|
@ -18,6 +18,7 @@ export const useAppVersion = () => {
|
|||
...versionGetAppVersion,
|
||||
refetchOnReconnect: true,
|
||||
refetchOnMount: true,
|
||||
notifyOnChangeProps: ['data'],
|
||||
});
|
||||
const isChanged = checkNewVersion(data?.buildId);
|
||||
|
||||
|
@ -64,6 +65,7 @@ export const useIsVersionChanged = () => {
|
|||
const { data = false } = useQuery({
|
||||
...versionGetAppVersion,
|
||||
select: useCallback((data: { buildId: string }) => checkNewVersion(data.buildId), []),
|
||||
notifyOnChangeProps: ['data'],
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
|
|
@ -61,7 +61,6 @@ const PosthogWrapper: React.FC<PropsWithChildren> = ({ children }) => {
|
|||
import('posthog-js'),
|
||||
import('posthog-js/react'),
|
||||
]);
|
||||
console.log('posthog', posthog);
|
||||
|
||||
setPosthogModules({ posthog, PostHogProvider });
|
||||
} catch (error) {
|
||||
|
|
|
@ -2,6 +2,7 @@ import { useLocation, useNavigate } from '@tanstack/react-router';
|
|||
import { useEffect, useLayoutEffect, useRef } from 'react';
|
||||
import type { BusterChatResponseMessage_file } from '@/api/asset_interfaces/chat';
|
||||
import { useGetChatMessageMemoized } from '@/api/buster_rest/chats';
|
||||
import { useIsVersionChanged } from '@/context/AppVersion/useAppVersion';
|
||||
import { useHasLoadedChat } from '@/context/Chats/useGetChat';
|
||||
import {
|
||||
useGetChatMessageCompleted,
|
||||
|
@ -20,17 +21,15 @@ export const useAutoRedirectStreaming = ({
|
|||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const getChatMessageMemoized = useGetChatMessageMemoized();
|
||||
const versionChanged = useIsVersionChanged();
|
||||
const isStreamFinished = useGetChatMessageCompleted({ messageId: lastMessageId });
|
||||
const lastReasoningMessageId = useGetChatMessageLastReasoningMessageId({
|
||||
messageId: lastMessageId,
|
||||
});
|
||||
const isFinishedReasoning = useGetChatMessageIsFinishedReasoning({ messageId: lastMessageId });
|
||||
const hasResponseFile = useGetChatMessageHasResponseFile({ messageId: lastMessageId });
|
||||
|
||||
const previousIsCompletedStream = useRef<boolean>(isStreamFinished);
|
||||
|
||||
const hasLoadedChat = useHasLoadedChat({ chatId: chatId || '' });
|
||||
|
||||
const hasReasoning = !!lastReasoningMessageId;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
|
@ -63,7 +62,7 @@ export const useAutoRedirectStreaming = ({
|
|||
versionNumber: firstFile.version_number,
|
||||
});
|
||||
|
||||
navigate({ ...linkProps, replace: true });
|
||||
navigate({ ...linkProps, replace: true, reloadDocument: versionChanged });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,6 +75,7 @@ export const useAutoRedirectStreaming = ({
|
|||
messageId: lastMessageId,
|
||||
},
|
||||
replace: true,
|
||||
reloadDocument: versionChanged,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -94,7 +94,14 @@ export const useAutoRedirectStreaming = ({
|
|||
chatId,
|
||||
},
|
||||
replace: true,
|
||||
reloadDocument: versionChanged,
|
||||
});
|
||||
}
|
||||
}, [isStreamFinished, hasReasoning, hasResponseFile, chatId, lastMessageId, isFinishedReasoning]); //only use these values to trigger the useEffect
|
||||
|
||||
useEffect(() => {
|
||||
if (!isStreamFinished && versionChanged) {
|
||||
window.location.reload();
|
||||
}
|
||||
}, [isStreamFinished, versionChanged]);
|
||||
};
|
||||
|
|
|
@ -22,6 +22,7 @@ import { Route as InfoGettingStartedRouteImport } from './routes/info/getting-st
|
|||
import { Route as AuthResetPasswordRouteImport } from './routes/auth.reset-password'
|
||||
import { Route as AuthLogoutRouteImport } from './routes/auth.logout'
|
||||
import { Route as AuthLoginRouteImport } from './routes/auth.login'
|
||||
import { Route as AppThrow2RouteImport } from './routes/app.throw2'
|
||||
import { Route as AppThrowRouteImport } from './routes/app.throw'
|
||||
import { Route as AppSettingsRouteImport } from './routes/app/_settings'
|
||||
import { Route as AppAppRouteImport } from './routes/app/_app'
|
||||
|
@ -213,6 +214,11 @@ const AuthLoginRoute = AuthLoginRouteImport.update({
|
|||
path: '/login',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any)
|
||||
const AppThrow2Route = AppThrow2RouteImport.update({
|
||||
id: '/throw2',
|
||||
path: '/throw2',
|
||||
getParentRoute: () => AppRoute,
|
||||
} as any)
|
||||
const AppThrowRoute = AppThrowRouteImport.update({
|
||||
id: '/throw',
|
||||
path: '/throw',
|
||||
|
@ -946,6 +952,7 @@ export interface FileRoutesByFullPath {
|
|||
'/embed': typeof EmbedRouteWithChildren
|
||||
'/healthcheck': typeof HealthcheckRoute
|
||||
'/app/throw': typeof AppThrowRoute
|
||||
'/app/throw2': typeof AppThrow2Route
|
||||
'/auth/login': typeof AuthLoginRoute
|
||||
'/auth/logout': typeof AuthLogoutRoute
|
||||
'/auth/reset-password': typeof AuthResetPasswordRoute
|
||||
|
@ -1056,6 +1063,7 @@ export interface FileRoutesByTo {
|
|||
'/healthcheck': typeof HealthcheckRoute
|
||||
'/app': typeof AppSettingsRestricted_layoutAdmin_onlyRouteWithChildren
|
||||
'/app/throw': typeof AppThrowRoute
|
||||
'/app/throw2': typeof AppThrow2Route
|
||||
'/auth/login': typeof AuthLoginRoute
|
||||
'/auth/logout': typeof AuthLogoutRoute
|
||||
'/auth/reset-password': typeof AuthResetPasswordRoute
|
||||
|
@ -1151,6 +1159,7 @@ export interface FileRoutesById {
|
|||
'/app/_app': typeof AppAppRouteWithChildren
|
||||
'/app/_settings': typeof AppSettingsRouteWithChildren
|
||||
'/app/throw': typeof AppThrowRoute
|
||||
'/app/throw2': typeof AppThrow2Route
|
||||
'/auth/login': typeof AuthLoginRoute
|
||||
'/auth/logout': typeof AuthLogoutRoute
|
||||
'/auth/reset-password': typeof AuthResetPasswordRoute
|
||||
|
@ -1277,6 +1286,7 @@ export interface FileRouteTypes {
|
|||
| '/embed'
|
||||
| '/healthcheck'
|
||||
| '/app/throw'
|
||||
| '/app/throw2'
|
||||
| '/auth/login'
|
||||
| '/auth/logout'
|
||||
| '/auth/reset-password'
|
||||
|
@ -1387,6 +1397,7 @@ export interface FileRouteTypes {
|
|||
| '/healthcheck'
|
||||
| '/app'
|
||||
| '/app/throw'
|
||||
| '/app/throw2'
|
||||
| '/auth/login'
|
||||
| '/auth/logout'
|
||||
| '/auth/reset-password'
|
||||
|
@ -1481,6 +1492,7 @@ export interface FileRouteTypes {
|
|||
| '/app/_app'
|
||||
| '/app/_settings'
|
||||
| '/app/throw'
|
||||
| '/app/throw2'
|
||||
| '/auth/login'
|
||||
| '/auth/logout'
|
||||
| '/auth/reset-password'
|
||||
|
@ -1701,6 +1713,13 @@ declare module '@tanstack/react-router' {
|
|||
preLoaderRoute: typeof AuthLoginRouteImport
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
'/app/throw2': {
|
||||
id: '/app/throw2'
|
||||
path: '/throw2'
|
||||
fullPath: '/app/throw2'
|
||||
preLoaderRoute: typeof AppThrow2RouteImport
|
||||
parentRoute: typeof AppRoute
|
||||
}
|
||||
'/app/throw': {
|
||||
id: '/app/throw'
|
||||
path: '/throw'
|
||||
|
@ -3235,6 +3254,7 @@ interface AppRouteChildren {
|
|||
AppAppRoute: typeof AppAppRouteWithChildren
|
||||
AppSettingsRoute: typeof AppSettingsRouteWithChildren
|
||||
AppThrowRoute: typeof AppThrowRoute
|
||||
AppThrow2Route: typeof AppThrow2Route
|
||||
AppIndexRoute: typeof AppIndexRoute
|
||||
}
|
||||
|
||||
|
@ -3242,6 +3262,7 @@ const AppRouteChildren: AppRouteChildren = {
|
|||
AppAppRoute: AppAppRouteWithChildren,
|
||||
AppSettingsRoute: AppSettingsRouteWithChildren,
|
||||
AppThrowRoute: AppThrowRoute,
|
||||
AppThrow2Route: AppThrow2Route,
|
||||
AppIndexRoute: AppIndexRoute,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,23 +1,42 @@
|
|||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { useState } from 'react';
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { z } from 'zod';
|
||||
import { useMount } from '../hooks/useMount';
|
||||
|
||||
export const Route = createFileRoute('/app/throw')({
|
||||
validateSearch: z.object({
|
||||
iterations: z.number().optional(),
|
||||
}),
|
||||
component: RouteComponent,
|
||||
beforeLoad: ({ search }) => {
|
||||
return {
|
||||
iterations: search.iterations,
|
||||
};
|
||||
},
|
||||
loader: async ({ context }) => {
|
||||
return {
|
||||
iterations: context.iterations,
|
||||
};
|
||||
},
|
||||
head: ({ loaderData }) => {
|
||||
return {
|
||||
meta: [
|
||||
{
|
||||
title: `Throw ${loaderData?.iterations}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const [throwError, setThrowError] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const { iterations } = Route.useSearch();
|
||||
|
||||
useMount(() => {
|
||||
setTimeout(() => {
|
||||
setThrowError(true);
|
||||
navigate({ to: '/app/throw2', replace: true, search: { iterations: (iterations ?? 0) + 1 } });
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
if (throwError) {
|
||||
throw new Error('Nate is testing this error');
|
||||
}
|
||||
|
||||
return <div>Hello "/app/throw"! {throwError ? 'Throwing error' : 'Not throwing error'}</div>;
|
||||
return <div className="bg-red-100">Hello "/app/throw"! </div>;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||
import { z } from 'zod';
|
||||
import { useMount } from '@/hooks/useMount';
|
||||
|
||||
const searchParamsSchema = z.object({
|
||||
iterations: z.number().optional(),
|
||||
});
|
||||
|
||||
export const Route = createFileRoute('/app/throw2')({
|
||||
validateSearch: searchParamsSchema,
|
||||
component: RouteComponent,
|
||||
beforeLoad: ({ search }) => {
|
||||
return {
|
||||
iterations: search.iterations,
|
||||
};
|
||||
},
|
||||
loader: async ({ context }) => {
|
||||
return {
|
||||
iterations: context.iterations,
|
||||
};
|
||||
},
|
||||
head: ({ loaderData }) => {
|
||||
return {
|
||||
meta: [
|
||||
{
|
||||
title: `Throw ${loaderData?.iterations}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const navigate = useNavigate();
|
||||
const { iterations } = Route.useSearch();
|
||||
useMount(() => {
|
||||
setTimeout(() => {
|
||||
navigate({ to: '/app/throw', replace: true, search: { iterations: (iterations ?? 0) + 1 } });
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
return <div className="bg-blue-100">Hello "/app/throw2"!</div>;
|
||||
}
|
|
@ -3,6 +3,7 @@ import { prefetchGetMyUserInfo } from '@/api/buster_rest/users/queryRequests';
|
|||
import { getAppLayout } from '@/api/server-functions/getAppLayout';
|
||||
import { AppProviders } from '@/context/Providers';
|
||||
import { getSupabaseSession } from '@/integrations/supabase/getSupabaseUserClient';
|
||||
import { BUSTER_SIGN_UP_URL } from '../config/externalRoutes';
|
||||
|
||||
export const Route = createFileRoute('/app')({
|
||||
context: ({ context }) => ({ ...context, getAppLayout }),
|
||||
|
@ -27,7 +28,7 @@ export const Route = createFileRoute('/app')({
|
|||
try {
|
||||
const [user] = await Promise.all([prefetchGetMyUserInfo(queryClient)]);
|
||||
if (!user || !user.organizations || user.organizations.length === 0) {
|
||||
throw redirect({ href: 'https://buster.so/sign-up', replace: true, statusCode: 307 });
|
||||
throw redirect({ href: BUSTER_SIGN_UP_URL, replace: true, statusCode: 307 });
|
||||
}
|
||||
return {
|
||||
supabaseSession,
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { useEffect } from 'react';
|
||||
import { createFileRoute, redirect } from '@tanstack/react-router';
|
||||
import { BUSTER_SIGN_UP_URL } from '../../config/externalRoutes';
|
||||
|
||||
export const Route = createFileRoute('/info/getting-started')({
|
||||
component: GettingStartedPage,
|
||||
component: () => null,
|
||||
beforeLoad: () => {
|
||||
throw redirect({ href: BUSTER_SIGN_UP_URL, replace: true, statusCode: 307 });
|
||||
},
|
||||
});
|
||||
|
||||
export default function GettingStartedPage() {
|
||||
useEffect(() => {
|
||||
window.location.replace('https://buster.so/sign-up');
|
||||
}, []);
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,11 @@ import { hasAssetPermission } from './permissions';
|
|||
* Check if a user has access to a metric through any dashboard that contains it.
|
||||
* If a user has access to a dashboard (direct, public, or workspace), they can view the metrics in it.
|
||||
*/
|
||||
export async function checkMetricDashboardAccess(metricId: string, user: User): Promise<boolean> {
|
||||
export async function checkMetricDashboardAccess(
|
||||
metricId: string,
|
||||
user: User,
|
||||
userSuppliedPassword?: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
// Get all dashboards containing this metric with their workspace sharing info
|
||||
const dashboards = await checkDashboardsContainingMetric(metricId);
|
||||
|
@ -33,6 +37,10 @@ export async function checkMetricDashboardAccess(metricId: string, user: User):
|
|||
requiredRole: 'can_view' as AssetPermissionRole,
|
||||
organizationId: dashboard.organizationId,
|
||||
workspaceSharing: (dashboard.workspaceSharing as WorkspaceSharing) ?? 'none',
|
||||
publiclyAccessible: dashboard.publiclyAccessible,
|
||||
publicExpiryDate: dashboard.publicExpiryDate ?? undefined,
|
||||
publicPassword: dashboard.publicPassword ?? undefined,
|
||||
userSuppliedPassword: userSuppliedPassword,
|
||||
});
|
||||
|
||||
if (hasAccess) {
|
||||
|
@ -72,6 +80,10 @@ export async function checkMetricChatAccess(metricId: string, user: User): Promi
|
|||
requiredRole: 'can_view' as AssetPermissionRole,
|
||||
organizationId: chat.organizationId,
|
||||
workspaceSharing: (chat.workspaceSharing as WorkspaceSharing) ?? 'none',
|
||||
publiclyAccessible: chat.publiclyAccessible,
|
||||
publicExpiryDate: chat.publicExpiryDate ?? undefined,
|
||||
publicPassword: undefined, // We don't support passwords on the chats table
|
||||
userSuppliedPassword: undefined, // We don't support passwords on the chats table
|
||||
});
|
||||
|
||||
if (hasAccess) {
|
||||
|
@ -93,7 +105,11 @@ export async function checkMetricChatAccess(metricId: string, user: User): Promi
|
|||
* Check if a user has access to a metric through any report that contains it.
|
||||
* If a user has access to a report (direct, public, or workspace), they can view the metrics in it.
|
||||
*/
|
||||
export async function checkMetricReportAccess(metricId: string, user: User): Promise<boolean> {
|
||||
export async function checkMetricReportAccess(
|
||||
metricId: string,
|
||||
user: User,
|
||||
userSuppliedPassword?: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
// Get all reports containing this metric with their workspace sharing info
|
||||
const reports = await checkReportsContainingMetric(metricId);
|
||||
|
@ -111,6 +127,10 @@ export async function checkMetricReportAccess(metricId: string, user: User): Pro
|
|||
requiredRole: 'can_view' as AssetPermissionRole,
|
||||
organizationId: report.organizationId,
|
||||
workspaceSharing: (report.workspaceSharing as WorkspaceSharing) ?? 'none',
|
||||
publiclyAccessible: report.publiclyAccessible,
|
||||
publicExpiryDate: report.publicExpiryDate ?? undefined,
|
||||
publicPassword: report.publicPassword ?? undefined,
|
||||
userSuppliedPassword: userSuppliedPassword,
|
||||
});
|
||||
|
||||
if (hasAccess) {
|
||||
|
@ -150,6 +170,10 @@ export async function checkDashboardChatAccess(dashboardId: string, user: User):
|
|||
requiredRole: 'can_view' as AssetPermissionRole,
|
||||
organizationId: chat.organizationId,
|
||||
workspaceSharing: (chat.workspaceSharing as WorkspaceSharing) ?? 'none',
|
||||
publiclyAccessible: chat.publiclyAccessible,
|
||||
publicExpiryDate: chat.publicExpiryDate ?? undefined,
|
||||
publicPassword: undefined, // We don't support passwords on the chats table
|
||||
userSuppliedPassword: undefined, // We don't support passwords on the chats table
|
||||
});
|
||||
|
||||
if (hasAccess) {
|
||||
|
@ -294,7 +318,8 @@ export async function checkChatCollectionAccess(chatId: string, user: User): Pro
|
|||
export async function checkCascadingPermissions(
|
||||
assetId: string,
|
||||
assetType: AssetType,
|
||||
user: User
|
||||
user: User,
|
||||
userSuppliedPassword?: string
|
||||
): Promise<boolean> {
|
||||
// Check cache first
|
||||
const cached = getCachedCascadingPermission(user.id, assetId, assetType);
|
||||
|
@ -308,7 +333,11 @@ export async function checkCascadingPermissions(
|
|||
switch (assetType) {
|
||||
case 'metric_file': {
|
||||
// Check access through dashboards, chats, collections, and reports
|
||||
const dashboardAccess = await checkMetricDashboardAccess(assetId, user);
|
||||
const dashboardAccess = await checkMetricDashboardAccess(
|
||||
assetId,
|
||||
user,
|
||||
userSuppliedPassword
|
||||
);
|
||||
if (dashboardAccess) {
|
||||
hasAccess = true;
|
||||
break;
|
||||
|
@ -326,7 +355,7 @@ export async function checkCascadingPermissions(
|
|||
break;
|
||||
}
|
||||
|
||||
const reportAccess = await checkMetricReportAccess(assetId, user);
|
||||
const reportAccess = await checkMetricReportAccess(assetId, user, userSuppliedPassword);
|
||||
if (reportAccess) {
|
||||
hasAccess = true;
|
||||
break;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { checkPermission, computeEffectivePermission, hasAnyAccess } from './checks';
|
||||
import { checkPermission, computeEffectivePermission } from './checks';
|
||||
import type { AssetPermissionResult } from './checks';
|
||||
|
||||
// Mock database queries
|
||||
|
@ -259,33 +259,4 @@ describe('Asset Permission Checks', () => {
|
|||
expect(result).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasAnyAccess', () => {
|
||||
it('should check for minimum can_view permission', async () => {
|
||||
mockGetUserOrganizationsByUserId.mockResolvedValue([]);
|
||||
mockCheckAssetPermission.mockResolvedValue({
|
||||
hasAccess: true,
|
||||
role: 'can_view',
|
||||
accessPath: 'direct',
|
||||
});
|
||||
|
||||
const result = await hasAnyAccess('user123', 'asset123', 'dashboard_file');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockCheckAssetPermission).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return false if no access', async () => {
|
||||
mockGetCachedPermission.mockReturnValue(undefined);
|
||||
mockGetUserOrganizationsByUserId.mockResolvedValue([]);
|
||||
mockCheckAssetPermission.mockResolvedValue({
|
||||
hasAccess: false,
|
||||
});
|
||||
mockCheckCascadingPermissions.mockResolvedValue(false);
|
||||
|
||||
const result = await hasAnyAccess('user123', 'asset123', 'dashboard_file');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,30 +6,46 @@ import {
|
|||
import type { User } from '@buster/database/queries';
|
||||
import type { AssetType } from '@buster/database/schema-types';
|
||||
import type { AssetPermissionRole, OrganizationMembership, WorkspaceSharing } from '../types';
|
||||
import { getHighestPermission, isPermissionSufficientForAny } from '../types/asset-permissions';
|
||||
import { getHighestPermission, isPermissionSufficient } from '../types/asset-permissions';
|
||||
import { getCachedPermission, setCachedPermission } from './cache';
|
||||
import { checkCascadingPermissions } from './cascading-permissions';
|
||||
import { hasPublicAccess } from './public-access-check';
|
||||
|
||||
export interface AssetPermissionCheck {
|
||||
userId: string;
|
||||
assetId: string;
|
||||
assetType: AssetType;
|
||||
requiredRole: AssetPermissionRole | AssetPermissionRole[];
|
||||
requiredRole: AssetPermissionRole;
|
||||
organizationId?: string;
|
||||
workspaceSharing?: WorkspaceSharing;
|
||||
publiclyAccessible?: boolean;
|
||||
publicExpiryDate?: string | undefined;
|
||||
publicPassword?: string | undefined;
|
||||
userSuppliedPassword?: string | undefined;
|
||||
}
|
||||
|
||||
export interface AssetPermissionResult {
|
||||
hasAccess: boolean;
|
||||
effectiveRole?: AssetPermissionRole;
|
||||
accessPath?: 'direct' | 'workspace_sharing' | 'cascading' | 'admin';
|
||||
accessPath?: 'direct' | 'workspace_sharing' | 'cascading' | 'admin' | 'public';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user has sufficient permission to perform an action on an asset
|
||||
*/
|
||||
export async function checkPermission(check: AssetPermissionCheck): Promise<AssetPermissionResult> {
|
||||
const { userId, assetId, assetType, requiredRole, organizationId, workspaceSharing } = check;
|
||||
const {
|
||||
userId,
|
||||
assetId,
|
||||
assetType,
|
||||
requiredRole,
|
||||
organizationId,
|
||||
workspaceSharing,
|
||||
publiclyAccessible,
|
||||
publicExpiryDate,
|
||||
publicPassword,
|
||||
userSuppliedPassword,
|
||||
} = check;
|
||||
|
||||
// Check cache first (only for single role checks)
|
||||
if (!Array.isArray(requiredRole)) {
|
||||
|
@ -59,7 +75,7 @@ export async function checkPermission(check: AssetPermissionCheck): Promise<Asse
|
|||
|
||||
if (dbResult.hasAccess && dbResult.role) {
|
||||
// Check if the role is sufficient
|
||||
if (isPermissionSufficientForAny(dbResult.role, requiredRole)) {
|
||||
if (isPermissionSufficient(dbResult.role, requiredRole)) {
|
||||
const result: AssetPermissionResult = {
|
||||
hasAccess: true,
|
||||
effectiveRole: dbResult.role,
|
||||
|
@ -81,7 +97,7 @@ export async function checkPermission(check: AssetPermissionCheck): Promise<Asse
|
|||
|
||||
if (isOrgMember) {
|
||||
const workspaceRole = mapWorkspaceSharingToRole(workspaceSharing);
|
||||
if (workspaceRole && isPermissionSufficientForAny(workspaceRole, requiredRole)) {
|
||||
if (workspaceRole && isPermissionSufficient(workspaceRole, requiredRole)) {
|
||||
const result = {
|
||||
hasAccess: true,
|
||||
effectiveRole: workspaceRole,
|
||||
|
@ -96,22 +112,44 @@ export async function checkPermission(check: AssetPermissionCheck): Promise<Asse
|
|||
}
|
||||
}
|
||||
|
||||
if (publiclyAccessible) {
|
||||
const hasPublicAccessCheck = hasPublicAccess(
|
||||
publiclyAccessible,
|
||||
publicExpiryDate,
|
||||
publicPassword,
|
||||
userSuppliedPassword
|
||||
);
|
||||
if (hasPublicAccessCheck) {
|
||||
const accessRole: AssetPermissionRole = 'can_view';
|
||||
if (isPermissionSufficient(accessRole, requiredRole)) {
|
||||
const result = {
|
||||
hasAccess: true,
|
||||
effectiveRole: accessRole,
|
||||
accessPath: 'public' as const,
|
||||
};
|
||||
setCachedPermission(userId, assetId, assetType, requiredRole, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check cascading permissions for specific asset types
|
||||
const requiredRoles = Array.isArray(requiredRole) ? requiredRole : [requiredRole];
|
||||
if (requiredRoles.includes('can_view')) {
|
||||
if (requiredRole === 'can_view') {
|
||||
// Create a user object for cascading permissions check
|
||||
const user: Pick<User, 'id'> = { id: userId };
|
||||
const hasCascadingAccess = await checkCascadingPermissions(assetId, assetType, user as User);
|
||||
const hasCascadingAccess = await checkCascadingPermissions(
|
||||
assetId,
|
||||
assetType,
|
||||
user as User,
|
||||
userSuppliedPassword
|
||||
);
|
||||
if (hasCascadingAccess) {
|
||||
const result = {
|
||||
hasAccess: true,
|
||||
effectiveRole: 'can_view' as AssetPermissionRole,
|
||||
accessPath: 'cascading' as const,
|
||||
};
|
||||
// Only cache single role checks
|
||||
if (!Array.isArray(requiredRole)) {
|
||||
setCachedPermission(userId, assetId, assetType, requiredRole, result);
|
||||
}
|
||||
setCachedPermission(userId, assetId, assetType, requiredRole, result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@ -177,21 +215,3 @@ function mapWorkspaceSharingToRole(workspaceSharing: WorkspaceSharing): AssetPer
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user has any access to an asset (simplified check)
|
||||
*/
|
||||
export async function hasAnyAccess(
|
||||
userId: string,
|
||||
assetId: string,
|
||||
assetType: AssetType
|
||||
): Promise<boolean> {
|
||||
const result = await checkPermission({
|
||||
userId,
|
||||
assetId,
|
||||
assetType,
|
||||
requiredRole: 'can_view', // Minimum permission level
|
||||
});
|
||||
|
||||
return result.hasAccess;
|
||||
}
|
||||
|
|
|
@ -207,6 +207,10 @@ export async function hasAssetPermission(params: {
|
|||
requiredRole: AssetPermissionRole;
|
||||
organizationId?: string;
|
||||
workspaceSharing?: WorkspaceSharing;
|
||||
publiclyAccessible?: boolean;
|
||||
publicExpiryDate?: string | undefined;
|
||||
publicPassword?: string | undefined;
|
||||
userSuppliedPassword?: string | undefined;
|
||||
}): Promise<boolean> {
|
||||
const { checkPermission } = await import('./checks');
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
export function hasPublicAccess(
|
||||
publiclyAccessible: boolean,
|
||||
publicExpiryDate?: string,
|
||||
publicPassword?: string,
|
||||
password?: string
|
||||
): boolean {
|
||||
if (!publiclyAccessible) {
|
||||
return false;
|
||||
}
|
||||
const today = new Date();
|
||||
if (publicExpiryDate && new Date(publicExpiryDate) < today) {
|
||||
return false;
|
||||
}
|
||||
if (publicPassword && publicPassword !== password) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import { db } from '@buster/database/connection';
|
||||
import { chats, messages } from '@buster/database/schema';
|
||||
import type { AssetType } from '@buster/database/schema-types';
|
||||
import { createTestChat, createTestMessage } from '@buster/test-utils';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
@ -50,7 +51,7 @@ describe('markMessageComplete integration test', () => {
|
|||
it('should update chat with selected file information', async () => {
|
||||
const selectedFile = {
|
||||
fileId: '123e4567-e89b-12d3-a456-426614174000',
|
||||
fileType: 'dashboard_file',
|
||||
fileType: 'dashboard_file' as AssetType,
|
||||
versionNumber: 1,
|
||||
};
|
||||
|
||||
|
@ -109,7 +110,7 @@ describe('markMessageComplete integration test', () => {
|
|||
it('should not update chat when chatId is missing', async () => {
|
||||
const selectedFile = {
|
||||
fileId: '123e4567-e89b-12d3-a456-426614174000',
|
||||
fileType: 'metric_file',
|
||||
fileType: 'metric_file' as AssetType,
|
||||
versionNumber: 2,
|
||||
};
|
||||
|
||||
|
|
|
@ -508,6 +508,81 @@ describe('done-tool-file-selection - report filtering functionality', () => {
|
|||
});
|
||||
|
||||
describe('BUG-1885: Metrics used in reports appearing in response messages', () => {
|
||||
it('should filter out metric when content has escaped quotes (actual production bug)', () => {
|
||||
// This test reproduces the EXACT bug scenario from production where escaped quotes
|
||||
// in the report content were causing the metric ID regex to fail
|
||||
const messages: ModelMessage[] = [
|
||||
// Create a metric first
|
||||
{
|
||||
role: 'tool',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-result',
|
||||
toolName: 'createMetrics',
|
||||
toolCallId: 'toolu_01R7jSBNXiNd1mdN162Kk5To',
|
||||
output: {
|
||||
type: 'json',
|
||||
value: {
|
||||
files: [
|
||||
{
|
||||
id: '229f7b5d-c660-42a9-b4f2-46a0bf1f8726',
|
||||
name: 'Total Customers',
|
||||
version_number: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// Create report with ESCAPED quotes in the content (as seen in production)
|
||||
{
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-call',
|
||||
toolName: 'createReports',
|
||||
toolCallId: 'toolu_019dwAk5Ls2XHMpCfZVJg9z3',
|
||||
input: {
|
||||
name: 'Total Customers',
|
||||
// This is the exact format from production with escaped quotes
|
||||
content:
|
||||
'<metric metricId=\\"229f7b5d-c660-42a9-b4f2-46a0bf1f8726\\"/>\\n\\nAdventure Works has **19,820 total customers** in their database.',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'tool',
|
||||
content: [
|
||||
{
|
||||
type: 'tool-result',
|
||||
toolName: 'createReports',
|
||||
toolCallId: 'toolu_019dwAk5Ls2XHMpCfZVJg9z3',
|
||||
output: {
|
||||
type: 'json',
|
||||
value: {
|
||||
file: {
|
||||
id: 'a41ae0e9-2215-4ba8-8045-7b5e68e6f4b8',
|
||||
name: 'Total Customers',
|
||||
version_number: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const files = extractFilesFromToolCalls(messages);
|
||||
|
||||
// The metric should be filtered out because it's referenced in the report
|
||||
// The report should also be filtered out (all reports are filtered)
|
||||
expect(files).toHaveLength(0);
|
||||
expect(files.find((f) => f.id === '229f7b5d-c660-42a9-b4f2-46a0bf1f8726')).toBeUndefined();
|
||||
expect(files.find((f) => f.id === 'a41ae0e9-2215-4ba8-8045-7b5e68e6f4b8')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should filter out metric when using new single-file report structure (user reported bug)', () => {
|
||||
// This test reproduces the exact bug scenario reported by the user
|
||||
const messages: ModelMessage[] = [
|
||||
|
|
|
@ -18,10 +18,12 @@ import {
|
|||
} from '../../../visualization-tools/metrics/modify-metrics-tool/modify-metrics-tool';
|
||||
import {
|
||||
CREATE_REPORTS_TOOL_NAME,
|
||||
type CreateReportsInput,
|
||||
type CreateReportsOutput,
|
||||
} from '../../../visualization-tools/reports/create-reports-tool/create-reports-tool';
|
||||
import {
|
||||
MODIFY_REPORTS_TOOL_NAME,
|
||||
type ModifyReportsInput,
|
||||
type ModifyReportsOutput,
|
||||
} from '../../../visualization-tools/reports/modify-reports-tool/modify-reports-tool';
|
||||
|
||||
|
@ -184,6 +186,56 @@ export function extractAllFilesForChatUpdate(messages: ModelMessage[]): Extracte
|
|||
function extractReferencedMetricIds(messages: ModelMessage[]): Set<string> {
|
||||
const referencedMetricIds = new Set<string>();
|
||||
|
||||
// First, collect all metric IDs that exist from tool results
|
||||
const allMetricIds = new Set<string>();
|
||||
|
||||
for (const message of messages) {
|
||||
if (message.role === 'tool') {
|
||||
const toolContent = message.content;
|
||||
|
||||
if (Array.isArray(toolContent)) {
|
||||
for (const content of toolContent) {
|
||||
if (
|
||||
content &&
|
||||
typeof content === 'object' &&
|
||||
'type' in content &&
|
||||
content.type === 'tool-result'
|
||||
) {
|
||||
const toolName = (content as unknown as Record<string, unknown>).toolName;
|
||||
const output = (content as unknown as Record<string, unknown>).output;
|
||||
|
||||
if (
|
||||
(toolName === CREATE_METRICS_TOOL_NAME || toolName === MODIFY_METRICS_TOOL_NAME) &&
|
||||
output
|
||||
) {
|
||||
const outputObj = output as Record<string, unknown>;
|
||||
if (outputObj.type === 'json' && outputObj.value) {
|
||||
try {
|
||||
const parsedOutput =
|
||||
typeof outputObj.value === 'string'
|
||||
? JSON.parse(outputObj.value)
|
||||
: outputObj.value;
|
||||
|
||||
const metricsOutput = parsedOutput as CreateMetricsOutput | ModifyMetricsOutput;
|
||||
if (metricsOutput.files && Array.isArray(metricsOutput.files)) {
|
||||
for (const file of metricsOutput.files) {
|
||||
if (file.id) {
|
||||
allMetricIds.add(file.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_error) {
|
||||
// Ignore parsing errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then check if any metric IDs appear in report content
|
||||
for (const message of messages) {
|
||||
if (message.role === 'assistant' && Array.isArray(message.content)) {
|
||||
for (const content of message.content) {
|
||||
|
@ -197,21 +249,19 @@ function extractReferencedMetricIds(messages: ModelMessage[]): Set<string> {
|
|||
const toolName = content.toolName;
|
||||
const input = (content as { input?: unknown }).input;
|
||||
|
||||
// Extract from CREATE_REPORTS
|
||||
// Extract from CREATE_REPORTS with type safety
|
||||
if (toolName === CREATE_REPORTS_TOOL_NAME && input) {
|
||||
const reportInput = input as {
|
||||
content?: string;
|
||||
// Type the input as CreateReportsInput with fallback for legacy
|
||||
const reportInput = input as Partial<CreateReportsInput> & {
|
||||
files?: Array<{ yml_content?: string; content?: string }>;
|
||||
};
|
||||
|
||||
// Handle new structure
|
||||
if (reportInput.content) {
|
||||
// Use a more flexible pattern that matches both UUID and simple IDs
|
||||
const metricIdPattern = /<metric\s+metricId\s*=\s*["']([a-zA-Z0-9-]+)["']\s*\/>/gi;
|
||||
const matches = reportInput.content.matchAll(metricIdPattern);
|
||||
for (const match of matches) {
|
||||
if (match[1]) {
|
||||
referencedMetricIds.add(match[1]);
|
||||
// Simply check if any metric ID appears in the content
|
||||
for (const metricId of allMetricIds) {
|
||||
if (reportInput.content.includes(metricId)) {
|
||||
referencedMetricIds.add(metricId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -221,12 +271,10 @@ function extractReferencedMetricIds(messages: ModelMessage[]): Set<string> {
|
|||
for (const file of reportInput.files) {
|
||||
const content = file.yml_content || file.content;
|
||||
if (content) {
|
||||
const metricIdPattern =
|
||||
/<metric\s+metricId\s*=\s*["']([a-zA-Z0-9-]+)["']\s*\/>/gi;
|
||||
const matches = content.matchAll(metricIdPattern);
|
||||
for (const match of matches) {
|
||||
if (match[1]) {
|
||||
referencedMetricIds.add(match[1]);
|
||||
// Simply check if any metric ID appears in the content
|
||||
for (const metricId of allMetricIds) {
|
||||
if (content.includes(metricId)) {
|
||||
referencedMetricIds.add(metricId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -234,24 +282,28 @@ function extractReferencedMetricIds(messages: ModelMessage[]): Set<string> {
|
|||
}
|
||||
}
|
||||
|
||||
// Extract from MODIFY_REPORTS
|
||||
// Extract from MODIFY_REPORTS with type safety
|
||||
if (toolName === MODIFY_REPORTS_TOOL_NAME && input) {
|
||||
const modifyInput = input as {
|
||||
edits?: Array<{ code?: string; new_content?: string; content?: string }>;
|
||||
// Type the input as ModifyReportsInput with support for variant field names
|
||||
const modifyInput = input as Partial<ModifyReportsInput> & {
|
||||
edits?: Array<{
|
||||
operation?: 'replace' | 'append';
|
||||
code?: string;
|
||||
code_to_replace?: string;
|
||||
new_content?: string;
|
||||
content?: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
if (modifyInput.edits && Array.isArray(modifyInput.edits)) {
|
||||
for (const edit of modifyInput.edits) {
|
||||
// The field is 'code' based on the schema
|
||||
// Check all possible field names for content
|
||||
const content = edit.code || edit.new_content || edit.content;
|
||||
if (content) {
|
||||
// Use a more flexible pattern that matches both UUID and simple IDs
|
||||
const metricIdPattern =
|
||||
/<metric\s+metricId\s*=\s*["']([a-zA-Z0-9-]+)["']\s*\/>/gi;
|
||||
const matches = content.matchAll(metricIdPattern);
|
||||
for (const match of matches) {
|
||||
if (match[1]) {
|
||||
referencedMetricIds.add(match[1]);
|
||||
// Simply check if any metric ID appears in the content
|
||||
for (const metricId of allMetricIds) {
|
||||
if (content.includes(metricId)) {
|
||||
referencedMetricIds.add(metricId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ export interface ChatWithSharing {
|
|||
id: string;
|
||||
organizationId: string;
|
||||
workspaceSharing: WorkspaceSharing | null;
|
||||
publiclyAccessible: boolean;
|
||||
publicExpiryDate: string | null;
|
||||
}
|
||||
|
||||
export async function checkChatsContainingAsset(
|
||||
|
@ -18,6 +20,8 @@ export async function checkChatsContainingAsset(
|
|||
id: chats.id,
|
||||
organizationId: chats.organizationId,
|
||||
workspaceSharing: chats.workspaceSharing,
|
||||
publiclyAccessible: chats.publiclyAccessible,
|
||||
publicExpiryDate: chats.publicExpiryDate,
|
||||
})
|
||||
.from(messagesToFiles)
|
||||
.innerJoin(messages, eq(messages.id, messagesToFiles.messageId))
|
||||
|
|
|
@ -47,9 +47,30 @@ describe('checkDashboardsContainingMetric', () => {
|
|||
|
||||
it('should return dashboards with organizationId and workspaceSharing', async () => {
|
||||
const mockDashboards = [
|
||||
{ id: 'dash1', organizationId: 'org1', workspaceSharing: 'can_view' },
|
||||
{ id: 'dash2', organizationId: 'org2', workspaceSharing: 'none' },
|
||||
{ id: 'dash3', organizationId: 'org1', workspaceSharing: null },
|
||||
{
|
||||
id: 'dash1',
|
||||
organizationId: 'org1',
|
||||
workspaceSharing: 'can_view',
|
||||
publiclyAccessible: false,
|
||||
publicExpiryDate: null,
|
||||
publicPassword: null,
|
||||
},
|
||||
{
|
||||
id: 'dash2',
|
||||
organizationId: 'org2',
|
||||
workspaceSharing: 'none',
|
||||
publiclyAccessible: true,
|
||||
publicExpiryDate: '2024-12-31T23:59:59Z',
|
||||
publicPassword: 'secret456',
|
||||
},
|
||||
{
|
||||
id: 'dash3',
|
||||
organizationId: 'org1',
|
||||
workspaceSharing: null,
|
||||
publiclyAccessible: false,
|
||||
publicExpiryDate: null,
|
||||
publicPassword: null,
|
||||
},
|
||||
];
|
||||
|
||||
mockQueryChain.where.mockResolvedValue(mockDashboards);
|
||||
|
@ -61,6 +82,9 @@ describe('checkDashboardsContainingMetric', () => {
|
|||
id: expect.anything(),
|
||||
organizationId: expect.anything(),
|
||||
workspaceSharing: expect.anything(),
|
||||
publiclyAccessible: expect.anything(),
|
||||
publicExpiryDate: expect.anything(),
|
||||
publicPassword: expect.anything(),
|
||||
});
|
||||
expect(mockQueryChain.from).toHaveBeenCalled();
|
||||
expect(mockQueryChain.innerJoin).toHaveBeenCalled();
|
||||
|
@ -76,21 +100,67 @@ describe('checkDashboardsContainingMetric', () => {
|
|||
});
|
||||
|
||||
it('should handle null workspace sharing values', async () => {
|
||||
const mockDashboards = [{ id: 'dash1', organizationId: 'org1', workspaceSharing: null }];
|
||||
const mockDashboards = [
|
||||
{
|
||||
id: 'dash1',
|
||||
organizationId: 'org1',
|
||||
workspaceSharing: null,
|
||||
publiclyAccessible: false,
|
||||
publicExpiryDate: null,
|
||||
publicPassword: null,
|
||||
},
|
||||
];
|
||||
|
||||
mockQueryChain.where.mockResolvedValue(mockDashboards);
|
||||
|
||||
const result = await checkDashboardsContainingMetric('metric123');
|
||||
|
||||
expect(result).toEqual([{ id: 'dash1', organizationId: 'org1', workspaceSharing: null }]);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: 'dash1',
|
||||
organizationId: 'org1',
|
||||
workspaceSharing: null,
|
||||
publiclyAccessible: false,
|
||||
publicExpiryDate: null,
|
||||
publicPassword: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle all workspace sharing levels', async () => {
|
||||
const mockDashboards = [
|
||||
{ id: 'dash1', organizationId: 'org1', workspaceSharing: 'none' },
|
||||
{ id: 'dash2', organizationId: 'org1', workspaceSharing: 'can_view' },
|
||||
{ id: 'dash3', organizationId: 'org1', workspaceSharing: 'can_edit' },
|
||||
{ id: 'dash4', organizationId: 'org1', workspaceSharing: 'full_access' },
|
||||
{
|
||||
id: 'dash1',
|
||||
organizationId: 'org1',
|
||||
workspaceSharing: 'none',
|
||||
publiclyAccessible: false,
|
||||
publicExpiryDate: null,
|
||||
publicPassword: null,
|
||||
},
|
||||
{
|
||||
id: 'dash2',
|
||||
organizationId: 'org1',
|
||||
workspaceSharing: 'can_view',
|
||||
publiclyAccessible: false,
|
||||
publicExpiryDate: null,
|
||||
publicPassword: null,
|
||||
},
|
||||
{
|
||||
id: 'dash3',
|
||||
organizationId: 'org1',
|
||||
workspaceSharing: 'can_edit',
|
||||
publiclyAccessible: false,
|
||||
publicExpiryDate: null,
|
||||
publicPassword: null,
|
||||
},
|
||||
{
|
||||
id: 'dash4',
|
||||
organizationId: 'org1',
|
||||
workspaceSharing: 'full_access',
|
||||
publiclyAccessible: false,
|
||||
publicExpiryDate: null,
|
||||
publicPassword: null,
|
||||
},
|
||||
];
|
||||
|
||||
mockQueryChain.where.mockResolvedValue(mockDashboards);
|
||||
|
|
|
@ -7,6 +7,9 @@ export interface DashboardWithSharing {
|
|||
id: string;
|
||||
organizationId: string;
|
||||
workspaceSharing: WorkspaceSharing | null;
|
||||
publiclyAccessible: boolean;
|
||||
publicExpiryDate: string | null;
|
||||
publicPassword: string | null;
|
||||
}
|
||||
|
||||
export async function checkDashboardsContainingMetric(
|
||||
|
@ -17,6 +20,9 @@ export async function checkDashboardsContainingMetric(
|
|||
id: dashboardFiles.id,
|
||||
organizationId: dashboardFiles.organizationId,
|
||||
workspaceSharing: dashboardFiles.workspaceSharing,
|
||||
publiclyAccessible: dashboardFiles.publiclyAccessible,
|
||||
publicExpiryDate: dashboardFiles.publicExpiryDate,
|
||||
publicPassword: dashboardFiles.publicPassword,
|
||||
})
|
||||
.from(metricFilesToDashboardFiles)
|
||||
.innerJoin(dashboardFiles, eq(dashboardFiles.id, metricFilesToDashboardFiles.dashboardFileId))
|
||||
|
|
|
@ -47,9 +47,30 @@ describe('checkReportsContainingMetric', () => {
|
|||
|
||||
it('should return reports with organizationId and workspaceSharing', async () => {
|
||||
const mockReports = [
|
||||
{ id: 'report1', organizationId: 'org1', workspaceSharing: 'can_view' },
|
||||
{ id: 'report2', organizationId: 'org2', workspaceSharing: 'full_access' },
|
||||
{ id: 'report3', organizationId: 'org1', workspaceSharing: null },
|
||||
{
|
||||
id: 'report1',
|
||||
organizationId: 'org1',
|
||||
workspaceSharing: 'can_view',
|
||||
publiclyAccessible: false,
|
||||
publicExpiryDate: null,
|
||||
publicPassword: null,
|
||||
},
|
||||
{
|
||||
id: 'report2',
|
||||
organizationId: 'org2',
|
||||
workspaceSharing: 'full_access',
|
||||
publiclyAccessible: true,
|
||||
publicExpiryDate: '2024-12-31T23:59:59Z',
|
||||
publicPassword: 'secret123',
|
||||
},
|
||||
{
|
||||
id: 'report3',
|
||||
organizationId: 'org1',
|
||||
workspaceSharing: null,
|
||||
publiclyAccessible: false,
|
||||
publicExpiryDate: null,
|
||||
publicPassword: null,
|
||||
},
|
||||
];
|
||||
|
||||
mockQueryChain.where.mockResolvedValue(mockReports);
|
||||
|
@ -61,6 +82,9 @@ describe('checkReportsContainingMetric', () => {
|
|||
id: expect.anything(),
|
||||
organizationId: expect.anything(),
|
||||
workspaceSharing: expect.anything(),
|
||||
publiclyAccessible: expect.anything(),
|
||||
publicExpiryDate: expect.anything(),
|
||||
publicPassword: expect.anything(),
|
||||
});
|
||||
expect(mockQueryChain.from).toHaveBeenCalled();
|
||||
expect(mockQueryChain.innerJoin).toHaveBeenCalled();
|
||||
|
@ -76,21 +100,67 @@ describe('checkReportsContainingMetric', () => {
|
|||
});
|
||||
|
||||
it('should handle null workspace sharing values', async () => {
|
||||
const mockReports = [{ id: 'report1', organizationId: 'org1', workspaceSharing: null }];
|
||||
const mockReports = [
|
||||
{
|
||||
id: 'report1',
|
||||
organizationId: 'org1',
|
||||
workspaceSharing: null,
|
||||
publiclyAccessible: false,
|
||||
publicExpiryDate: null,
|
||||
publicPassword: null,
|
||||
},
|
||||
];
|
||||
|
||||
mockQueryChain.where.mockResolvedValue(mockReports);
|
||||
|
||||
const result = await checkReportsContainingMetric('metric123');
|
||||
|
||||
expect(result).toEqual([{ id: 'report1', organizationId: 'org1', workspaceSharing: null }]);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: 'report1',
|
||||
organizationId: 'org1',
|
||||
workspaceSharing: null,
|
||||
publiclyAccessible: false,
|
||||
publicExpiryDate: null,
|
||||
publicPassword: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle all workspace sharing levels', async () => {
|
||||
const mockReports = [
|
||||
{ id: 'report1', organizationId: 'org1', workspaceSharing: 'none' },
|
||||
{ id: 'report2', organizationId: 'org1', workspaceSharing: 'can_view' },
|
||||
{ id: 'report3', organizationId: 'org1', workspaceSharing: 'can_edit' },
|
||||
{ id: 'report4', organizationId: 'org1', workspaceSharing: 'full_access' },
|
||||
{
|
||||
id: 'report1',
|
||||
organizationId: 'org1',
|
||||
workspaceSharing: 'none',
|
||||
publiclyAccessible: false,
|
||||
publicExpiryDate: null,
|
||||
publicPassword: null,
|
||||
},
|
||||
{
|
||||
id: 'report2',
|
||||
organizationId: 'org1',
|
||||
workspaceSharing: 'can_view',
|
||||
publiclyAccessible: false,
|
||||
publicExpiryDate: null,
|
||||
publicPassword: null,
|
||||
},
|
||||
{
|
||||
id: 'report3',
|
||||
organizationId: 'org1',
|
||||
workspaceSharing: 'can_edit',
|
||||
publiclyAccessible: false,
|
||||
publicExpiryDate: null,
|
||||
publicPassword: null,
|
||||
},
|
||||
{
|
||||
id: 'report4',
|
||||
organizationId: 'org1',
|
||||
workspaceSharing: 'full_access',
|
||||
publiclyAccessible: false,
|
||||
publicExpiryDate: null,
|
||||
publicPassword: null,
|
||||
},
|
||||
];
|
||||
|
||||
mockQueryChain.where.mockResolvedValue(mockReports);
|
||||
|
@ -103,7 +173,16 @@ describe('checkReportsContainingMetric', () => {
|
|||
it('should filter out deleted reports and deleted relationships', async () => {
|
||||
// This test validates that the query conditions are set up correctly
|
||||
// In a real implementation, deleted items would be filtered by the database
|
||||
const activeReports = [{ id: 'report1', organizationId: 'org1', workspaceSharing: 'can_view' }];
|
||||
const activeReports = [
|
||||
{
|
||||
id: 'report1',
|
||||
organizationId: 'org1',
|
||||
workspaceSharing: 'can_view',
|
||||
publiclyAccessible: false,
|
||||
publicExpiryDate: null,
|
||||
publicPassword: null,
|
||||
},
|
||||
];
|
||||
|
||||
mockQueryChain.where.mockResolvedValue(activeReports);
|
||||
|
||||
|
|
|
@ -7,6 +7,9 @@ export interface ReportWithSharing {
|
|||
id: string;
|
||||
organizationId: string;
|
||||
workspaceSharing: WorkspaceSharing | null;
|
||||
publiclyAccessible: boolean;
|
||||
publicExpiryDate: string | null;
|
||||
publicPassword: string | null;
|
||||
}
|
||||
|
||||
export async function checkReportsContainingMetric(metricId: string): Promise<ReportWithSharing[]> {
|
||||
|
@ -15,6 +18,9 @@ export async function checkReportsContainingMetric(metricId: string): Promise<Re
|
|||
id: reportFiles.id,
|
||||
organizationId: reportFiles.organizationId,
|
||||
workspaceSharing: reportFiles.workspaceSharing,
|
||||
publiclyAccessible: reportFiles.publiclyAccessible,
|
||||
publicExpiryDate: reportFiles.publicExpiryDate,
|
||||
publicPassword: reportFiles.publicPassword,
|
||||
})
|
||||
.from(metricFilesToReportFiles)
|
||||
.innerJoin(reportFiles, eq(reportFiles.id, metricFilesToReportFiles.reportFileId))
|
||||
|
|
|
@ -24,6 +24,9 @@ export async function getReportMetadata(params: GetReportMetadataInput) {
|
|||
.select({
|
||||
organizationId: reportFiles.organizationId,
|
||||
workspaceSharing: reportFiles.workspaceSharing,
|
||||
publiclyAccessible: reportFiles.publiclyAccessible,
|
||||
publicExpiryDate: reportFiles.publicExpiryDate,
|
||||
publicPassword: reportFiles.publicPassword,
|
||||
})
|
||||
.from(reportFiles)
|
||||
.where(and(eq(reportFiles.id, validated.reportId), isNull(reportFiles.deletedAt)))
|
||||
|
|
Loading…
Reference in New Issue