From ae082074d522f49efe6b8969fcaca92b6f623e16 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Fri, 13 Feb 2026 13:43:07 -0800 Subject: [PATCH 01/16] refactor(trpc): extract shared tRPC client setup to @superset/trpc/client Move duplicate tRPC client code (react.tsx, server.tsx, query-client.ts) from apps/web and apps/admin into packages/trpc/src/client/ with factory functions. Each app now uses thin wrappers that pass env-specific config. --- apps/admin/src/trpc/react.tsx | 71 ++----------------- apps/admin/src/trpc/server.tsx | 17 ++--- apps/web/src/trpc/query-client.ts | 33 --------- apps/web/src/trpc/react.tsx | 66 ++--------------- apps/web/src/trpc/server.tsx | 17 ++--- bun.lock | 5 ++ packages/trpc/package.json | 17 +++++ .../trpc/src/client}/query-client.ts | 0 packages/trpc/src/client/react.tsx | 69 ++++++++++++++++++ packages/trpc/src/client/server.ts | 23 ++++++ packages/trpc/tsconfig.json | 3 +- 11 files changed, 136 insertions(+), 185 deletions(-) delete mode 100644 apps/web/src/trpc/query-client.ts rename {apps/admin/src/trpc => packages/trpc/src/client}/query-client.ts (100%) create mode 100644 packages/trpc/src/client/react.tsx create mode 100644 packages/trpc/src/client/server.ts diff --git a/apps/admin/src/trpc/react.tsx b/apps/admin/src/trpc/react.tsx index b654b409fe1..417b58919ab 100644 --- a/apps/admin/src/trpc/react.tsx +++ b/apps/admin/src/trpc/react.tsx @@ -1,70 +1,11 @@ "use client"; -import type { AppRouter } from "@superset/trpc"; -import type { QueryClient } from "@tanstack/react-query"; -import { QueryClientProvider } from "@tanstack/react-query"; -import { - createTRPCClient, - httpBatchStreamLink, - loggerLink, -} from "@trpc/client"; -import { createTRPCContext } from "@trpc/tanstack-react-query"; -import { useState } from "react"; -import SuperJSON from "superjson"; +export { type UseTRPC, useTRPC } from "@superset/trpc/client/react"; +import { createTRPCReactProvider } from "@superset/trpc/client/react"; import { env } from "../env"; -import { createQueryClient } from "./query-client"; -let clientQueryClientSingleton: QueryClient | undefined; -const getQueryClient = () => { - if (typeof window === "undefined") { - return createQueryClient(); - } - if (!clientQueryClientSingleton) { - clientQueryClientSingleton = createQueryClient(); - } - return clientQueryClientSingleton; -}; - -const context = createTRPCContext(); -export const { useTRPC, TRPCProvider } = context; -export type UseTRPC = typeof useTRPC; - -export function TRPCReactProvider(props: { children: React.ReactNode }) { - const queryClient = getQueryClient(); - - const [trpcClient] = useState(() => - createTRPCClient({ - links: [ - loggerLink({ - enabled: (op) => - env.NODE_ENV === "development" || - (op.direction === "down" && op.result instanceof Error), - }), - httpBatchStreamLink({ - transformer: SuperJSON, - url: `${env.NEXT_PUBLIC_API_URL}/api/trpc`, - headers() { - return { - "x-trpc-source": "nextjs-react", - }; - }, - fetch(url, options) { - return fetch(url, { - ...options, - credentials: "include", - }); - }, - }), - ], - }), - ); - - return ( - - - {props.children} - - - ); -} +export const TRPCReactProvider = createTRPCReactProvider({ + apiUrl: env.NEXT_PUBLIC_API_URL, + isDev: env.NODE_ENV === "development", +}); diff --git a/apps/admin/src/trpc/server.tsx b/apps/admin/src/trpc/server.tsx index 3042598abba..75258674a29 100644 --- a/apps/admin/src/trpc/server.tsx +++ b/apps/admin/src/trpc/server.tsx @@ -1,10 +1,8 @@ import "server-only"; -import type { AppRouter } from "@superset/trpc"; -import { createTRPCClient, httpBatchLink } from "@trpc/client"; +import { createServerTRPCClient } from "@superset/trpc/client/server"; import { headers } from "next/headers"; import { cache } from "react"; -import SuperJSON from "superjson"; import { env } from "../env"; @@ -12,15 +10,8 @@ export const api = cache(async () => { const heads = new Headers(await headers()); heads.set("x-trpc-source", "rsc"); - return createTRPCClient({ - links: [ - httpBatchLink({ - transformer: SuperJSON, - url: `${env.NEXT_PUBLIC_API_URL}/api/trpc`, - headers() { - return Object.fromEntries(heads.entries()); - }, - }), - ], + return createServerTRPCClient({ + apiUrl: env.NEXT_PUBLIC_API_URL, + headers: heads, }); }); diff --git a/apps/web/src/trpc/query-client.ts b/apps/web/src/trpc/query-client.ts deleted file mode 100644 index 5780964e5e4..00000000000 --- a/apps/web/src/trpc/query-client.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { - defaultShouldDehydrateQuery, - QueryClient, -} from "@tanstack/react-query"; -import SuperJSON from "superjson"; - -export const createQueryClient = () => - new QueryClient({ - defaultOptions: { - queries: { - // With SSR, we usually want to set some default staleTime - // above 0 to avoid refetching immediately on the client - staleTime: 30 * 1000, - }, - dehydrate: { - serializeData: SuperJSON.serialize, - shouldDehydrateQuery: (query) => - defaultShouldDehydrateQuery(query) || - query.state.status === "pending", - shouldRedactErrors: () => { - // We should not catch Next.js server errors - // as that's how Next.js detects dynamic pages - // so we cannot redact them. - // Next.js also automatically redacts errors for us - // with better digests. - return false; - }, - }, - hydrate: { - deserializeData: SuperJSON.deserialize, - }, - }, - }); diff --git a/apps/web/src/trpc/react.tsx b/apps/web/src/trpc/react.tsx index 78f7c07f9f0..417b58919ab 100644 --- a/apps/web/src/trpc/react.tsx +++ b/apps/web/src/trpc/react.tsx @@ -1,65 +1,11 @@ "use client"; -import type { AppRouter } from "@superset/trpc"; -import type { QueryClient } from "@tanstack/react-query"; -import { QueryClientProvider } from "@tanstack/react-query"; -import { - createTRPCClient, - httpBatchStreamLink, - loggerLink, -} from "@trpc/client"; -import { createTRPCContext } from "@trpc/tanstack-react-query"; -import { useState } from "react"; -import SuperJSON from "superjson"; +export { type UseTRPC, useTRPC } from "@superset/trpc/client/react"; +import { createTRPCReactProvider } from "@superset/trpc/client/react"; import { env } from "../env"; -import { createQueryClient } from "./query-client"; -let clientQueryClientSingleton: QueryClient | undefined; -const getQueryClient = () => { - if (typeof window === "undefined") { - return createQueryClient(); - } - if (!clientQueryClientSingleton) { - clientQueryClientSingleton = createQueryClient(); - } - return clientQueryClientSingleton; -}; - -const context = createTRPCContext(); -export const { useTRPC, TRPCProvider } = context; -export type UseTRPC = typeof useTRPC; - -export function TRPCReactProvider(props: { children: React.ReactNode }) { - const queryClient = getQueryClient(); - - const [trpcClient] = useState(() => - createTRPCClient({ - links: [ - loggerLink({ - enabled: (op) => - env.NODE_ENV === "development" || - (op.direction === "down" && op.result instanceof Error), - }), - httpBatchStreamLink({ - transformer: SuperJSON, - url: `${env.NEXT_PUBLIC_API_URL}/api/trpc`, - headers() { - return { "x-trpc-source": "nextjs-react" }; - }, - fetch(url, options) { - return fetch(url, { ...options, credentials: "include" }); - }, - }), - ], - }), - ); - - return ( - - - {props.children} - - - ); -} +export const TRPCReactProvider = createTRPCReactProvider({ + apiUrl: env.NEXT_PUBLIC_API_URL, + isDev: env.NODE_ENV === "development", +}); diff --git a/apps/web/src/trpc/server.tsx b/apps/web/src/trpc/server.tsx index 3042598abba..75258674a29 100644 --- a/apps/web/src/trpc/server.tsx +++ b/apps/web/src/trpc/server.tsx @@ -1,10 +1,8 @@ import "server-only"; -import type { AppRouter } from "@superset/trpc"; -import { createTRPCClient, httpBatchLink } from "@trpc/client"; +import { createServerTRPCClient } from "@superset/trpc/client/server"; import { headers } from "next/headers"; import { cache } from "react"; -import SuperJSON from "superjson"; import { env } from "../env"; @@ -12,15 +10,8 @@ export const api = cache(async () => { const heads = new Headers(await headers()); heads.set("x-trpc-source", "rsc"); - return createTRPCClient({ - links: [ - httpBatchLink({ - transformer: SuperJSON, - url: `${env.NEXT_PUBLIC_API_URL}/api/trpc`, - headers() { - return Object.fromEntries(heads.entries()); - }, - }), - ], + return createServerTRPCClient({ + apiUrl: env.NEXT_PUBLIC_API_URL, + headers: heads, }); }); diff --git a/bun.lock b/bun.lock index caf60e653df..3d96dee010a 100644 --- a/bun.lock +++ b/bun.lock @@ -701,16 +701,21 @@ "@superset/db": "workspace:*", "@superset/shared": "workspace:*", "@t3-oss/env-core": "^0.13.8", + "@tanstack/react-query": "^5.90.19", + "@trpc/client": "^11.7.1", "@trpc/server": "^11.7.1", + "@trpc/tanstack-react-query": "^11.7.1", "@upstash/qstash": "^2.8.4", "@vercel/blob": "^2.0.0", "@vercel/kv": "^3.0.0", "drizzle-orm": "0.45.1", + "react": "^19.2.0", "superjson": "^2.2.5", "zod": "^4.3.5", }, "devDependencies": { "@superset/typescript": "workspace:*", + "@types/react": "^19.1.8", "typescript": "^5.9.3", }, }, diff --git a/packages/trpc/package.json b/packages/trpc/package.json index 1939359989b..98b6863afb8 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -11,6 +11,18 @@ "./integrations/*": { "types": "./src/lib/integrations/*/index.ts", "default": "./src/lib/integrations/*/index.ts" + }, + "./client/query-client": { + "types": "./src/client/query-client.ts", + "default": "./src/client/query-client.ts" + }, + "./client/react": { + "types": "./src/client/react.tsx", + "default": "./src/client/react.tsx" + }, + "./client/server": { + "types": "./src/client/server.ts", + "default": "./src/client/server.ts" } }, "scripts": { @@ -24,16 +36,21 @@ "@superset/db": "workspace:*", "@superset/shared": "workspace:*", "@t3-oss/env-core": "^0.13.8", + "@tanstack/react-query": "^5.90.19", + "@trpc/client": "^11.7.1", "@trpc/server": "^11.7.1", + "@trpc/tanstack-react-query": "^11.7.1", "@upstash/qstash": "^2.8.4", "@vercel/blob": "^2.0.0", "@vercel/kv": "^3.0.0", "drizzle-orm": "0.45.1", + "react": "^19.2.0", "superjson": "^2.2.5", "zod": "^4.3.5" }, "devDependencies": { "@superset/typescript": "workspace:*", + "@types/react": "^19.1.8", "typescript": "^5.9.3" } } diff --git a/apps/admin/src/trpc/query-client.ts b/packages/trpc/src/client/query-client.ts similarity index 100% rename from apps/admin/src/trpc/query-client.ts rename to packages/trpc/src/client/query-client.ts diff --git a/packages/trpc/src/client/react.tsx b/packages/trpc/src/client/react.tsx new file mode 100644 index 00000000000..c460557d01a --- /dev/null +++ b/packages/trpc/src/client/react.tsx @@ -0,0 +1,69 @@ +"use client"; + +import type { QueryClient } from "@tanstack/react-query"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { + createTRPCClient, + httpBatchStreamLink, + loggerLink, +} from "@trpc/client"; +import { createTRPCContext } from "@trpc/tanstack-react-query"; +import { useState } from "react"; +import SuperJSON from "superjson"; +import type { AppRouter } from "../root"; + +import { createQueryClient } from "./query-client"; + +let clientQueryClientSingleton: QueryClient | undefined; +const getQueryClient = () => { + if (typeof window === "undefined") { + return createQueryClient(); + } + if (!clientQueryClientSingleton) { + clientQueryClientSingleton = createQueryClient(); + } + return clientQueryClientSingleton; +}; + +const context = createTRPCContext(); +export const { useTRPC, TRPCProvider } = context; +export type UseTRPC = typeof useTRPC; + +export function createTRPCReactProvider(config: { + apiUrl: string; + isDev: boolean; +}) { + return function TRPCReactProvider(props: { children: React.ReactNode }) { + const queryClient = getQueryClient(); + + const [trpcClient] = useState(() => + createTRPCClient({ + links: [ + loggerLink({ + enabled: (op) => + config.isDev || + (op.direction === "down" && op.result instanceof Error), + }), + httpBatchStreamLink({ + transformer: SuperJSON, + url: `${config.apiUrl}/api/trpc`, + headers() { + return { "x-trpc-source": "nextjs-react" }; + }, + fetch(url, options) { + return fetch(url, { ...options, credentials: "include" }); + }, + }), + ], + }), + ); + + return ( + + + {props.children} + + + ); + }; +} diff --git a/packages/trpc/src/client/server.ts b/packages/trpc/src/client/server.ts new file mode 100644 index 00000000000..8536478399f --- /dev/null +++ b/packages/trpc/src/client/server.ts @@ -0,0 +1,23 @@ +import { createTRPCClient, httpBatchLink } from "@trpc/client"; +import SuperJSON from "superjson"; +import type { AppRouter } from "../root"; + +export function createServerTRPCClient({ + apiUrl, + headers, +}: { + apiUrl: string; + headers: Headers; +}) { + return createTRPCClient({ + links: [ + httpBatchLink({ + transformer: SuperJSON, + url: `${apiUrl}/api/trpc`, + headers() { + return Object.fromEntries(headers.entries()); + }, + }), + ], + }); +} diff --git a/packages/trpc/tsconfig.json b/packages/trpc/tsconfig.json index 921e87545ce..31ddfa6ca9a 100644 --- a/packages/trpc/tsconfig.json +++ b/packages/trpc/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "@superset/typescript/base.json", "compilerOptions": { - "jsx": "react-jsx" + "jsx": "react-jsx", + "lib": ["ES2022", "DOM", "DOM.Iterable"] }, "include": ["src"], "exclude": ["node_modules"] From ae60b736d035e9d109b0ecc70866e56597059cdd Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Fri, 13 Feb 2026 13:47:11 -0800 Subject: [PATCH 02/16] refactor(db): extract findOrgMembership to deduplicate membership checks Move the org membership query into @superset/db/utils so both tRPC procedures and API route handlers share the same base query instead of duplicating the drizzle query inline across 7+ files. --- apps/api/src/app/api/github/install/route.ts | 12 ++++-------- .../api/integrations/linear/connect/route.ts | 12 ++++-------- .../api/integrations/slack/connect/route.ts | 11 ++--------- .../app/api/integrations/slack/link/route.ts | 15 +++++---------- packages/db/src/utils/index.ts | 1 + packages/db/src/utils/membership.ts | 19 +++++++++++++++++++ packages/trpc/src/router/integration/utils.ts | 11 ++--------- .../src/router/organization/organization.ts | 17 +++++++---------- 8 files changed, 44 insertions(+), 54 deletions(-) create mode 100644 packages/db/src/utils/membership.ts diff --git a/apps/api/src/app/api/github/install/route.ts b/apps/api/src/app/api/github/install/route.ts index 7dd5d45ee54..2ad4af899cb 100644 --- a/apps/api/src/app/api/github/install/route.ts +++ b/apps/api/src/app/api/github/install/route.ts @@ -1,7 +1,5 @@ import { auth } from "@superset/auth/server"; -import { db } from "@superset/db/client"; -import { members } from "@superset/db/schema"; -import { and, eq } from "drizzle-orm"; +import { findOrgMembership } from "@superset/db/utils"; import { env } from "@/env"; import { createSignedState } from "@/lib/oauth-state"; @@ -23,11 +21,9 @@ export async function GET(request: Request) { ); } - const membership = await db.query.members.findFirst({ - where: and( - eq(members.organizationId, organizationId), - eq(members.userId, session.user.id), - ), + const membership = await findOrgMembership({ + userId: session.user.id, + organizationId, }); if (!membership) { diff --git a/apps/api/src/app/api/integrations/linear/connect/route.ts b/apps/api/src/app/api/integrations/linear/connect/route.ts index a8a59830ad4..3e2e43db0e2 100644 --- a/apps/api/src/app/api/integrations/linear/connect/route.ts +++ b/apps/api/src/app/api/integrations/linear/connect/route.ts @@ -1,7 +1,5 @@ import { auth } from "@superset/auth/server"; -import { db } from "@superset/db/client"; -import { members } from "@superset/db/schema"; -import { and, eq } from "drizzle-orm"; +import { findOrgMembership } from "@superset/db/utils"; import { env } from "@/env"; import { createSignedState } from "@/lib/oauth-state"; @@ -25,11 +23,9 @@ export async function GET(request: Request) { ); } - const membership = await db.query.members.findFirst({ - where: and( - eq(members.organizationId, organizationId), - eq(members.userId, session.user.id), - ), + const membership = await findOrgMembership({ + userId: session.user.id, + organizationId, }); if (!membership) { diff --git a/apps/api/src/app/api/integrations/slack/connect/route.ts b/apps/api/src/app/api/integrations/slack/connect/route.ts index badee4495b6..c4ea30c3122 100644 --- a/apps/api/src/app/api/integrations/slack/connect/route.ts +++ b/apps/api/src/app/api/integrations/slack/connect/route.ts @@ -1,7 +1,5 @@ import { auth } from "@superset/auth/server"; -import { db } from "@superset/db/client"; -import { members } from "@superset/db/schema"; -import { and, eq } from "drizzle-orm"; +import { findOrgMembership } from "@superset/db/utils"; import { env } from "@/env"; import { createSignedState } from "@/lib/oauth-state"; @@ -42,12 +40,7 @@ export async function GET(request: Request) { const userId = session.user.id; - const membership = await db.query.members.findFirst({ - where: and( - eq(members.organizationId, organizationId), - eq(members.userId, userId), - ), - }); + const membership = await findOrgMembership({ userId, organizationId }); if (!membership) { return Response.json( diff --git a/apps/api/src/app/api/integrations/slack/link/route.ts b/apps/api/src/app/api/integrations/slack/link/route.ts index 23dc6da3073..d9daf905875 100644 --- a/apps/api/src/app/api/integrations/slack/link/route.ts +++ b/apps/api/src/app/api/integrations/slack/link/route.ts @@ -1,11 +1,8 @@ import { createHmac } from "node:crypto"; import { auth } from "@superset/auth/server"; import { db } from "@superset/db/client"; -import { - integrationConnections, - members, - usersSlackUsers, -} from "@superset/db/schema"; +import { integrationConnections, usersSlackUsers } from "@superset/db/schema"; +import { findOrgMembership } from "@superset/db/utils"; import { and, eq } from "drizzle-orm"; import { headers } from "next/headers"; import { env } from "@/env"; @@ -65,11 +62,9 @@ export async function GET(request: Request) { ); } - const membership = await db.query.members.findFirst({ - where: and( - eq(members.organizationId, connection.organizationId), - eq(members.userId, session.user.id), - ), + const membership = await findOrgMembership({ + userId: session.user.id, + organizationId: connection.organizationId, }); if (!membership) { diff --git a/packages/db/src/utils/index.ts b/packages/db/src/utils/index.ts index f1a2f5d569e..0d00e4fedd7 100644 --- a/packages/db/src/utils/index.ts +++ b/packages/db/src/utils/index.ts @@ -1 +1,2 @@ +export * from "./membership"; export * from "./sql"; diff --git a/packages/db/src/utils/membership.ts b/packages/db/src/utils/membership.ts new file mode 100644 index 00000000000..4da606613eb --- /dev/null +++ b/packages/db/src/utils/membership.ts @@ -0,0 +1,19 @@ +import { and, eq } from "drizzle-orm"; + +import { db } from "../client"; +import { members } from "../schema"; + +export async function findOrgMembership({ + userId, + organizationId, +}: { + userId: string; + organizationId: string; +}) { + return db.query.members.findFirst({ + where: and( + eq(members.organizationId, organizationId), + eq(members.userId, userId), + ), + }); +} diff --git a/packages/trpc/src/router/integration/utils.ts b/packages/trpc/src/router/integration/utils.ts index 56e36b3cbed..d4828ab574c 100644 --- a/packages/trpc/src/router/integration/utils.ts +++ b/packages/trpc/src/router/integration/utils.ts @@ -1,18 +1,11 @@ -import { db } from "@superset/db/client"; -import { members } from "@superset/db/schema"; +import { findOrgMembership } from "@superset/db/utils"; import { TRPCError } from "@trpc/server"; -import { and, eq } from "drizzle-orm"; export async function verifyOrgMembership( userId: string, organizationId: string, ) { - const membership = await db.query.members.findFirst({ - where: and( - eq(members.organizationId, organizationId), - eq(members.userId, userId), - ), - }); + const membership = await findOrgMembership({ userId, organizationId }); if (!membership) { throw new TRPCError({ diff --git a/packages/trpc/src/router/organization/organization.ts b/packages/trpc/src/router/organization/organization.ts index fd6729d2ea5..3cbff3198d8 100644 --- a/packages/trpc/src/router/organization/organization.ts +++ b/packages/trpc/src/router/organization/organization.ts @@ -5,6 +5,7 @@ import { sessions as authSessions, invitations, } from "@superset/db/schema/auth"; +import { findOrgMembership } from "@superset/db/utils"; import { canRemoveMember, type OrganizationRole } from "@superset/shared/auth"; import { TRPCError, type TRPCRouterRecord } from "@trpc/server"; import { del, put } from "@vercel/blob"; @@ -146,11 +147,9 @@ export const organizationRouter = { .mutation(async ({ ctx, input }) => { const { id, ...data } = input; - const membership = await db.query.members.findFirst({ - where: and( - eq(members.organizationId, id), - eq(members.userId, ctx.session.user.id), - ), + const membership = await findOrgMembership({ + userId: ctx.session.user.id, + organizationId: id, }); if (!membership) { @@ -215,11 +214,9 @@ export const organizationRouter = { }), ) .mutation(async ({ ctx, input }) => { - const membership = await db.query.members.findFirst({ - where: and( - eq(members.organizationId, input.organizationId), - eq(members.userId, ctx.session.user.id), - ), + const membership = await findOrgMembership({ + userId: ctx.session.user.id, + organizationId: input.organizationId, }); if (!membership) { From fff2be28a61eb6d7852a9f91d10a0570027c43d0 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Fri, 13 Feb 2026 13:48:37 -0800 Subject: [PATCH 03/16] refactor(trpc): extract shared image upload utility Deduplicate image upload logic (mime validation, base64 decoding, size check, old image deletion, blob upload) from uploadAvatar and uploadLogo into a shared uploadImage utility in packages/trpc/src/lib/upload.ts. --- packages/trpc/src/lib/upload.ts | 64 +++++++++++++++++++ .../src/router/organization/organization.ts | 55 ++++------------ packages/trpc/src/router/user/user.ts | 55 ++++------------ 3 files changed, 90 insertions(+), 84 deletions(-) create mode 100644 packages/trpc/src/lib/upload.ts diff --git a/packages/trpc/src/lib/upload.ts b/packages/trpc/src/lib/upload.ts new file mode 100644 index 00000000000..36f5e2dcdc3 --- /dev/null +++ b/packages/trpc/src/lib/upload.ts @@ -0,0 +1,64 @@ +import { TRPCError } from "@trpc/server"; +import { del, put } from "@vercel/blob"; + +const ALLOWED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/webp"]; +const MAX_SIZE_MB = 4.5; + +export async function uploadImage({ + fileData, + mimeType, + pathname, + existingUrl, +}: { + fileData: string; + mimeType: string; + pathname: string; + existingUrl: string | null; +}) { + if (!ALLOWED_IMAGE_TYPES.includes(mimeType)) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Invalid image type. Only PNG, JPEG, and WebP are allowed", + }); + } + + const base64Data = fileData.includes("base64,") + ? fileData.split("base64,")[1] || fileData + : fileData; + const buffer = Buffer.from(base64Data, "base64"); + + const sizeInMB = buffer.length / (1024 * 1024); + if (sizeInMB > MAX_SIZE_MB) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `File too large (${sizeInMB.toFixed(2)}MB). Maximum size is ${MAX_SIZE_MB}MB`, + }); + } + + if (existingUrl) { + try { + await del(existingUrl); + } catch { + // Old image doesn't exist or isn't in blob storage + } + } + + const blob = await put(pathname, buffer, { + access: "public", + contentType: mimeType, + }); + + return blob.url; +} + +export function generateImagePathname({ + prefix, + mimeType, +}: { + prefix: string; + mimeType: string; +}) { + const ext = mimeType.split("/")[1]?.replace("jpeg", "jpg") || "png"; + const randomId = Math.random().toString(36).substring(2, 15); + return `${prefix}/${randomId}.${ext}`; +} diff --git a/packages/trpc/src/router/organization/organization.ts b/packages/trpc/src/router/organization/organization.ts index 3cbff3198d8..00a7247ac4f 100644 --- a/packages/trpc/src/router/organization/organization.ts +++ b/packages/trpc/src/router/organization/organization.ts @@ -8,9 +8,9 @@ import { import { findOrgMembership } from "@superset/db/utils"; import { canRemoveMember, type OrganizationRole } from "@superset/shared/auth"; import { TRPCError, type TRPCRouterRecord } from "@trpc/server"; -import { del, put } from "@vercel/blob"; import { and, desc, eq, ne } from "drizzle-orm"; import { z } from "zod"; +import { generateImagePathname, uploadImage } from "../../lib/upload"; import { protectedProcedure, publicProcedure } from "../../trpc"; export const organizationRouter = { @@ -244,57 +244,28 @@ export const organizationRouter = { }); } - if (organization.logo) { - try { - await del(organization.logo); - } catch { - // Old logo doesn't exist or isn't in blob storage - that's fine - } - } - - const allowedMimeTypes = ["image/png", "image/jpeg", "image/webp"]; - if (!allowedMimeTypes.includes(input.mimeType)) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Invalid image type. Only PNG, JPEG, and WebP are allowed", - }); - } - - const ext = input.mimeType.split("/")[1]?.replace("jpeg", "jpg") || "png"; - const randomId = Math.random().toString(36).substring(2, 15); - const pathname = `organization/${input.organizationId}/logo/${randomId}.${ext}`; - - const base64Data = input.fileData.includes("base64,") - ? input.fileData.split("base64,")[1] || input.fileData - : input.fileData; - const buffer = Buffer.from(base64Data, "base64"); - - const sizeInMB = buffer.length / (1024 * 1024); - if (sizeInMB > 4.5) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `File too large (${sizeInMB.toFixed(2)}MB). Maximum size is 4.5MB`, - }); - } + const pathname = generateImagePathname({ + prefix: `organization/${input.organizationId}/logo`, + mimeType: input.mimeType, + }); try { - const blob = await put(pathname, buffer, { - access: "public", - contentType: input.mimeType, + const url = await uploadImage({ + fileData: input.fileData, + mimeType: input.mimeType, + pathname, + existingUrl: organization.logo, }); const [updatedOrg] = await db .update(organizations) - .set({ logo: blob.url }) + .set({ logo: url }) .where(eq(organizations.id, input.organizationId)) .returning(); - return { - success: true, - url: blob.url, - organization: updatedOrg, - }; + return { success: true, url, organization: updatedOrg }; } catch (error) { + if (error instanceof TRPCError) throw error; console.error("[organization/uploadLogo] Upload failed:", error); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", diff --git a/packages/trpc/src/router/user/user.ts b/packages/trpc/src/router/user/user.ts index 5840a707f76..233e04b15e2 100644 --- a/packages/trpc/src/router/user/user.ts +++ b/packages/trpc/src/router/user/user.ts @@ -1,10 +1,10 @@ import { db } from "@superset/db/client"; import { members, users } from "@superset/db/schema"; import { TRPCError, type TRPCRouterRecord } from "@trpc/server"; -import { del, put } from "@vercel/blob"; import { and, desc, eq } from "drizzle-orm"; import { z } from "zod"; +import { generateImagePathname, uploadImage } from "../../lib/upload"; import { protectedProcedure } from "../../trpc"; export const userRouter = { @@ -74,57 +74,28 @@ export const userRouter = { }); } - const allowedMimeTypes = ["image/png", "image/jpeg", "image/webp"]; - if (!allowedMimeTypes.includes(input.mimeType)) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Invalid image type. Only PNG, JPEG, and WebP are allowed", - }); - } - - const base64Data = input.fileData.includes("base64,") - ? input.fileData.split("base64,")[1] || input.fileData - : input.fileData; - const buffer = Buffer.from(base64Data, "base64"); - - const sizeInMB = buffer.length / (1024 * 1024); - if (sizeInMB > 4.5) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: `File too large (${sizeInMB.toFixed(2)}MB). Maximum size is 4.5MB`, - }); - } - - if (user.image) { - try { - await del(user.image); - } catch { - // Old avatar doesn't exist or isn't in blob storage - that's fine - } - } - - const ext = input.mimeType.split("/")[1]?.replace("jpeg", "jpg") || "png"; - const randomId = Math.random().toString(36).substring(2, 15); - const pathname = `user/${userId}/avatar/${randomId}.${ext}`; + const pathname = generateImagePathname({ + prefix: `user/${userId}/avatar`, + mimeType: input.mimeType, + }); try { - const blob = await put(pathname, buffer, { - access: "public", - contentType: input.mimeType, + const url = await uploadImage({ + fileData: input.fileData, + mimeType: input.mimeType, + pathname, + existingUrl: user.image, }); const [updatedUser] = await db .update(users) - .set({ image: blob.url }) + .set({ image: url }) .where(eq(users.id, userId)) .returning(); - return { - success: true, - url: blob.url, - user: updatedUser, - }; + return { success: true, url, user: updatedUser }; } catch (error) { + if (error instanceof TRPCError) throw error; console.error("[user/uploadAvatar] Upload failed:", error); throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", From dcce28f9d2501c736664aedbdbf395e43d8bae42 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Fri, 13 Feb 2026 13:51:29 -0800 Subject: [PATCH 04/16] refactor(mcp): deduplicate task tools (priority enums, UUID/slug resolution, response formatting) - Import taskPriorityValues from @superset/db/enums instead of redefining - Extract resolveTaskId and taskIdCondition for UUID/slug resolution - Extract formatMcpResponse helper for consistent response formatting - Extract isPriority type guard to shared utils --- .../tools/tasks/create-task/create-task.ts | 25 ++++------ .../tools/tasks/delete-task/delete-task.ts | 28 ++++------- .../mcp/src/tools/tasks/get-task/get-task.ts | 14 ++---- .../src/tools/tasks/list-tasks/list-tasks.ts | 28 ++--------- .../tools/tasks/update-task/update-task.ts | 29 ++++------- packages/mcp/src/tools/tasks/utils.ts | 48 +++++++++++++++++++ 6 files changed, 80 insertions(+), 92 deletions(-) create mode 100644 packages/mcp/src/tools/tasks/utils.ts diff --git a/packages/mcp/src/tools/tasks/create-task/create-task.ts b/packages/mcp/src/tools/tasks/create-task/create-task.ts index b00c4288458..fde75764e2a 100644 --- a/packages/mcp/src/tools/tasks/create-task/create-task.ts +++ b/packages/mcp/src/tools/tasks/create-task/create-task.ts @@ -4,19 +4,18 @@ import { taskStatuses, tasks } from "@superset/db/schema"; import { and, eq, ilike, or } from "drizzle-orm"; import { z } from "zod"; import { getMcpContext } from "../../utils"; - -const PRIORITIES = ["urgent", "high", "medium", "low", "none"] as const; -type TaskPriority = (typeof PRIORITIES)[number]; - -function isPriority(value: unknown): value is TaskPriority { - return PRIORITIES.includes(value as TaskPriority); -} +import { + formatMcpResponse, + isPriority, + type TaskPriority, + taskPriorityValues, +} from "../utils"; const taskInputSchema = z.object({ title: z.string().min(1).describe("Task title"), description: z.string().optional().describe("Task description (markdown)"), priority: z - .enum(["urgent", "high", "medium", "low", "none"]) + .enum(taskPriorityValues) .default("none") .describe("Task priority"), assigneeId: z.string().uuid().optional().describe("User ID to assign to"), @@ -176,15 +175,7 @@ export function register(server: McpServer) { .returning({ id: tasks.id, slug: tasks.slug, title: tasks.title }); }); - return { - structuredContent: { created: createdTasks }, - content: [ - { - type: "text", - text: JSON.stringify({ created: createdTasks }, null, 2), - }, - ], - }; + return formatMcpResponse({ created: createdTasks }); }, ); } diff --git a/packages/mcp/src/tools/tasks/delete-task/delete-task.ts b/packages/mcp/src/tools/tasks/delete-task/delete-task.ts index bde21e1da13..8bb2ff5b1c6 100644 --- a/packages/mcp/src/tools/tasks/delete-task/delete-task.ts +++ b/packages/mcp/src/tools/tasks/delete-task/delete-task.ts @@ -1,9 +1,10 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { db, dbWs } from "@superset/db/client"; +import { dbWs } from "@superset/db/client"; import { tasks } from "@superset/db/schema"; -import { and, eq, inArray, isNull } from "drizzle-orm"; +import { inArray } from "drizzle-orm"; import { z } from "zod"; import { getMcpContext } from "../../utils"; +import { formatMcpResponse, resolveTaskId } from "../utils"; export function register(server: McpServer) { server.registerTool( @@ -28,19 +29,10 @@ export function register(server: McpServer) { const resolvedTasks: { id: string; identifier: string }[] = []; for (const taskId of taskIds) { - const isUuid = z.string().uuid().safeParse(taskId).success; - - const [existingTask] = await db - .select({ id: tasks.id }) - .from(tasks) - .where( - and( - isUuid ? eq(tasks.id, taskId) : eq(tasks.slug, taskId), - eq(tasks.organizationId, ctx.organizationId), - isNull(tasks.deletedAt), - ), - ) - .limit(1); + const existingTask = await resolveTaskId({ + taskId, + organizationId: ctx.organizationId, + }); if (!existingTask) { return { @@ -62,11 +54,7 @@ export function register(server: McpServer) { .set({ deletedAt }) .where(inArray(tasks.id, taskIdsToDelete)); - const data = { deleted: taskIdsToDelete }; - return { - structuredContent: data, - content: [{ type: "text", text: JSON.stringify(data, null, 2) }], - }; + return formatMcpResponse({ deleted: taskIdsToDelete }); }, ); } diff --git a/packages/mcp/src/tools/tasks/get-task/get-task.ts b/packages/mcp/src/tools/tasks/get-task/get-task.ts index b8b0ae8e865..a17d4e6c8d1 100644 --- a/packages/mcp/src/tools/tasks/get-task/get-task.ts +++ b/packages/mcp/src/tools/tasks/get-task/get-task.ts @@ -5,6 +5,7 @@ import { and, eq, isNull } from "drizzle-orm"; import { alias } from "drizzle-orm/pg-core"; import { z } from "zod"; import { getMcpContext } from "../../utils"; +import { formatMcpResponse, taskIdCondition } from "../utils"; export function register(server: McpServer) { server.registerTool( @@ -41,7 +42,6 @@ export function register(server: McpServer) { async (args, extra) => { const ctx = getMcpContext(extra); const taskId = args.taskId as string; - const isUuid = z.string().uuid().safeParse(taskId).success; const assignee = alias(users, "assignee"); const creator = alias(users, "creator"); @@ -75,7 +75,7 @@ export function register(server: McpServer) { .leftJoin(status, eq(tasks.statusId, status.id)) .where( and( - isUuid ? eq(tasks.id, taskId) : eq(tasks.slug, taskId), + taskIdCondition(taskId), eq(tasks.organizationId, ctx.organizationId), isNull(tasks.deletedAt), ), @@ -93,15 +93,7 @@ export function register(server: McpServer) { ...task, dueDate: task.dueDate?.toISOString() ?? null, }; - return { - structuredContent: { task: serializedTask }, - content: [ - { - type: "text", - text: JSON.stringify({ task: serializedTask }, null, 2), - }, - ], - }; + return formatMcpResponse({ task: serializedTask }); }, ); } diff --git a/packages/mcp/src/tools/tasks/list-tasks/list-tasks.ts b/packages/mcp/src/tools/tasks/list-tasks/list-tasks.ts index 3effa9fc43d..f1192bdffeb 100644 --- a/packages/mcp/src/tools/tasks/list-tasks/list-tasks.ts +++ b/packages/mcp/src/tools/tasks/list-tasks/list-tasks.ts @@ -6,6 +6,7 @@ import { and, desc, eq, ilike, isNull, or, sql } from "drizzle-orm"; import { alias } from "drizzle-orm/pg-core"; import { z } from "zod"; import { getMcpContext } from "../../utils"; +import { formatMcpResponse, isPriority, taskPriorityValues } from "../utils"; type TaskStatusType = | "backlog" @@ -14,13 +15,6 @@ type TaskStatusType = | "completed" | "canceled"; -const PRIORITIES = ["urgent", "high", "medium", "low", "none"] as const; -type TaskPriority = (typeof PRIORITIES)[number]; - -function isPriority(value: unknown): value is TaskPriority { - return PRIORITIES.includes(value as TaskPriority); -} - export function register(server: McpServer) { server.registerTool( "list_tasks", @@ -42,9 +36,7 @@ export function register(server: McpServer) { .boolean() .optional() .describe("Filter to tasks created by current user"), - priority: z - .enum(["urgent", "high", "medium", "low", "none"]) - .optional(), + priority: z.enum(taskPriorityValues).optional(), labels: z .array(z.string()) .optional() @@ -164,16 +156,7 @@ export function register(server: McpServer) { conditions.push(statusCondition); } } else { - const data = { tasks: [], count: 0, hasMore: false }; - return { - structuredContent: data, - content: [ - { - type: "text", - text: JSON.stringify(data, null, 2), - }, - ], - }; + return formatMcpResponse({ tasks: [], count: 0, hasMore: false }); } } @@ -214,10 +197,7 @@ export function register(server: McpServer) { count: tasksList.length, hasMore: tasksList.length === limit, }; - return { - structuredContent: data, - content: [{ type: "text", text: JSON.stringify(data, null, 2) }], - }; + return formatMcpResponse(data); }, ); } diff --git a/packages/mcp/src/tools/tasks/update-task/update-task.ts b/packages/mcp/src/tools/tasks/update-task/update-task.ts index 8fd7df59f34..1aee4a1316a 100644 --- a/packages/mcp/src/tools/tasks/update-task/update-task.ts +++ b/packages/mcp/src/tools/tasks/update-task/update-task.ts @@ -1,15 +1,16 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { db, dbWs } from "@superset/db/client"; +import { dbWs } from "@superset/db/client"; import { tasks } from "@superset/db/schema"; -import { and, eq, isNull } from "drizzle-orm"; +import { eq } from "drizzle-orm"; import { z } from "zod"; import { getMcpContext } from "../../utils"; +import { formatMcpResponse, resolveTaskId, taskPriorityValues } from "../utils"; const updateSchema = z.object({ taskId: z.string().describe("Task ID (uuid) or slug"), title: z.string().min(1).optional().describe("New title"), description: z.string().optional().describe("New description"), - priority: z.enum(["urgent", "high", "medium", "low", "none"]).optional(), + priority: z.enum(taskPriorityValues).optional(), assigneeId: z .string() .uuid() @@ -62,19 +63,11 @@ export function register(server: McpServer) { for (const [i, update] of updates.entries()) { const taskId = update.taskId; - const isUuid = z.string().uuid().safeParse(taskId).success; - const [existingTask] = await db - .select({ id: tasks.id }) - .from(tasks) - .where( - and( - isUuid ? eq(tasks.id, taskId) : eq(tasks.slug, taskId), - eq(tasks.organizationId, ctx.organizationId), - isNull(tasks.deletedAt), - ), - ) - .limit(1); + const existingTask = await resolveTaskId({ + taskId, + organizationId: ctx.organizationId, + }); if (!existingTask) { return { @@ -137,11 +130,7 @@ export function register(server: McpServer) { } } - const data = { updated: updatedTasks }; - return { - structuredContent: data, - content: [{ type: "text", text: JSON.stringify(data, null, 2) }], - }; + return formatMcpResponse({ updated: updatedTasks }); }, ); } diff --git a/packages/mcp/src/tools/tasks/utils.ts b/packages/mcp/src/tools/tasks/utils.ts new file mode 100644 index 00000000000..ce1cab88148 --- /dev/null +++ b/packages/mcp/src/tools/tasks/utils.ts @@ -0,0 +1,48 @@ +import { db } from "@superset/db/client"; +import type { TaskPriority } from "@superset/db/enums"; +import { taskPriorityValues } from "@superset/db/enums"; +import { tasks } from "@superset/db/schema"; +import { and, eq, isNull } from "drizzle-orm"; +import { z } from "zod"; + +export { taskPriorityValues, type TaskPriority }; + +export function isPriority(value: unknown): value is TaskPriority { + return taskPriorityValues.includes(value as TaskPriority); +} + +export async function resolveTaskId({ + taskId, + organizationId, +}: { + taskId: string; + organizationId: string; +}) { + const isUuid = z.string().uuid().safeParse(taskId).success; + + const [task] = await db + .select({ id: tasks.id }) + .from(tasks) + .where( + and( + isUuid ? eq(tasks.id, taskId) : eq(tasks.slug, taskId), + eq(tasks.organizationId, organizationId), + isNull(tasks.deletedAt), + ), + ) + .limit(1); + + return task ?? null; +} + +export function taskIdCondition(taskId: string) { + const isUuid = z.string().uuid().safeParse(taskId).success; + return isUuid ? eq(tasks.id, taskId) : eq(tasks.slug, taskId); +} + +export function formatMcpResponse(data: T) { + return { + structuredContent: data, + content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }], + }; +} From 5827f120b3fc6b2209f0d8716869a83e9aa41f80 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Fri, 13 Feb 2026 13:53:30 -0800 Subject: [PATCH 05/16] refactor(trpc): extract disconnectIntegration utility for linear/slack Deduplicate the identical disconnect logic from linear and slack routers into a shared disconnectIntegration function parameterized by provider. --- .../src/router/integration/linear/linear.ts | 26 ++++++----------- .../src/router/integration/slack/slack.ts | 26 ++++++----------- packages/trpc/src/router/integration/utils.ts | 28 +++++++++++++++++++ 3 files changed, 46 insertions(+), 34 deletions(-) diff --git a/packages/trpc/src/router/integration/linear/linear.ts b/packages/trpc/src/router/integration/linear/linear.ts index f4e8320795b..50731e4484c 100644 --- a/packages/trpc/src/router/integration/linear/linear.ts +++ b/packages/trpc/src/router/integration/linear/linear.ts @@ -4,7 +4,11 @@ import type { TRPCRouterRecord } from "@trpc/server"; import { and, eq } from "drizzle-orm"; import { z } from "zod"; import { protectedProcedure } from "../../../trpc"; -import { verifyOrgAdmin, verifyOrgMembership } from "../utils"; +import { + disconnectIntegration, + verifyOrgAdmin, + verifyOrgMembership, +} from "../utils"; import { getLinearClient } from "./utils"; export const linearRouter = { @@ -27,22 +31,10 @@ export const linearRouter = { .input(z.object({ organizationId: z.uuid() })) .mutation(async ({ ctx, input }) => { await verifyOrgAdmin(ctx.session.user.id, input.organizationId); - - const result = await db - .delete(integrationConnections) - .where( - and( - eq(integrationConnections.organizationId, input.organizationId), - eq(integrationConnections.provider, "linear"), - ), - ) - .returning({ id: integrationConnections.id }); - - if (result.length === 0) { - return { success: false, error: "No connection found" }; - } - - return { success: true }; + return disconnectIntegration({ + organizationId: input.organizationId, + provider: "linear", + }); }), getTeams: protectedProcedure diff --git a/packages/trpc/src/router/integration/slack/slack.ts b/packages/trpc/src/router/integration/slack/slack.ts index 156f4ec8e30..f715d4c977c 100644 --- a/packages/trpc/src/router/integration/slack/slack.ts +++ b/packages/trpc/src/router/integration/slack/slack.ts @@ -4,7 +4,11 @@ import type { TRPCRouterRecord } from "@trpc/server"; import { and, eq } from "drizzle-orm"; import { z } from "zod"; import { protectedProcedure } from "../../../trpc"; -import { verifyOrgAdmin, verifyOrgMembership } from "../utils"; +import { + disconnectIntegration, + verifyOrgAdmin, + verifyOrgMembership, +} from "../utils"; export const slackRouter = { getConnection: protectedProcedure @@ -37,21 +41,9 @@ export const slackRouter = { .input(z.object({ organizationId: z.uuid() })) .mutation(async ({ ctx, input }) => { await verifyOrgAdmin(ctx.session.user.id, input.organizationId); - - const result = await db - .delete(integrationConnections) - .where( - and( - eq(integrationConnections.organizationId, input.organizationId), - eq(integrationConnections.provider, "slack"), - ), - ) - .returning({ id: integrationConnections.id }); - - if (result.length === 0) { - return { success: false, error: "No connection found" }; - } - - return { success: true }; + return disconnectIntegration({ + organizationId: input.organizationId, + provider: "slack", + }); }), } satisfies TRPCRouterRecord; diff --git a/packages/trpc/src/router/integration/utils.ts b/packages/trpc/src/router/integration/utils.ts index d4828ab574c..9c1205e8780 100644 --- a/packages/trpc/src/router/integration/utils.ts +++ b/packages/trpc/src/router/integration/utils.ts @@ -1,5 +1,9 @@ +import { db } from "@superset/db/client"; +import type { IntegrationProvider } from "@superset/db/enums"; +import { integrationConnections } from "@superset/db/schema"; import { findOrgMembership } from "@superset/db/utils"; import { TRPCError } from "@trpc/server"; +import { and, eq } from "drizzle-orm"; export async function verifyOrgMembership( userId: string, @@ -29,3 +33,27 @@ export async function verifyOrgAdmin(userId: string, organizationId: string) { return { membership }; } + +export async function disconnectIntegration({ + organizationId, + provider, +}: { + organizationId: string; + provider: IntegrationProvider; +}) { + const result = await db + .delete(integrationConnections) + .where( + and( + eq(integrationConnections.organizationId, organizationId), + eq(integrationConnections.provider, provider), + ), + ) + .returning({ id: integrationConnections.id }); + + if (result.length === 0) { + return { success: false as const, error: "No connection found" }; + } + + return { success: true as const }; +} From a0013e32b4402d359d25c6fa980b8ec72fad1b8f Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Fri, 13 Feb 2026 13:55:27 -0800 Subject: [PATCH 06/16] refactor(shared): consolidate DEVICE_ONLINE_THRESHOLD_MS constant Move the device online threshold constant (60s) from packages/trpc and packages/mcp into @superset/shared/constants as the single source of truth. --- bun.lock | 1 + packages/mcp/package.json | 1 + packages/mcp/src/tools/utils/utils.ts | 3 ++- packages/shared/src/constants.ts | 3 +++ packages/trpc/src/router/device/device.ts | 5 ++--- 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index 3d96dee010a..63f9aa3980a 100644 --- a/bun.lock +++ b/bun.lock @@ -666,6 +666,7 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.25.3", "@superset/db": "workspace:*", + "@superset/shared": "workspace:*", "drizzle-orm": "0.45.1", "zod": "^4.3.5", }, diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 011d340f80a..17b60235ae6 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -23,6 +23,7 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.25.3", "@superset/db": "workspace:*", + "@superset/shared": "workspace:*", "drizzle-orm": "0.45.1", "zod": "^4.3.5" }, diff --git a/packages/mcp/src/tools/utils/utils.ts b/packages/mcp/src/tools/utils/utils.ts index 8d9b7412b6b..d36e01aac2f 100644 --- a/packages/mcp/src/tools/utils/utils.ts +++ b/packages/mcp/src/tools/utils/utils.ts @@ -6,6 +6,7 @@ import type { } from "@modelcontextprotocol/sdk/types.js"; import { db } from "@superset/db/client"; import { agentCommands, devicePresence } from "@superset/db/schema"; +import { DEVICE_ONLINE_THRESHOLD_MS } from "@superset/shared/constants"; import { and, eq, gt, inArray } from "drizzle-orm"; import type { McpContext } from "../../auth"; @@ -27,7 +28,7 @@ export function getMcpContext( // --- Device execution --- -export const DEVICE_ONLINE_THRESHOLD_MS = 60_000; +export { DEVICE_ONLINE_THRESHOLD_MS }; const POLL_INTERVAL_MS = 500; const DEFAULT_TIMEOUT_MS = 30_000; diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index b1bcec3e00a..2a9935ded2b 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -45,6 +45,9 @@ export const TOKEN_CONFIG = { REFRESH_THRESHOLD: 5 * 60, } as const; +// Device presence +export const DEVICE_ONLINE_THRESHOLD_MS = 60_000; + // PostHog export const POSTHOG_COOKIE_NAME = "superset"; diff --git a/packages/trpc/src/router/device/device.ts b/packages/trpc/src/router/device/device.ts index c6417a5fdfd..6891173bca6 100644 --- a/packages/trpc/src/router/device/device.ts +++ b/packages/trpc/src/router/device/device.ts @@ -1,12 +1,11 @@ import { db } from "@superset/db/client"; import { devicePresence, deviceTypeValues, users } from "@superset/db/schema"; +import { DEVICE_ONLINE_THRESHOLD_MS } from "@superset/shared/constants"; import { TRPCError, type TRPCRouterRecord } from "@trpc/server"; import { and, eq, gt } from "drizzle-orm"; import { z } from "zod"; import { protectedProcedure } from "../../trpc"; -const OFFLINE_THRESHOLD_MS = 60_000; - export const deviceRouter = { /** * Register or update device presence (heartbeat) @@ -66,7 +65,7 @@ export const deviceRouter = { return []; } - const threshold = new Date(Date.now() - OFFLINE_THRESHOLD_MS); + const threshold = new Date(Date.now() - DEVICE_ONLINE_THRESHOLD_MS); const devices = await db .select({ From 90ac3de9067828a0cca35df1be6cc928de8454cd Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Fri, 13 Feb 2026 14:04:24 -0800 Subject: [PATCH 07/16] Marketing refactor --- apps/marketing/src/lib/blog-utils.ts | 16 ++------ apps/marketing/src/lib/blog.ts | 11 +----- apps/marketing/src/lib/changelog-utils.ts | 17 +++------ apps/marketing/src/lib/changelog.ts | 11 +----- apps/marketing/src/lib/compare-utils.ts | 8 ++-- apps/marketing/src/lib/compare.ts | 21 +++-------- apps/marketing/src/lib/content-utils.ts | 45 +++++++++++++++++++++++ 7 files changed, 66 insertions(+), 63 deletions(-) create mode 100644 apps/marketing/src/lib/content-utils.ts diff --git a/apps/marketing/src/lib/blog-utils.ts b/apps/marketing/src/lib/blog-utils.ts index cfa4f014a23..cd3e9270953 100644 --- a/apps/marketing/src/lib/blog-utils.ts +++ b/apps/marketing/src/lib/blog-utils.ts @@ -4,6 +4,7 @@ */ import type { BlogCategory } from "./blog-constants"; +import { formatContentDate } from "./content-utils"; import type { Person } from "./people"; export interface TocItem { @@ -25,17 +26,8 @@ export interface BlogPost { content: string; } -export function formatBlogDate(date: string): string { - return new Date(date).toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - }); -} +export { slugify } from "./content-utils"; -export function slugify(text: string): string { - return text - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/(^-|-$)/g, ""); +export function formatBlogDate(date: string): string { + return formatContentDate(date, "short"); } diff --git a/apps/marketing/src/lib/blog.ts b/apps/marketing/src/lib/blog.ts index 4931233613b..146a4e772c9 100644 --- a/apps/marketing/src/lib/blog.ts +++ b/apps/marketing/src/lib/blog.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import path from "node:path"; import matter from "gray-matter"; import { type BlogPost, slugify, type TocItem } from "./blog-utils"; +import { normalizeContentDate } from "./content-utils"; import { getPersonById } from "./people"; export { BLOG_CATEGORIES, type BlogCategory } from "./blog-constants"; @@ -21,15 +22,7 @@ function parseFrontmatter(filePath: string): BlogPost | null { const { data, content } = matter(fileContent); const slug = path.basename(filePath, ".mdx"); - - let dateValue: string; - if (data.date instanceof Date) { - dateValue = data.date.toISOString().split("T")[0] as string; - } else if (data.date) { - dateValue = String(data.date); - } else { - dateValue = new Date().toISOString().split("T")[0] as string; - } + const dateValue = normalizeContentDate(data.date) as string; const authorId: string = data.author ?? "unknown"; const author = getPersonById(authorId) ?? { diff --git a/apps/marketing/src/lib/changelog-utils.ts b/apps/marketing/src/lib/changelog-utils.ts index 4a33ff33517..d7337fd5fb8 100644 --- a/apps/marketing/src/lib/changelog-utils.ts +++ b/apps/marketing/src/lib/changelog-utils.ts @@ -3,6 +3,8 @@ * These can be safely imported in both server and client components. */ +import { formatContentDate } from "./content-utils"; + export interface ChangelogEntry { slug: string; url: string; @@ -13,17 +15,8 @@ export interface ChangelogEntry { content: string; } -export function formatChangelogDate(date: string): string { - return new Date(date).toLocaleDateString("en-US", { - year: "numeric", - month: "long", - day: "numeric", - }); -} +export { slugify } from "./content-utils"; -export function slugify(text: string): string { - return text - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/(^-|-$)/g, ""); +export function formatChangelogDate(date: string): string { + return formatContentDate(date, "long"); } diff --git a/apps/marketing/src/lib/changelog.ts b/apps/marketing/src/lib/changelog.ts index dc724a51a43..936c949cfd7 100644 --- a/apps/marketing/src/lib/changelog.ts +++ b/apps/marketing/src/lib/changelog.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import path from "node:path"; import matter from "gray-matter"; import { type ChangelogEntry, slugify } from "./changelog-utils"; +import { normalizeContentDate } from "./content-utils"; export { type ChangelogEntry, @@ -17,15 +18,7 @@ function parseFrontmatter(filePath: string): ChangelogEntry | null { const { data, content } = matter(fileContent); const slug = path.basename(filePath, ".mdx"); - - let dateValue: string; - if (data.date instanceof Date) { - dateValue = data.date.toISOString().split("T")[0] as string; - } else if (data.date) { - dateValue = String(data.date); - } else { - dateValue = new Date().toISOString().split("T")[0] as string; - } + const dateValue = normalizeContentDate(data.date) as string; return { slug, diff --git a/apps/marketing/src/lib/compare-utils.ts b/apps/marketing/src/lib/compare-utils.ts index 1b0361e8ab4..6e5ee5cafd8 100644 --- a/apps/marketing/src/lib/compare-utils.ts +++ b/apps/marketing/src/lib/compare-utils.ts @@ -3,6 +3,8 @@ * These can be safely imported in both server and client components. */ +import { formatContentDate } from "./content-utils"; + export interface ComparisonPage { slug: string; url: string; @@ -18,9 +20,5 @@ export interface ComparisonPage { } export function formatCompareDate(date: string): string { - return new Date(date).toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - }); + return formatContentDate(date, "short"); } diff --git a/apps/marketing/src/lib/compare.ts b/apps/marketing/src/lib/compare.ts index 2f98fea39fd..08d02217541 100644 --- a/apps/marketing/src/lib/compare.ts +++ b/apps/marketing/src/lib/compare.ts @@ -3,6 +3,7 @@ import path from "node:path"; import matter from "gray-matter"; import { slugify, type TocItem } from "./blog-utils"; import type { ComparisonPage } from "./compare-utils"; +import { normalizeContentDate } from "./content-utils"; export { type ComparisonPage, formatCompareDate } from "./compare-utils"; @@ -14,22 +15,10 @@ function parseFrontmatter(filePath: string): ComparisonPage | null { const { data, content } = matter(fileContent); const slug = path.basename(filePath, ".mdx"); - - let dateValue: string; - if (data.date instanceof Date) { - dateValue = data.date.toISOString().split("T")[0] as string; - } else if (data.date) { - dateValue = String(data.date); - } else { - dateValue = new Date().toISOString().split("T")[0] as string; - } - - let lastUpdated: string | undefined; - if (data.lastUpdated instanceof Date) { - lastUpdated = data.lastUpdated.toISOString().split("T")[0] as string; - } else if (data.lastUpdated) { - lastUpdated = String(data.lastUpdated); - } + const dateValue = normalizeContentDate(data.date) as string; + const lastUpdated = normalizeContentDate(data.lastUpdated, { + fallbackToNow: false, + }); return { slug, diff --git a/apps/marketing/src/lib/content-utils.ts b/apps/marketing/src/lib/content-utils.ts new file mode 100644 index 00000000000..86787c3e9f3 --- /dev/null +++ b/apps/marketing/src/lib/content-utils.ts @@ -0,0 +1,45 @@ +const MONTH_SHORT = "short"; +const _MONTH_LONG = "long"; + +export function formatContentDate( + date: string, + monthStyle: "short" | "long" = MONTH_SHORT, +): string { + return new Date(date).toLocaleDateString("en-US", { + year: "numeric", + month: monthStyle, + day: "numeric", + }); +} + +export function slugify(text: string): string { + return text + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/(^-|-$)/g, ""); +} + +function toDateInput(dateValue: Date | string | number): string { + return new Date(dateValue).toISOString().split("T")[0] as string; +} + +export function normalizeContentDate( + value: unknown, + options: { fallbackToNow?: boolean } = {}, +): string | undefined { + const { fallbackToNow = true } = options; + + if (value instanceof Date) { + return toDateInput(value); + } + + if (typeof value === "string" || typeof value === "number") { + return String(value); + } + + if (value) { + return String(value); + } + + return fallbackToNow ? toDateInput(Date.now()) : undefined; +} From 8ea3b3a70cba78f3b84b7e2c1b5699b851951920 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Fri, 13 Feb 2026 14:20:02 -0800 Subject: [PATCH 08/16] date fallback --- apps/marketing/src/lib/content-utils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/marketing/src/lib/content-utils.ts b/apps/marketing/src/lib/content-utils.ts index 86787c3e9f3..e8df1f7ddc7 100644 --- a/apps/marketing/src/lib/content-utils.ts +++ b/apps/marketing/src/lib/content-utils.ts @@ -28,18 +28,19 @@ export function normalizeContentDate( options: { fallbackToNow?: boolean } = {}, ): string | undefined { const { fallbackToNow = true } = options; + const fallback = fallbackToNow ? toDateInput(Date.now()) : undefined; if (value instanceof Date) { return toDateInput(value); } if (typeof value === "string" || typeof value === "number") { - return String(value); + return value ? String(value) : fallback; } if (value) { return String(value); } - return fallbackToNow ? toDateInput(Date.now()) : undefined; + return fallback; } From 515abc9237937a7a893d3f13a0acdb5511623449 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Fri, 13 Feb 2026 14:21:58 -0800 Subject: [PATCH 09/16] fix(trpc): align react and @types/react versions with monorepo --- packages/trpc/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/trpc/package.json b/packages/trpc/package.json index 98b6863afb8..fa997342cac 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -44,13 +44,13 @@ "@vercel/blob": "^2.0.0", "@vercel/kv": "^3.0.0", "drizzle-orm": "0.45.1", - "react": "^19.2.0", + "react": "19.2.0", "superjson": "^2.2.5", "zod": "^4.3.5" }, "devDependencies": { "@superset/typescript": "workspace:*", - "@types/react": "^19.1.8", + "@types/react": "~19.2.2", "typescript": "^5.9.3" } } From d0a268b34a652bc6fbabfd174fce71ca960832b0 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Fri, 13 Feb 2026 15:08:23 -0800 Subject: [PATCH 10/16] Revert "fix(trpc): align react and @types/react versions with monorepo" This reverts commit 515abc9237937a7a893d3f13a0acdb5511623449. --- packages/trpc/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/trpc/package.json b/packages/trpc/package.json index fa997342cac..98b6863afb8 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -44,13 +44,13 @@ "@vercel/blob": "^2.0.0", "@vercel/kv": "^3.0.0", "drizzle-orm": "0.45.1", - "react": "19.2.0", + "react": "^19.2.0", "superjson": "^2.2.5", "zod": "^4.3.5" }, "devDependencies": { "@superset/typescript": "workspace:*", - "@types/react": "~19.2.2", + "@types/react": "^19.1.8", "typescript": "^5.9.3" } } From bc338747c4f8155121fcf196e23f3ab430ef0e80 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Fri, 13 Feb 2026 15:08:31 -0800 Subject: [PATCH 11/16] Revert "refactor(shared): consolidate DEVICE_ONLINE_THRESHOLD_MS constant" This reverts commit a0013e32b4402d359d25c6fa980b8ec72fad1b8f. --- bun.lock | 1 - packages/mcp/package.json | 1 - packages/mcp/src/tools/utils/utils.ts | 3 +-- packages/shared/src/constants.ts | 3 --- packages/trpc/src/router/device/device.ts | 5 +++-- 5 files changed, 4 insertions(+), 9 deletions(-) diff --git a/bun.lock b/bun.lock index 63f9aa3980a..3d96dee010a 100644 --- a/bun.lock +++ b/bun.lock @@ -666,7 +666,6 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.25.3", "@superset/db": "workspace:*", - "@superset/shared": "workspace:*", "drizzle-orm": "0.45.1", "zod": "^4.3.5", }, diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 17b60235ae6..011d340f80a 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -23,7 +23,6 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.25.3", "@superset/db": "workspace:*", - "@superset/shared": "workspace:*", "drizzle-orm": "0.45.1", "zod": "^4.3.5" }, diff --git a/packages/mcp/src/tools/utils/utils.ts b/packages/mcp/src/tools/utils/utils.ts index d36e01aac2f..8d9b7412b6b 100644 --- a/packages/mcp/src/tools/utils/utils.ts +++ b/packages/mcp/src/tools/utils/utils.ts @@ -6,7 +6,6 @@ import type { } from "@modelcontextprotocol/sdk/types.js"; import { db } from "@superset/db/client"; import { agentCommands, devicePresence } from "@superset/db/schema"; -import { DEVICE_ONLINE_THRESHOLD_MS } from "@superset/shared/constants"; import { and, eq, gt, inArray } from "drizzle-orm"; import type { McpContext } from "../../auth"; @@ -28,7 +27,7 @@ export function getMcpContext( // --- Device execution --- -export { DEVICE_ONLINE_THRESHOLD_MS }; +export const DEVICE_ONLINE_THRESHOLD_MS = 60_000; const POLL_INTERVAL_MS = 500; const DEFAULT_TIMEOUT_MS = 30_000; diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index 2a9935ded2b..b1bcec3e00a 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -45,9 +45,6 @@ export const TOKEN_CONFIG = { REFRESH_THRESHOLD: 5 * 60, } as const; -// Device presence -export const DEVICE_ONLINE_THRESHOLD_MS = 60_000; - // PostHog export const POSTHOG_COOKIE_NAME = "superset"; diff --git a/packages/trpc/src/router/device/device.ts b/packages/trpc/src/router/device/device.ts index 6891173bca6..c6417a5fdfd 100644 --- a/packages/trpc/src/router/device/device.ts +++ b/packages/trpc/src/router/device/device.ts @@ -1,11 +1,12 @@ import { db } from "@superset/db/client"; import { devicePresence, deviceTypeValues, users } from "@superset/db/schema"; -import { DEVICE_ONLINE_THRESHOLD_MS } from "@superset/shared/constants"; import { TRPCError, type TRPCRouterRecord } from "@trpc/server"; import { and, eq, gt } from "drizzle-orm"; import { z } from "zod"; import { protectedProcedure } from "../../trpc"; +const OFFLINE_THRESHOLD_MS = 60_000; + export const deviceRouter = { /** * Register or update device presence (heartbeat) @@ -65,7 +66,7 @@ export const deviceRouter = { return []; } - const threshold = new Date(Date.now() - DEVICE_ONLINE_THRESHOLD_MS); + const threshold = new Date(Date.now() - OFFLINE_THRESHOLD_MS); const devices = await db .select({ From f945a5b3a19b20e8a99762eb843b86d8946f2b28 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Fri, 13 Feb 2026 15:08:37 -0800 Subject: [PATCH 12/16] Revert "refactor(trpc): extract disconnectIntegration utility for linear/slack" This reverts commit 5827f120b3fc6b2209f0d8716869a83e9aa41f80. --- .../src/router/integration/linear/linear.ts | 26 +++++++++++------ .../src/router/integration/slack/slack.ts | 26 +++++++++++------ packages/trpc/src/router/integration/utils.ts | 28 ------------------- 3 files changed, 34 insertions(+), 46 deletions(-) diff --git a/packages/trpc/src/router/integration/linear/linear.ts b/packages/trpc/src/router/integration/linear/linear.ts index 50731e4484c..f4e8320795b 100644 --- a/packages/trpc/src/router/integration/linear/linear.ts +++ b/packages/trpc/src/router/integration/linear/linear.ts @@ -4,11 +4,7 @@ import type { TRPCRouterRecord } from "@trpc/server"; import { and, eq } from "drizzle-orm"; import { z } from "zod"; import { protectedProcedure } from "../../../trpc"; -import { - disconnectIntegration, - verifyOrgAdmin, - verifyOrgMembership, -} from "../utils"; +import { verifyOrgAdmin, verifyOrgMembership } from "../utils"; import { getLinearClient } from "./utils"; export const linearRouter = { @@ -31,10 +27,22 @@ export const linearRouter = { .input(z.object({ organizationId: z.uuid() })) .mutation(async ({ ctx, input }) => { await verifyOrgAdmin(ctx.session.user.id, input.organizationId); - return disconnectIntegration({ - organizationId: input.organizationId, - provider: "linear", - }); + + const result = await db + .delete(integrationConnections) + .where( + and( + eq(integrationConnections.organizationId, input.organizationId), + eq(integrationConnections.provider, "linear"), + ), + ) + .returning({ id: integrationConnections.id }); + + if (result.length === 0) { + return { success: false, error: "No connection found" }; + } + + return { success: true }; }), getTeams: protectedProcedure diff --git a/packages/trpc/src/router/integration/slack/slack.ts b/packages/trpc/src/router/integration/slack/slack.ts index f715d4c977c..156f4ec8e30 100644 --- a/packages/trpc/src/router/integration/slack/slack.ts +++ b/packages/trpc/src/router/integration/slack/slack.ts @@ -4,11 +4,7 @@ import type { TRPCRouterRecord } from "@trpc/server"; import { and, eq } from "drizzle-orm"; import { z } from "zod"; import { protectedProcedure } from "../../../trpc"; -import { - disconnectIntegration, - verifyOrgAdmin, - verifyOrgMembership, -} from "../utils"; +import { verifyOrgAdmin, verifyOrgMembership } from "../utils"; export const slackRouter = { getConnection: protectedProcedure @@ -41,9 +37,21 @@ export const slackRouter = { .input(z.object({ organizationId: z.uuid() })) .mutation(async ({ ctx, input }) => { await verifyOrgAdmin(ctx.session.user.id, input.organizationId); - return disconnectIntegration({ - organizationId: input.organizationId, - provider: "slack", - }); + + const result = await db + .delete(integrationConnections) + .where( + and( + eq(integrationConnections.organizationId, input.organizationId), + eq(integrationConnections.provider, "slack"), + ), + ) + .returning({ id: integrationConnections.id }); + + if (result.length === 0) { + return { success: false, error: "No connection found" }; + } + + return { success: true }; }), } satisfies TRPCRouterRecord; diff --git a/packages/trpc/src/router/integration/utils.ts b/packages/trpc/src/router/integration/utils.ts index 9c1205e8780..d4828ab574c 100644 --- a/packages/trpc/src/router/integration/utils.ts +++ b/packages/trpc/src/router/integration/utils.ts @@ -1,9 +1,5 @@ -import { db } from "@superset/db/client"; -import type { IntegrationProvider } from "@superset/db/enums"; -import { integrationConnections } from "@superset/db/schema"; import { findOrgMembership } from "@superset/db/utils"; import { TRPCError } from "@trpc/server"; -import { and, eq } from "drizzle-orm"; export async function verifyOrgMembership( userId: string, @@ -33,27 +29,3 @@ export async function verifyOrgAdmin(userId: string, organizationId: string) { return { membership }; } - -export async function disconnectIntegration({ - organizationId, - provider, -}: { - organizationId: string; - provider: IntegrationProvider; -}) { - const result = await db - .delete(integrationConnections) - .where( - and( - eq(integrationConnections.organizationId, organizationId), - eq(integrationConnections.provider, provider), - ), - ) - .returning({ id: integrationConnections.id }); - - if (result.length === 0) { - return { success: false as const, error: "No connection found" }; - } - - return { success: true as const }; -} From d53c8a9cd677fface32e63524cc4a2a6f37d3dc5 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Fri, 13 Feb 2026 15:08:42 -0800 Subject: [PATCH 13/16] Revert "refactor(mcp): deduplicate task tools (priority enums, UUID/slug resolution, response formatting)" This reverts commit dcce28f9d2501c736664aedbdbf395e43d8bae42. --- .../tools/tasks/create-task/create-task.ts | 25 ++++++---- .../tools/tasks/delete-task/delete-task.ts | 28 +++++++---- .../mcp/src/tools/tasks/get-task/get-task.ts | 14 ++++-- .../src/tools/tasks/list-tasks/list-tasks.ts | 28 +++++++++-- .../tools/tasks/update-task/update-task.ts | 29 +++++++---- packages/mcp/src/tools/tasks/utils.ts | 48 ------------------- 6 files changed, 92 insertions(+), 80 deletions(-) delete mode 100644 packages/mcp/src/tools/tasks/utils.ts diff --git a/packages/mcp/src/tools/tasks/create-task/create-task.ts b/packages/mcp/src/tools/tasks/create-task/create-task.ts index fde75764e2a..b00c4288458 100644 --- a/packages/mcp/src/tools/tasks/create-task/create-task.ts +++ b/packages/mcp/src/tools/tasks/create-task/create-task.ts @@ -4,18 +4,19 @@ import { taskStatuses, tasks } from "@superset/db/schema"; import { and, eq, ilike, or } from "drizzle-orm"; import { z } from "zod"; import { getMcpContext } from "../../utils"; -import { - formatMcpResponse, - isPriority, - type TaskPriority, - taskPriorityValues, -} from "../utils"; + +const PRIORITIES = ["urgent", "high", "medium", "low", "none"] as const; +type TaskPriority = (typeof PRIORITIES)[number]; + +function isPriority(value: unknown): value is TaskPriority { + return PRIORITIES.includes(value as TaskPriority); +} const taskInputSchema = z.object({ title: z.string().min(1).describe("Task title"), description: z.string().optional().describe("Task description (markdown)"), priority: z - .enum(taskPriorityValues) + .enum(["urgent", "high", "medium", "low", "none"]) .default("none") .describe("Task priority"), assigneeId: z.string().uuid().optional().describe("User ID to assign to"), @@ -175,7 +176,15 @@ export function register(server: McpServer) { .returning({ id: tasks.id, slug: tasks.slug, title: tasks.title }); }); - return formatMcpResponse({ created: createdTasks }); + return { + structuredContent: { created: createdTasks }, + content: [ + { + type: "text", + text: JSON.stringify({ created: createdTasks }, null, 2), + }, + ], + }; }, ); } diff --git a/packages/mcp/src/tools/tasks/delete-task/delete-task.ts b/packages/mcp/src/tools/tasks/delete-task/delete-task.ts index 8bb2ff5b1c6..bde21e1da13 100644 --- a/packages/mcp/src/tools/tasks/delete-task/delete-task.ts +++ b/packages/mcp/src/tools/tasks/delete-task/delete-task.ts @@ -1,10 +1,9 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { dbWs } from "@superset/db/client"; +import { db, dbWs } from "@superset/db/client"; import { tasks } from "@superset/db/schema"; -import { inArray } from "drizzle-orm"; +import { and, eq, inArray, isNull } from "drizzle-orm"; import { z } from "zod"; import { getMcpContext } from "../../utils"; -import { formatMcpResponse, resolveTaskId } from "../utils"; export function register(server: McpServer) { server.registerTool( @@ -29,10 +28,19 @@ export function register(server: McpServer) { const resolvedTasks: { id: string; identifier: string }[] = []; for (const taskId of taskIds) { - const existingTask = await resolveTaskId({ - taskId, - organizationId: ctx.organizationId, - }); + const isUuid = z.string().uuid().safeParse(taskId).success; + + const [existingTask] = await db + .select({ id: tasks.id }) + .from(tasks) + .where( + and( + isUuid ? eq(tasks.id, taskId) : eq(tasks.slug, taskId), + eq(tasks.organizationId, ctx.organizationId), + isNull(tasks.deletedAt), + ), + ) + .limit(1); if (!existingTask) { return { @@ -54,7 +62,11 @@ export function register(server: McpServer) { .set({ deletedAt }) .where(inArray(tasks.id, taskIdsToDelete)); - return formatMcpResponse({ deleted: taskIdsToDelete }); + const data = { deleted: taskIdsToDelete }; + return { + structuredContent: data, + content: [{ type: "text", text: JSON.stringify(data, null, 2) }], + }; }, ); } diff --git a/packages/mcp/src/tools/tasks/get-task/get-task.ts b/packages/mcp/src/tools/tasks/get-task/get-task.ts index a17d4e6c8d1..b8b0ae8e865 100644 --- a/packages/mcp/src/tools/tasks/get-task/get-task.ts +++ b/packages/mcp/src/tools/tasks/get-task/get-task.ts @@ -5,7 +5,6 @@ import { and, eq, isNull } from "drizzle-orm"; import { alias } from "drizzle-orm/pg-core"; import { z } from "zod"; import { getMcpContext } from "../../utils"; -import { formatMcpResponse, taskIdCondition } from "../utils"; export function register(server: McpServer) { server.registerTool( @@ -42,6 +41,7 @@ export function register(server: McpServer) { async (args, extra) => { const ctx = getMcpContext(extra); const taskId = args.taskId as string; + const isUuid = z.string().uuid().safeParse(taskId).success; const assignee = alias(users, "assignee"); const creator = alias(users, "creator"); @@ -75,7 +75,7 @@ export function register(server: McpServer) { .leftJoin(status, eq(tasks.statusId, status.id)) .where( and( - taskIdCondition(taskId), + isUuid ? eq(tasks.id, taskId) : eq(tasks.slug, taskId), eq(tasks.organizationId, ctx.organizationId), isNull(tasks.deletedAt), ), @@ -93,7 +93,15 @@ export function register(server: McpServer) { ...task, dueDate: task.dueDate?.toISOString() ?? null, }; - return formatMcpResponse({ task: serializedTask }); + return { + structuredContent: { task: serializedTask }, + content: [ + { + type: "text", + text: JSON.stringify({ task: serializedTask }, null, 2), + }, + ], + }; }, ); } diff --git a/packages/mcp/src/tools/tasks/list-tasks/list-tasks.ts b/packages/mcp/src/tools/tasks/list-tasks/list-tasks.ts index f1192bdffeb..3effa9fc43d 100644 --- a/packages/mcp/src/tools/tasks/list-tasks/list-tasks.ts +++ b/packages/mcp/src/tools/tasks/list-tasks/list-tasks.ts @@ -6,7 +6,6 @@ import { and, desc, eq, ilike, isNull, or, sql } from "drizzle-orm"; import { alias } from "drizzle-orm/pg-core"; import { z } from "zod"; import { getMcpContext } from "../../utils"; -import { formatMcpResponse, isPriority, taskPriorityValues } from "../utils"; type TaskStatusType = | "backlog" @@ -15,6 +14,13 @@ type TaskStatusType = | "completed" | "canceled"; +const PRIORITIES = ["urgent", "high", "medium", "low", "none"] as const; +type TaskPriority = (typeof PRIORITIES)[number]; + +function isPriority(value: unknown): value is TaskPriority { + return PRIORITIES.includes(value as TaskPriority); +} + export function register(server: McpServer) { server.registerTool( "list_tasks", @@ -36,7 +42,9 @@ export function register(server: McpServer) { .boolean() .optional() .describe("Filter to tasks created by current user"), - priority: z.enum(taskPriorityValues).optional(), + priority: z + .enum(["urgent", "high", "medium", "low", "none"]) + .optional(), labels: z .array(z.string()) .optional() @@ -156,7 +164,16 @@ export function register(server: McpServer) { conditions.push(statusCondition); } } else { - return formatMcpResponse({ tasks: [], count: 0, hasMore: false }); + const data = { tasks: [], count: 0, hasMore: false }; + return { + structuredContent: data, + content: [ + { + type: "text", + text: JSON.stringify(data, null, 2), + }, + ], + }; } } @@ -197,7 +214,10 @@ export function register(server: McpServer) { count: tasksList.length, hasMore: tasksList.length === limit, }; - return formatMcpResponse(data); + return { + structuredContent: data, + content: [{ type: "text", text: JSON.stringify(data, null, 2) }], + }; }, ); } diff --git a/packages/mcp/src/tools/tasks/update-task/update-task.ts b/packages/mcp/src/tools/tasks/update-task/update-task.ts index 1aee4a1316a..8fd7df59f34 100644 --- a/packages/mcp/src/tools/tasks/update-task/update-task.ts +++ b/packages/mcp/src/tools/tasks/update-task/update-task.ts @@ -1,16 +1,15 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { dbWs } from "@superset/db/client"; +import { db, dbWs } from "@superset/db/client"; import { tasks } from "@superset/db/schema"; -import { eq } from "drizzle-orm"; +import { and, eq, isNull } from "drizzle-orm"; import { z } from "zod"; import { getMcpContext } from "../../utils"; -import { formatMcpResponse, resolveTaskId, taskPriorityValues } from "../utils"; const updateSchema = z.object({ taskId: z.string().describe("Task ID (uuid) or slug"), title: z.string().min(1).optional().describe("New title"), description: z.string().optional().describe("New description"), - priority: z.enum(taskPriorityValues).optional(), + priority: z.enum(["urgent", "high", "medium", "low", "none"]).optional(), assigneeId: z .string() .uuid() @@ -63,11 +62,19 @@ export function register(server: McpServer) { for (const [i, update] of updates.entries()) { const taskId = update.taskId; + const isUuid = z.string().uuid().safeParse(taskId).success; - const existingTask = await resolveTaskId({ - taskId, - organizationId: ctx.organizationId, - }); + const [existingTask] = await db + .select({ id: tasks.id }) + .from(tasks) + .where( + and( + isUuid ? eq(tasks.id, taskId) : eq(tasks.slug, taskId), + eq(tasks.organizationId, ctx.organizationId), + isNull(tasks.deletedAt), + ), + ) + .limit(1); if (!existingTask) { return { @@ -130,7 +137,11 @@ export function register(server: McpServer) { } } - return formatMcpResponse({ updated: updatedTasks }); + const data = { updated: updatedTasks }; + return { + structuredContent: data, + content: [{ type: "text", text: JSON.stringify(data, null, 2) }], + }; }, ); } diff --git a/packages/mcp/src/tools/tasks/utils.ts b/packages/mcp/src/tools/tasks/utils.ts deleted file mode 100644 index ce1cab88148..00000000000 --- a/packages/mcp/src/tools/tasks/utils.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { db } from "@superset/db/client"; -import type { TaskPriority } from "@superset/db/enums"; -import { taskPriorityValues } from "@superset/db/enums"; -import { tasks } from "@superset/db/schema"; -import { and, eq, isNull } from "drizzle-orm"; -import { z } from "zod"; - -export { taskPriorityValues, type TaskPriority }; - -export function isPriority(value: unknown): value is TaskPriority { - return taskPriorityValues.includes(value as TaskPriority); -} - -export async function resolveTaskId({ - taskId, - organizationId, -}: { - taskId: string; - organizationId: string; -}) { - const isUuid = z.string().uuid().safeParse(taskId).success; - - const [task] = await db - .select({ id: tasks.id }) - .from(tasks) - .where( - and( - isUuid ? eq(tasks.id, taskId) : eq(tasks.slug, taskId), - eq(tasks.organizationId, organizationId), - isNull(tasks.deletedAt), - ), - ) - .limit(1); - - return task ?? null; -} - -export function taskIdCondition(taskId: string) { - const isUuid = z.string().uuid().safeParse(taskId).success; - return isUuid ? eq(tasks.id, taskId) : eq(tasks.slug, taskId); -} - -export function formatMcpResponse(data: T) { - return { - structuredContent: data, - content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }], - }; -} From 80879648a8544a9d73b80439172664b5e7bda7ab Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Fri, 13 Feb 2026 15:08:49 -0800 Subject: [PATCH 14/16] Revert "refactor(trpc): extract shared tRPC client setup to @superset/trpc/client" This reverts commit ae082074d522f49efe6b8969fcaca92b6f623e16. --- .../admin/src/trpc}/query-client.ts | 0 apps/admin/src/trpc/react.tsx | 71 +++++++++++++++++-- apps/admin/src/trpc/server.tsx | 17 +++-- apps/web/src/trpc/query-client.ts | 33 +++++++++ apps/web/src/trpc/react.tsx | 66 +++++++++++++++-- apps/web/src/trpc/server.tsx | 17 +++-- bun.lock | 5 -- packages/trpc/package.json | 17 ----- packages/trpc/src/client/react.tsx | 69 ------------------ packages/trpc/src/client/server.ts | 23 ------ packages/trpc/tsconfig.json | 3 +- 11 files changed, 185 insertions(+), 136 deletions(-) rename {packages/trpc/src/client => apps/admin/src/trpc}/query-client.ts (100%) create mode 100644 apps/web/src/trpc/query-client.ts delete mode 100644 packages/trpc/src/client/react.tsx delete mode 100644 packages/trpc/src/client/server.ts diff --git a/packages/trpc/src/client/query-client.ts b/apps/admin/src/trpc/query-client.ts similarity index 100% rename from packages/trpc/src/client/query-client.ts rename to apps/admin/src/trpc/query-client.ts diff --git a/apps/admin/src/trpc/react.tsx b/apps/admin/src/trpc/react.tsx index 417b58919ab..b654b409fe1 100644 --- a/apps/admin/src/trpc/react.tsx +++ b/apps/admin/src/trpc/react.tsx @@ -1,11 +1,70 @@ "use client"; -export { type UseTRPC, useTRPC } from "@superset/trpc/client/react"; +import type { AppRouter } from "@superset/trpc"; +import type { QueryClient } from "@tanstack/react-query"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { + createTRPCClient, + httpBatchStreamLink, + loggerLink, +} from "@trpc/client"; +import { createTRPCContext } from "@trpc/tanstack-react-query"; +import { useState } from "react"; +import SuperJSON from "superjson"; -import { createTRPCReactProvider } from "@superset/trpc/client/react"; import { env } from "../env"; +import { createQueryClient } from "./query-client"; -export const TRPCReactProvider = createTRPCReactProvider({ - apiUrl: env.NEXT_PUBLIC_API_URL, - isDev: env.NODE_ENV === "development", -}); +let clientQueryClientSingleton: QueryClient | undefined; +const getQueryClient = () => { + if (typeof window === "undefined") { + return createQueryClient(); + } + if (!clientQueryClientSingleton) { + clientQueryClientSingleton = createQueryClient(); + } + return clientQueryClientSingleton; +}; + +const context = createTRPCContext(); +export const { useTRPC, TRPCProvider } = context; +export type UseTRPC = typeof useTRPC; + +export function TRPCReactProvider(props: { children: React.ReactNode }) { + const queryClient = getQueryClient(); + + const [trpcClient] = useState(() => + createTRPCClient({ + links: [ + loggerLink({ + enabled: (op) => + env.NODE_ENV === "development" || + (op.direction === "down" && op.result instanceof Error), + }), + httpBatchStreamLink({ + transformer: SuperJSON, + url: `${env.NEXT_PUBLIC_API_URL}/api/trpc`, + headers() { + return { + "x-trpc-source": "nextjs-react", + }; + }, + fetch(url, options) { + return fetch(url, { + ...options, + credentials: "include", + }); + }, + }), + ], + }), + ); + + return ( + + + {props.children} + + + ); +} diff --git a/apps/admin/src/trpc/server.tsx b/apps/admin/src/trpc/server.tsx index 75258674a29..3042598abba 100644 --- a/apps/admin/src/trpc/server.tsx +++ b/apps/admin/src/trpc/server.tsx @@ -1,8 +1,10 @@ import "server-only"; -import { createServerTRPCClient } from "@superset/trpc/client/server"; +import type { AppRouter } from "@superset/trpc"; +import { createTRPCClient, httpBatchLink } from "@trpc/client"; import { headers } from "next/headers"; import { cache } from "react"; +import SuperJSON from "superjson"; import { env } from "../env"; @@ -10,8 +12,15 @@ export const api = cache(async () => { const heads = new Headers(await headers()); heads.set("x-trpc-source", "rsc"); - return createServerTRPCClient({ - apiUrl: env.NEXT_PUBLIC_API_URL, - headers: heads, + return createTRPCClient({ + links: [ + httpBatchLink({ + transformer: SuperJSON, + url: `${env.NEXT_PUBLIC_API_URL}/api/trpc`, + headers() { + return Object.fromEntries(heads.entries()); + }, + }), + ], }); }); diff --git a/apps/web/src/trpc/query-client.ts b/apps/web/src/trpc/query-client.ts new file mode 100644 index 00000000000..5780964e5e4 --- /dev/null +++ b/apps/web/src/trpc/query-client.ts @@ -0,0 +1,33 @@ +import { + defaultShouldDehydrateQuery, + QueryClient, +} from "@tanstack/react-query"; +import SuperJSON from "superjson"; + +export const createQueryClient = () => + new QueryClient({ + defaultOptions: { + queries: { + // With SSR, we usually want to set some default staleTime + // above 0 to avoid refetching immediately on the client + staleTime: 30 * 1000, + }, + dehydrate: { + serializeData: SuperJSON.serialize, + shouldDehydrateQuery: (query) => + defaultShouldDehydrateQuery(query) || + query.state.status === "pending", + shouldRedactErrors: () => { + // We should not catch Next.js server errors + // as that's how Next.js detects dynamic pages + // so we cannot redact them. + // Next.js also automatically redacts errors for us + // with better digests. + return false; + }, + }, + hydrate: { + deserializeData: SuperJSON.deserialize, + }, + }, + }); diff --git a/apps/web/src/trpc/react.tsx b/apps/web/src/trpc/react.tsx index 417b58919ab..78f7c07f9f0 100644 --- a/apps/web/src/trpc/react.tsx +++ b/apps/web/src/trpc/react.tsx @@ -1,11 +1,65 @@ "use client"; -export { type UseTRPC, useTRPC } from "@superset/trpc/client/react"; +import type { AppRouter } from "@superset/trpc"; +import type { QueryClient } from "@tanstack/react-query"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { + createTRPCClient, + httpBatchStreamLink, + loggerLink, +} from "@trpc/client"; +import { createTRPCContext } from "@trpc/tanstack-react-query"; +import { useState } from "react"; +import SuperJSON from "superjson"; -import { createTRPCReactProvider } from "@superset/trpc/client/react"; import { env } from "../env"; +import { createQueryClient } from "./query-client"; -export const TRPCReactProvider = createTRPCReactProvider({ - apiUrl: env.NEXT_PUBLIC_API_URL, - isDev: env.NODE_ENV === "development", -}); +let clientQueryClientSingleton: QueryClient | undefined; +const getQueryClient = () => { + if (typeof window === "undefined") { + return createQueryClient(); + } + if (!clientQueryClientSingleton) { + clientQueryClientSingleton = createQueryClient(); + } + return clientQueryClientSingleton; +}; + +const context = createTRPCContext(); +export const { useTRPC, TRPCProvider } = context; +export type UseTRPC = typeof useTRPC; + +export function TRPCReactProvider(props: { children: React.ReactNode }) { + const queryClient = getQueryClient(); + + const [trpcClient] = useState(() => + createTRPCClient({ + links: [ + loggerLink({ + enabled: (op) => + env.NODE_ENV === "development" || + (op.direction === "down" && op.result instanceof Error), + }), + httpBatchStreamLink({ + transformer: SuperJSON, + url: `${env.NEXT_PUBLIC_API_URL}/api/trpc`, + headers() { + return { "x-trpc-source": "nextjs-react" }; + }, + fetch(url, options) { + return fetch(url, { ...options, credentials: "include" }); + }, + }), + ], + }), + ); + + return ( + + + {props.children} + + + ); +} diff --git a/apps/web/src/trpc/server.tsx b/apps/web/src/trpc/server.tsx index 75258674a29..3042598abba 100644 --- a/apps/web/src/trpc/server.tsx +++ b/apps/web/src/trpc/server.tsx @@ -1,8 +1,10 @@ import "server-only"; -import { createServerTRPCClient } from "@superset/trpc/client/server"; +import type { AppRouter } from "@superset/trpc"; +import { createTRPCClient, httpBatchLink } from "@trpc/client"; import { headers } from "next/headers"; import { cache } from "react"; +import SuperJSON from "superjson"; import { env } from "../env"; @@ -10,8 +12,15 @@ export const api = cache(async () => { const heads = new Headers(await headers()); heads.set("x-trpc-source", "rsc"); - return createServerTRPCClient({ - apiUrl: env.NEXT_PUBLIC_API_URL, - headers: heads, + return createTRPCClient({ + links: [ + httpBatchLink({ + transformer: SuperJSON, + url: `${env.NEXT_PUBLIC_API_URL}/api/trpc`, + headers() { + return Object.fromEntries(heads.entries()); + }, + }), + ], }); }); diff --git a/bun.lock b/bun.lock index 3d96dee010a..caf60e653df 100644 --- a/bun.lock +++ b/bun.lock @@ -701,21 +701,16 @@ "@superset/db": "workspace:*", "@superset/shared": "workspace:*", "@t3-oss/env-core": "^0.13.8", - "@tanstack/react-query": "^5.90.19", - "@trpc/client": "^11.7.1", "@trpc/server": "^11.7.1", - "@trpc/tanstack-react-query": "^11.7.1", "@upstash/qstash": "^2.8.4", "@vercel/blob": "^2.0.0", "@vercel/kv": "^3.0.0", "drizzle-orm": "0.45.1", - "react": "^19.2.0", "superjson": "^2.2.5", "zod": "^4.3.5", }, "devDependencies": { "@superset/typescript": "workspace:*", - "@types/react": "^19.1.8", "typescript": "^5.9.3", }, }, diff --git a/packages/trpc/package.json b/packages/trpc/package.json index 98b6863afb8..1939359989b 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -11,18 +11,6 @@ "./integrations/*": { "types": "./src/lib/integrations/*/index.ts", "default": "./src/lib/integrations/*/index.ts" - }, - "./client/query-client": { - "types": "./src/client/query-client.ts", - "default": "./src/client/query-client.ts" - }, - "./client/react": { - "types": "./src/client/react.tsx", - "default": "./src/client/react.tsx" - }, - "./client/server": { - "types": "./src/client/server.ts", - "default": "./src/client/server.ts" } }, "scripts": { @@ -36,21 +24,16 @@ "@superset/db": "workspace:*", "@superset/shared": "workspace:*", "@t3-oss/env-core": "^0.13.8", - "@tanstack/react-query": "^5.90.19", - "@trpc/client": "^11.7.1", "@trpc/server": "^11.7.1", - "@trpc/tanstack-react-query": "^11.7.1", "@upstash/qstash": "^2.8.4", "@vercel/blob": "^2.0.0", "@vercel/kv": "^3.0.0", "drizzle-orm": "0.45.1", - "react": "^19.2.0", "superjson": "^2.2.5", "zod": "^4.3.5" }, "devDependencies": { "@superset/typescript": "workspace:*", - "@types/react": "^19.1.8", "typescript": "^5.9.3" } } diff --git a/packages/trpc/src/client/react.tsx b/packages/trpc/src/client/react.tsx deleted file mode 100644 index c460557d01a..00000000000 --- a/packages/trpc/src/client/react.tsx +++ /dev/null @@ -1,69 +0,0 @@ -"use client"; - -import type { QueryClient } from "@tanstack/react-query"; -import { QueryClientProvider } from "@tanstack/react-query"; -import { - createTRPCClient, - httpBatchStreamLink, - loggerLink, -} from "@trpc/client"; -import { createTRPCContext } from "@trpc/tanstack-react-query"; -import { useState } from "react"; -import SuperJSON from "superjson"; -import type { AppRouter } from "../root"; - -import { createQueryClient } from "./query-client"; - -let clientQueryClientSingleton: QueryClient | undefined; -const getQueryClient = () => { - if (typeof window === "undefined") { - return createQueryClient(); - } - if (!clientQueryClientSingleton) { - clientQueryClientSingleton = createQueryClient(); - } - return clientQueryClientSingleton; -}; - -const context = createTRPCContext(); -export const { useTRPC, TRPCProvider } = context; -export type UseTRPC = typeof useTRPC; - -export function createTRPCReactProvider(config: { - apiUrl: string; - isDev: boolean; -}) { - return function TRPCReactProvider(props: { children: React.ReactNode }) { - const queryClient = getQueryClient(); - - const [trpcClient] = useState(() => - createTRPCClient({ - links: [ - loggerLink({ - enabled: (op) => - config.isDev || - (op.direction === "down" && op.result instanceof Error), - }), - httpBatchStreamLink({ - transformer: SuperJSON, - url: `${config.apiUrl}/api/trpc`, - headers() { - return { "x-trpc-source": "nextjs-react" }; - }, - fetch(url, options) { - return fetch(url, { ...options, credentials: "include" }); - }, - }), - ], - }), - ); - - return ( - - - {props.children} - - - ); - }; -} diff --git a/packages/trpc/src/client/server.ts b/packages/trpc/src/client/server.ts deleted file mode 100644 index 8536478399f..00000000000 --- a/packages/trpc/src/client/server.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createTRPCClient, httpBatchLink } from "@trpc/client"; -import SuperJSON from "superjson"; -import type { AppRouter } from "../root"; - -export function createServerTRPCClient({ - apiUrl, - headers, -}: { - apiUrl: string; - headers: Headers; -}) { - return createTRPCClient({ - links: [ - httpBatchLink({ - transformer: SuperJSON, - url: `${apiUrl}/api/trpc`, - headers() { - return Object.fromEntries(headers.entries()); - }, - }), - ], - }); -} diff --git a/packages/trpc/tsconfig.json b/packages/trpc/tsconfig.json index 31ddfa6ca9a..921e87545ce 100644 --- a/packages/trpc/tsconfig.json +++ b/packages/trpc/tsconfig.json @@ -1,8 +1,7 @@ { "extends": "@superset/typescript/base.json", "compilerOptions": { - "jsx": "react-jsx", - "lib": ["ES2022", "DOM", "DOM.Iterable"] + "jsx": "react-jsx" }, "include": ["src"], "exclude": ["node_modules"] From 73a2dc7f2cb2bfa883e4fedc4aaf2c8e6306130a Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Fri, 13 Feb 2026 15:34:59 -0800 Subject: [PATCH 15/16] Cleanup --- packages/trpc/src/lib/upload.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/trpc/src/lib/upload.ts b/packages/trpc/src/lib/upload.ts index 36f5e2dcdc3..cea7a9b9ecd 100644 --- a/packages/trpc/src/lib/upload.ts +++ b/packages/trpc/src/lib/upload.ts @@ -38,9 +38,7 @@ export async function uploadImage({ if (existingUrl) { try { await del(existingUrl); - } catch { - // Old image doesn't exist or isn't in blob storage - } + } catch {} } const blob = await put(pathname, buffer, { From e7f4f4da4d02588c86743b17108dfdcf30cd3b4a Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Fri, 13 Feb 2026 15:36:58 -0800 Subject: [PATCH 16/16] fix(mcp): add missing @superset/shared dependency for claude-command import --- packages/mcp/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 011d340f80a..17b60235ae6 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -23,6 +23,7 @@ "dependencies": { "@modelcontextprotocol/sdk": "^1.25.3", "@superset/db": "workspace:*", + "@superset/shared": "workspace:*", "drizzle-orm": "0.45.1", "zod": "^4.3.5" },