From 6d18a031c5cd28e9bbd97a1cfaf82c1a39144111 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Wed, 8 Oct 2025 12:57:12 -0600 Subject: [PATCH] get screenshot from a get request --- apps/server/package.json | 1 + .../src/api/v2/chats/[id]/screenshot/GET.ts | 15 +- .../screenshot/getChatScreenshotHandler.ts | 37 ---- .../api/v2/dashboards/[id]/screenshot/GET.ts | 15 +- .../getDashboardScreenshotHandler.ts | 37 ---- .../src/api/v2/metric_files/[id]/data/GET.ts | 27 ++- .../v2/metric_files/[id]/screenshot/GET.ts | 12 +- .../screenshot/getMetricScreenshotHandler.ts | 34 ---- .../screenshot/saveMetricScreenshotHandler.ts | 91 --------- .../shared-helpers/create-image-response.ts | 4 +- .../src/shared-helpers/metric-helpers.test.ts | 1 + apps/trigger/package.json | 15 +- apps/trigger/src/task-keys.ts | 1 + apps/trigger/src/task-schemas.ts | 5 + apps/trigger/src/tasks/screenshots/index.ts | 1 + apps/trigger/src/tasks/screenshots/schemas.ts | 9 + .../take-metric-screenshot-handler.ts | 70 +++++++ .../src/tasks/screenshots/task-keys.ts | 3 + .../screenshots/upload-screenshot-handler.ts | 177 ++++++++++++++++++ packages/server-shared/package.json | 2 + packages/server-shared/src/index.ts | 1 - .../server-shared/src/screenshots/index.ts | 1 + .../src/screenshots/methods}/browser-login.ts | 37 ++-- .../methods}/create-href-from-link.test.ts | 0 .../methods}/create-href-from-link.ts | 0 .../methods/get-chat-screenshot.ts | 33 ++++ .../methods/get-dashboard-screenshot.ts | 39 ++++ .../methods/get-metric-screenshot.ts | 39 ++++ .../methods/get-report-screenshot.ts | 37 ++++ .../src/screenshots/methods/index.ts | 7 + .../screenshots/methods}/screenshot-config.ts | 0 pnpm-lock.yaml | 89 +++++---- pnpm-workspace.yaml | 2 +- 33 files changed, 555 insertions(+), 287 deletions(-) delete mode 100644 apps/server/src/api/v2/chats/[id]/screenshot/getChatScreenshotHandler.ts delete mode 100644 apps/server/src/api/v2/dashboards/[id]/screenshot/getDashboardScreenshotHandler.ts delete mode 100644 apps/server/src/api/v2/metric_files/[id]/screenshot/getMetricScreenshotHandler.ts delete mode 100644 apps/server/src/api/v2/metric_files/[id]/screenshot/saveMetricScreenshotHandler.ts create mode 100644 apps/trigger/src/task-keys.ts create mode 100644 apps/trigger/src/task-schemas.ts create mode 100644 apps/trigger/src/tasks/screenshots/index.ts create mode 100644 apps/trigger/src/tasks/screenshots/schemas.ts create mode 100644 apps/trigger/src/tasks/screenshots/take-metric-screenshot-handler.ts create mode 100644 apps/trigger/src/tasks/screenshots/task-keys.ts create mode 100644 apps/trigger/src/tasks/screenshots/upload-screenshot-handler.ts rename {apps/server/src/shared-helpers => packages/server-shared/src/screenshots/methods}/browser-login.ts (76%) rename {apps/server/src/shared-helpers => packages/server-shared/src/screenshots/methods}/create-href-from-link.test.ts (100%) rename {apps/server/src/shared-helpers => packages/server-shared/src/screenshots/methods}/create-href-from-link.ts (100%) create mode 100644 packages/server-shared/src/screenshots/methods/get-chat-screenshot.ts create mode 100644 packages/server-shared/src/screenshots/methods/get-dashboard-screenshot.ts create mode 100644 packages/server-shared/src/screenshots/methods/get-metric-screenshot.ts create mode 100644 packages/server-shared/src/screenshots/methods/get-report-screenshot.ts create mode 100644 packages/server-shared/src/screenshots/methods/index.ts rename {apps/server/src/shared-helpers => packages/server-shared/src/screenshots/methods}/screenshot-config.ts (100%) diff --git a/apps/server/package.json b/apps/server/package.json index 9dad67780..4f4618c1d 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -35,6 +35,7 @@ "@buster/test-utils": "workspace:*", "@buster/typescript-config": "workspace:*", "@buster/vitest-config": "workspace:*", + "@buster-app/trigger": "workspace:*", "@electric-sql/client": "catalog:", "@hono/zod-validator": "^0.7.3", "@octokit/webhooks": "^14.1.3", diff --git a/apps/server/src/api/v2/chats/[id]/screenshot/GET.ts b/apps/server/src/api/v2/chats/[id]/screenshot/GET.ts index 373d63c04..03da5516e 100644 --- a/apps/server/src/api/v2/chats/[id]/screenshot/GET.ts +++ b/apps/server/src/api/v2/chats/[id]/screenshot/GET.ts @@ -4,11 +4,11 @@ import { GetChatScreenshotParamsSchema, GetChatScreenshotQuerySchema, } from '@buster/server-shared/screenshots'; +import { getChatScreenshot } from '@buster/server-shared/screenshots'; import { zValidator } from '@hono/zod-validator'; import { Hono } from 'hono'; import { HTTPException } from 'hono/http-exception'; import { createImageResponse } from '../../../../../shared-helpers/create-image-response'; -import { getChatScreenshotHandler } from './getChatScreenshotHandler'; const app = new Hono().get( '/', @@ -40,10 +40,15 @@ const app = new Hono().get( } try { - const screenshotBuffer = await getChatScreenshotHandler({ - params: { id: chatId }, - search, - context: c, + const screenshotBuffer = await getChatScreenshot({ + chatId, + width: search.width, + height: search.height, + type: search.type, + supabaseCookieKey: c.get('supabaseCookieKey'), + supabaseUser: c.get('supabaseUser'), + accessToken: c.get('accessToken') || '', + organizationId: chat.organizationId, }); return createImageResponse(screenshotBuffer, search.type); diff --git a/apps/server/src/api/v2/chats/[id]/screenshot/getChatScreenshotHandler.ts b/apps/server/src/api/v2/chats/[id]/screenshot/getChatScreenshotHandler.ts deleted file mode 100644 index 7321551af..000000000 --- a/apps/server/src/api/v2/chats/[id]/screenshot/getChatScreenshotHandler.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { - GetChatScreenshotParams, - GetChatScreenshotQuery, -} from '@buster/server-shared/screenshots'; -import type { Context } from 'hono'; -import { createHrefFromLink } from '../../../../../shared-helpers/create-href-from-link'; - -export const getChatScreenshotHandler = async ({ - params, - search, - context, -}: { - params: GetChatScreenshotParams; - search: GetChatScreenshotQuery; - context: Context; -}) => { - const { width, height, type } = search; - const { id: chatId } = params; - - const { browserLogin } = await import('../../../../../shared-helpers/browser-login'); - - const { result: screenshotBuffer } = await browserLogin({ - width, - height, - fullPath: createHrefFromLink({ - to: '/screenshots/chats/$chatId/content' as const, - params: { chatId }, - search: { type, width, height }, - }), - context, - callback: async ({ page }) => { - return await page.screenshot({ type }); - }, - }); - - return screenshotBuffer; -}; diff --git a/apps/server/src/api/v2/dashboards/[id]/screenshot/GET.ts b/apps/server/src/api/v2/dashboards/[id]/screenshot/GET.ts index f3771fead..171d05a87 100644 --- a/apps/server/src/api/v2/dashboards/[id]/screenshot/GET.ts +++ b/apps/server/src/api/v2/dashboards/[id]/screenshot/GET.ts @@ -1,15 +1,15 @@ import { checkPermission } from '@buster/access-controls'; -import { getDashboardById } from '@buster/database/queries'; +import { getDashboardById, getUserOrganizationId } from '@buster/database/queries'; import { GetDashboardScreenshotParamsSchema, GetDashboardScreenshotQuerySchema, } from '@buster/server-shared/screenshots'; +import { getDashboardScreenshot } from '@buster/server-shared/screenshots'; import { zValidator } from '@hono/zod-validator'; import { Hono } from 'hono'; import { HTTPException } from 'hono/http-exception'; import { createImageResponse } from '../../../../../shared-helpers/create-image-response'; import { standardErrorHandler } from '../../../../../utils/response'; -import { getDashboardScreenshotHandler } from './getDashboardScreenshotHandler'; const app = new Hono() .get( @@ -43,10 +43,13 @@ const app = new Hono() } try { - const screenshotBuffer = await getDashboardScreenshotHandler({ - params: { id: dashboardId }, - search, - context: c, + const screenshotBuffer = await getDashboardScreenshot({ + ...search, + dashboardId, + supabaseCookieKey: c.get('supabaseCookieKey'), + supabaseUser: c.get('supabaseUser'), + accessToken: c.get('accessToken') || '', + organizationId: dashboard.organizationId, }); return createImageResponse(screenshotBuffer, search.type); diff --git a/apps/server/src/api/v2/dashboards/[id]/screenshot/getDashboardScreenshotHandler.ts b/apps/server/src/api/v2/dashboards/[id]/screenshot/getDashboardScreenshotHandler.ts deleted file mode 100644 index d0a47d131..000000000 --- a/apps/server/src/api/v2/dashboards/[id]/screenshot/getDashboardScreenshotHandler.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { - GetDashboardScreenshotParams, - GetDashboardScreenshotQuery, -} from '@buster/server-shared/screenshots'; -import type { Context } from 'hono'; -import { createHrefFromLink } from '../../../../../shared-helpers/create-href-from-link'; - -export const getDashboardScreenshotHandler = async ({ - params, - search, - context, -}: { - params: GetDashboardScreenshotParams; - search: GetDashboardScreenshotQuery; - context: Context; -}) => { - const { width, height, type } = search; - const { id: dashboardId } = params; - - const { browserLogin } = await import('../../../../../shared-helpers/browser-login'); - - const { result: screenshotBuffer } = await browserLogin({ - width, - height, - fullPath: createHrefFromLink({ - to: '/screenshots/dashboards/$dashboardId/content' as const, - params: { dashboardId }, - search: { type, width, height }, - }), - context, - callback: async ({ page }) => { - return await page.screenshot({ type }); - }, - }); - - return screenshotBuffer; -}; diff --git a/apps/server/src/api/v2/metric_files/[id]/data/GET.ts b/apps/server/src/api/v2/metric_files/[id]/data/GET.ts index 09ef17946..949823add 100644 --- a/apps/server/src/api/v2/metric_files/[id]/data/GET.ts +++ b/apps/server/src/api/v2/metric_files/[id]/data/GET.ts @@ -1,12 +1,14 @@ +import { screenshots_task_keys } from '@buster-app/trigger/task-keys'; +import type { TakeMetricScreenshotTrigger } from '@buster-app/trigger/task-schemas'; +import { getUserOrganizationId } from '@buster/database/queries'; import { MetricDataParamsSchema, MetricDataQuerySchema } from '@buster/server-shared'; import { zValidator } from '@hono/zod-validator'; +import { tasks } from '@trigger.dev/sdk'; import { Hono } from 'hono'; import { standardErrorHandler } from '../../../../../utils/response'; -import { saveMetricScreenshotHandler } from '../screenshot/saveMetricScreenshotHandler'; import { getMetricDataHandler } from './get-metric-data'; const app = new Hono() - // GET /metric_files/:id/data - Get metric data with pagination .get( '/', zValidator('param', MetricDataParamsSchema), @@ -25,12 +27,21 @@ const app = new Hono() password ); - saveMetricScreenshotHandler({ - metricId: id, - version_number, - isOnSaveEvent: false, - context: c, - }); + await tasks.trigger( + screenshots_task_keys.take_metric_screenshot, + { + metricId: id, + isOnSaveEvent: true, + supabaseCookieKey: c.get('supabaseCookieKey'), + supabaseUser: c.get('supabaseUser'), + accessToken: c.get('accessToken') || '', + organizationId: + (await getUserOrganizationId(user.id).then((res) => res?.organizationId)) || '', + } satisfies TakeMetricScreenshotTrigger, + { + idempotencyKey: `take-metric-screenshot-${id}`, + } + ); return c.json(response); } diff --git a/apps/server/src/api/v2/metric_files/[id]/screenshot/GET.ts b/apps/server/src/api/v2/metric_files/[id]/screenshot/GET.ts index bfa3c79e6..c7f8aaac1 100644 --- a/apps/server/src/api/v2/metric_files/[id]/screenshot/GET.ts +++ b/apps/server/src/api/v2/metric_files/[id]/screenshot/GET.ts @@ -4,12 +4,12 @@ import { GetMetricScreenshotParamsSchema, GetMetricScreenshotQuerySchema, } from '@buster/server-shared/screenshots'; +import { getMetricScreenshot } from '@buster/server-shared/screenshots'; import { zValidator } from '@hono/zod-validator'; +import { createImageResponse } from '@shared-helpers/create-image-response'; import { Hono } from 'hono'; import { HTTPException } from 'hono/http-exception'; -import { createImageResponse } from '../../../../../shared-helpers/create-image-response'; import { standardErrorHandler } from '../../../../../utils/response'; -import { getMetricScreenshotHandler } from './getMetricScreenshotHandler'; const app = new Hono() .get( @@ -42,12 +42,16 @@ const app = new Hono() } try { - const screenshotBuffer = await getMetricScreenshotHandler({ + const screenshotBuffer = await getMetricScreenshot({ metricId, width, height, version_number, - context: c, + type, + supabaseCookieKey: c.get('supabaseCookieKey'), + supabaseUser: c.get('supabaseUser'), + accessToken: c.get('accessToken') || '', + organizationId: metric.organizationId, }); return createImageResponse(screenshotBuffer, type); diff --git a/apps/server/src/api/v2/metric_files/[id]/screenshot/getMetricScreenshotHandler.ts b/apps/server/src/api/v2/metric_files/[id]/screenshot/getMetricScreenshotHandler.ts deleted file mode 100644 index f2d8efcdf..000000000 --- a/apps/server/src/api/v2/metric_files/[id]/screenshot/getMetricScreenshotHandler.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { BrowserParamsContextOrDirectRequest } from '@shared-helpers/browser-login'; -import { createHrefFromLink } from '@shared-helpers/create-href-from-link'; -import { DEFAULT_SCREENSHOT_CONFIG } from '@shared-helpers/screenshot-config'; - -type GetMetricScreenshotHandlerArgs = { - metricId: string; - width: number; - height: number; - version_number: number | undefined; - type?: 'png' | 'jpeg'; -} & BrowserParamsContextOrDirectRequest; - -export const getMetricScreenshotHandler = async (args: GetMetricScreenshotHandlerArgs) => { - const { browserLogin } = await import('../../../../../shared-helpers/browser-login'); - const { width, height, type = DEFAULT_SCREENSHOT_CONFIG.type } = args; - - const { result: screenshotBuffer } = await browserLogin({ - ...args, - fullPath: createHrefFromLink({ - to: '/screenshots/metrics/$metricId/content' as const, - params: { metricId: args.metricId }, - search: { - version_number: args.version_number, - width, - height, - }, - }), - callback: async ({ page }) => { - return await page.screenshot({ type }); - }, - }); - - return screenshotBuffer; -}; diff --git a/apps/server/src/api/v2/metric_files/[id]/screenshot/saveMetricScreenshotHandler.ts b/apps/server/src/api/v2/metric_files/[id]/screenshot/saveMetricScreenshotHandler.ts deleted file mode 100644 index 52e0c8858..000000000 --- a/apps/server/src/api/v2/metric_files/[id]/screenshot/saveMetricScreenshotHandler.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { - getUserOrganizationId, - hasMetricScreenshotBeenTakenWithin, -} from '@buster/database/queries'; -import type { BrowserParamsContextOrDirectRequest } from '@shared-helpers/browser-login'; -import { DEFAULT_SCREENSHOT_CONFIG } from '@shared-helpers/screenshot-config'; -import { uploadScreenshotHandler } from '@shared-helpers/upload-screenshot-handler'; -import dayjs from 'dayjs'; -import { getMetricScreenshotHandler } from './getMetricScreenshotHandler'; - -const shouldTakenNewScreenshot = async ({ - metricId, - isOnSaveEvent, -}: { metricId: string; isOnSaveEvent: boolean }) => { - if (isOnSaveEvent) { - return true; - } - - const isScreenshotExpired = await hasMetricScreenshotBeenTakenWithin( - metricId, - dayjs().subtract(6, 'hours') - ); - - return !isScreenshotExpired; -}; - -type SaveMetricScreenshotHandlerArgs = { - metricId: string; - version_number: number | undefined; - isOnSaveEvent: boolean; -} & BrowserParamsContextOrDirectRequest; - -const activelyCapturingScreenshot = new Set(); - -export const saveMetricScreenshotHandler = async (args: SaveMetricScreenshotHandlerArgs) => { - try { - const { isOnSaveEvent, metricId } = args; - - if (activelyCapturingScreenshot.has(metricId)) { - return; - } - - console.log('activelyCapturingScreenshot', activelyCapturingScreenshot); - - const shouldTakeNewScreenshot = await shouldTakenNewScreenshot({ - metricId, - isOnSaveEvent, - }); - - if (!shouldTakeNewScreenshot) { - return; - } - - activelyCapturingScreenshot.add(metricId); - const organizationId = - 'context' in args - ? args.context.get('userOrganizationInfo')?.organizationId || - (await getUserOrganizationId(args.context.get('busterUser')?.id))?.organizationId - : args.organizationId; - - if (!organizationId) { - return { - success: false, - }; - } - - const screenshotBuffer = await getMetricScreenshotHandler({ - ...args, - width: DEFAULT_SCREENSHOT_CONFIG.width, - height: DEFAULT_SCREENSHOT_CONFIG.height, - }); - - console.log('screenshotBuffer', screenshotBuffer.length); - - const result = await uploadScreenshotHandler({ - assetType: 'metric_file', - assetId: metricId, - image: screenshotBuffer, - organizationId, - }); - - return result; - } catch (error) { - console.error('Error in saveMetricScreenshotHandler:', error); - return { - success: false, - }; - } finally { - activelyCapturingScreenshot.delete(args.metricId); - } -}; diff --git a/apps/server/src/shared-helpers/create-image-response.ts b/apps/server/src/shared-helpers/create-image-response.ts index 8e77aaccd..e3923403c 100644 --- a/apps/server/src/shared-helpers/create-image-response.ts +++ b/apps/server/src/shared-helpers/create-image-response.ts @@ -1,8 +1,6 @@ -import { DEFAULT_SCREENSHOT_CONFIG } from './screenshot-config'; - export const createImageResponse = ( imageBuffer: Buffer, - type: 'png' | 'jpeg' = DEFAULT_SCREENSHOT_CONFIG.type + type: 'png' | 'jpeg' = 'png' ): Response => { return new Response(imageBuffer, { headers: { diff --git a/apps/server/src/shared-helpers/metric-helpers.test.ts b/apps/server/src/shared-helpers/metric-helpers.test.ts index 1f75eb63c..c43ebd1e1 100644 --- a/apps/server/src/shared-helpers/metric-helpers.test.ts +++ b/apps/server/src/shared-helpers/metric-helpers.test.ts @@ -102,6 +102,7 @@ describe('metric-helpers', () => { deletedAt: null, screenshotBucketKey: null, savedToLibrary: false, + screenshotTakenAt: null, ...overrides, }); diff --git a/apps/trigger/package.json b/apps/trigger/package.json index 89dd0cca9..8e7cab2a1 100644 --- a/apps/trigger/package.json +++ b/apps/trigger/package.json @@ -3,6 +3,16 @@ "version": "1.0.0", "private": false, "type": "module", + "exports": { + "./task-keys": { + "types": "./src/task-keys.ts", + "default": "./src/task-keys.ts" + }, + "./task-schemas": { + "types": "./src/task-schemas.ts", + "default": "./src/task-schemas.ts" + } + }, "scripts": { "dev": "echo 'y' | npx trigger.dev@latest dev --env-file ../../.env", "dev:fast": "echo 'y' | npx trigger.dev@latest dev", @@ -22,6 +32,7 @@ "dependencies": { "@aws-sdk/client-s3": "catalog:", "@aws-sdk/s3-request-presigner": "catalog:", + "@buster-app/supabase": "workspace:*", "@buster/access-controls": "workspace:*", "@buster/ai": "workspace:*", "@buster/data-source": "workspace:^", @@ -29,19 +40,21 @@ "@buster/search": "workspace:*", "@buster/server-shared": "workspace:*", "@buster/slack": "workspace:*", - "@buster-app/supabase": "workspace:*", "@buster/test-utils": "workspace:*", "@buster/typescript-config": "workspace:*", "@buster/vitest-config": "workspace:*", "@buster/web-tools": "workspace:*", "@duckdb/node-api": "1.3.2-alpha.26", "@duckdb/node-bindings": "1.3.2-alpha.26", + "@supabase/supabase-js": "catalog:", "@trigger.dev/sdk": "4.0.4", "@types/js-yaml": "catalog:", "ai": "catalog:", "braintrust": "catalog:", + "dayjs": "^1.11.18", "drizzle-orm": "catalog:", "js-yaml": "catalog:", + "playwright": "^1.55.1", "vitest": "catalog:", "zod": "catalog:" }, diff --git a/apps/trigger/src/task-keys.ts b/apps/trigger/src/task-keys.ts new file mode 100644 index 000000000..448cff2c7 --- /dev/null +++ b/apps/trigger/src/task-keys.ts @@ -0,0 +1 @@ +export * from './tasks/screenshots/task-keys'; diff --git a/apps/trigger/src/task-schemas.ts b/apps/trigger/src/task-schemas.ts new file mode 100644 index 000000000..4ae777e62 --- /dev/null +++ b/apps/trigger/src/task-schemas.ts @@ -0,0 +1,5 @@ +//SCREENSHOTS +export { + TakeMetricScreenshotTriggerSchema, + type TakeMetricScreenshotTrigger, +} from './tasks/screenshots/schemas'; diff --git a/apps/trigger/src/tasks/screenshots/index.ts b/apps/trigger/src/tasks/screenshots/index.ts new file mode 100644 index 000000000..f66d85065 --- /dev/null +++ b/apps/trigger/src/tasks/screenshots/index.ts @@ -0,0 +1 @@ +export { takeMetricScreenshotHandlerTask } from './take-metric-screenshot-handler'; diff --git a/apps/trigger/src/tasks/screenshots/schemas.ts b/apps/trigger/src/tasks/screenshots/schemas.ts new file mode 100644 index 000000000..9fcf214cf --- /dev/null +++ b/apps/trigger/src/tasks/screenshots/schemas.ts @@ -0,0 +1,9 @@ +import { GetMetricScreenshotHandlerArgsSchema } from '@buster/server-shared/screenshots'; +import { z } from 'zod'; + +export const TakeMetricScreenshotTriggerSchema = GetMetricScreenshotHandlerArgsSchema.extend({ + isOnSaveEvent: z.boolean(), + metricId: z.string(), +}); + +export type TakeMetricScreenshotTrigger = z.infer; diff --git a/apps/trigger/src/tasks/screenshots/take-metric-screenshot-handler.ts b/apps/trigger/src/tasks/screenshots/take-metric-screenshot-handler.ts new file mode 100644 index 000000000..8dfd973b2 --- /dev/null +++ b/apps/trigger/src/tasks/screenshots/take-metric-screenshot-handler.ts @@ -0,0 +1,70 @@ +import { hasMetricScreenshotBeenTakenWithin } from '@buster/database/queries'; +import { getMetricScreenshot } from '@buster/server-shared/screenshots'; +import { logger, schemaTask } from '@trigger.dev/sdk'; +import dayjs from 'dayjs'; +import { z } from 'zod'; +import { TakeMetricScreenshotTriggerSchema } from './schemas'; +import { screenshots_task_keys } from './task-keys'; +import { uploadScreenshotHandler } from './upload-screenshot-handler'; + +export const takeMetricScreenshotHandlerTask: ReturnType< + typeof schemaTask< + typeof screenshots_task_keys.take_metric_screenshot, + typeof TakeMetricScreenshotTriggerSchema, + { success: boolean } | undefined + > +> = schemaTask({ + id: screenshots_task_keys.take_metric_screenshot, + schema: TakeMetricScreenshotTriggerSchema, + maxDuration: 60 * 3, // 3 minutes max + retry: { + maxAttempts: 1, + minTimeoutInMs: 1000, // 1 second + maxTimeoutInMs: 60 * 1000, // 1 minute + }, + run: async (args) => { + logger.info('Getting metric screenshot', { args }); + + const { isOnSaveEvent, metricId, organizationId } = args; + + const shouldTakeNewScreenshot = await shouldTakenNewScreenshot({ + metricId, + isOnSaveEvent, + }); + + if (!shouldTakeNewScreenshot) { + return; + } + + const screenshotBuffer = await getMetricScreenshot(args); + + logger.info('Metric screenshot taken', { screenshotBufferLength: screenshotBuffer.length }); + + const result = await uploadScreenshotHandler({ + assetType: 'metric_file', + assetId: metricId, + image: screenshotBuffer, + organizationId, + }); + + logger.info('Metric screenshot uploaded', { result }); + + return result; + }, +}); + +const shouldTakenNewScreenshot = async ({ + metricId, + isOnSaveEvent, +}: { metricId: string; isOnSaveEvent: boolean }) => { + if (isOnSaveEvent) { + return true; + } + + const isScreenshotExpired = await hasMetricScreenshotBeenTakenWithin( + metricId, + dayjs().subtract(6, 'hours') + ); + + return !isScreenshotExpired; +}; diff --git a/apps/trigger/src/tasks/screenshots/task-keys.ts b/apps/trigger/src/tasks/screenshots/task-keys.ts new file mode 100644 index 000000000..ade863136 --- /dev/null +++ b/apps/trigger/src/tasks/screenshots/task-keys.ts @@ -0,0 +1,3 @@ +export const screenshots_task_keys = { + take_metric_screenshot: 'take-metric-screenshot', +} as const; diff --git a/apps/trigger/src/tasks/screenshots/upload-screenshot-handler.ts b/apps/trigger/src/tasks/screenshots/upload-screenshot-handler.ts new file mode 100644 index 000000000..560fa3c7b --- /dev/null +++ b/apps/trigger/src/tasks/screenshots/upload-screenshot-handler.ts @@ -0,0 +1,177 @@ +import { getProviderForOrganization } from '@buster/data-source'; +import { updateAssetScreenshotBucketKey } from '@buster/database/queries'; +import type { AssetType } from '@buster/server-shared/assets'; +import { AssetTypeSchema } from '@buster/server-shared/assets'; +import { + PutChatScreenshotRequestSchema, + type PutScreenshotResponse, + PutScreenshotResponseSchema, +} from '@buster/server-shared/screenshots'; +import z from 'zod'; + +export const UploadScreenshotParamsSchema = PutChatScreenshotRequestSchema.extend({ + assetType: AssetTypeSchema, + assetId: z.string().uuid('Asset ID must be a valid UUID'), + organizationId: z.string().uuid('Organization ID must be a valid UUID'), +}); + +export type UploadScreenshotParams = z.infer; + +function getExtensionFromContentType(contentType: string): string { + switch (contentType) { + case 'image/jpeg': + case 'image/jpg': + return '.jpg'; + case 'image/png': + return '.png'; + case 'image/webp': + return '.webp'; + default: + return '.png'; + } +} + +function parseBase64Image(base64Image: string): { + buffer: Buffer; + contentType: string; + extension: string; +} { + const dataUriPattern = /^data:(?[^;]+);base64,(?.+)$/; + const match = base64Image.match(dataUriPattern); + + const contentType = match?.groups?.mime ?? 'image/png'; + const base64Data = match?.groups?.data ?? base64Image; + + const buffer = Buffer.from(base64Data, 'base64'); + + if (buffer.length === 0) { + throw new Error('Provided image data is empty'); + } + + return { + buffer, + contentType, + extension: getExtensionFromContentType(contentType), + }; +} + +async function parseImageFile(file: File): Promise<{ + buffer: Buffer; + contentType: string; + extension: string; +}> { + const arrayBuffer = await file.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + + if (buffer.length === 0) { + throw new Error('Provided image file is empty'); + } + + return { + buffer, + contentType: file.type, + extension: getExtensionFromContentType(file.type), + }; +} + +function detectImageTypeFromBuffer(buffer: Buffer): { contentType: string; extension: string } { + // Check PNG signature (89 50 4E 47) + if ( + buffer.length >= 4 && + buffer[0] === 0x89 && + buffer[1] === 0x50 && + buffer[2] === 0x4e && + buffer[3] === 0x47 + ) { + return { contentType: 'image/png', extension: '.png' }; + } + + // Check JPEG signature (FF D8 FF) + if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) { + return { contentType: 'image/jpeg', extension: '.jpg' }; + } + + // Check WebP signature (RIFF ... WEBP) + if ( + buffer.length >= 12 && + buffer[0] === 0x52 && + buffer[1] === 0x49 && + buffer[2] === 0x46 && + buffer[3] === 0x46 && + buffer[8] === 0x57 && + buffer[9] === 0x45 && + buffer[10] === 0x42 && + buffer[11] === 0x50 + ) { + return { contentType: 'image/webp', extension: '.webp' }; + } + + // Default to PNG if unknown + return { contentType: 'image/png', extension: '.png' }; +} + +function parseBuffer(buffer: Buffer): { + buffer: Buffer; + contentType: string; + extension: string; +} { + const { contentType, extension } = detectImageTypeFromBuffer(buffer); + return { + buffer, + contentType, + extension, + }; +} + +async function parseImageInput(image: string | File | Buffer): Promise<{ + buffer: Buffer; + contentType: string; + extension: string; +}> { + if (Buffer.isBuffer(image)) { + return parseBuffer(image); + } + if (typeof image === 'string') { + return parseBase64Image(image); + } + return parseImageFile(image); +} + +function buildScreenshotKey( + assetType: AssetType, + assetId: string, + extension: string, + organizationId: string +): string { + return `screenshots/${organizationId}/${assetType}-${assetId}${extension}`; +} + +export async function uploadScreenshotHandler( + params: UploadScreenshotParams +): Promise { + const { assetType, assetId, image, organizationId } = UploadScreenshotParamsSchema.parse(params); + + const { buffer, contentType, extension } = await parseImageInput(image); + + const targetKey = buildScreenshotKey(assetType, assetId, extension, organizationId); + + const provider = await getProviderForOrganization(organizationId); + const result = await provider.upload(targetKey, buffer, { + contentType, + }); + + if (!result.success) { + throw new Error(result.error ?? 'Failed to upload screenshot'); + } + + await updateAssetScreenshotBucketKey({ + assetId, + assetType, + screenshotBucketKey: result.key, + }); + + return PutScreenshotResponseSchema.parse({ + success: true, + bucketKey: result.key, + }); +} diff --git a/packages/server-shared/package.json b/packages/server-shared/package.json index 937e5f416..f0cbeefe2 100644 --- a/packages/server-shared/package.json +++ b/packages/server-shared/package.json @@ -130,6 +130,8 @@ "@buster/database": "workspace:*", "@buster/typescript-config": "workspace:*", "@buster/vitest-config": "workspace:*", + "@supabase/supabase-js": "catalog:", + "playwright": "^1.55.1", "zod": "catalog:" }, "devDependencies": { diff --git a/packages/server-shared/src/index.ts b/packages/server-shared/src/index.ts index 2b434bd91..ccef0e17a 100644 --- a/packages/server-shared/src/index.ts +++ b/packages/server-shared/src/index.ts @@ -22,7 +22,6 @@ export * from './metrics'; export * from './organization'; export * from './public-chat'; export * from './s3-integrations'; -export * from './screenshots'; export * from './search'; export * from './security'; // Export share module (has some naming conflicts with chats and metrics) diff --git a/packages/server-shared/src/screenshots/index.ts b/packages/server-shared/src/screenshots/index.ts index e21ddf45b..8d25088f1 100644 --- a/packages/server-shared/src/screenshots/index.ts +++ b/packages/server-shared/src/screenshots/index.ts @@ -4,3 +4,4 @@ export * from './requests.dashboards'; export * from './requests.reports'; export * from './requests.chats'; export * from './requests.base'; +export * from './methods'; diff --git a/apps/server/src/shared-helpers/browser-login.ts b/packages/server-shared/src/screenshots/methods/browser-login.ts similarity index 76% rename from apps/server/src/shared-helpers/browser-login.ts rename to packages/server-shared/src/screenshots/methods/browser-login.ts index 7eaeaadba..171945a53 100644 --- a/apps/server/src/shared-helpers/browser-login.ts +++ b/packages/server-shared/src/screenshots/methods/browser-login.ts @@ -1,29 +1,25 @@ import type { User } from '@supabase/supabase-js'; -import type { Context } from 'hono'; import type { Browser, Page } from 'playwright'; +import { z } from 'zod'; import { DEFAULT_SCREENSHOT_CONFIG } from './screenshot-config'; type BrowserParamsBase = { - width: number | undefined; - height: number | undefined; + width?: number | undefined; + height?: number | undefined; fullPath: string; callback: ({ page, browser }: { page: Page; browser: Browser }) => Promise; }; -type BrowserParamsContext = { - context: Context; -}; +export const BrowserParamsContextSchema = z.object({ + supabaseUser: z.any() as z.ZodType, + supabaseCookieKey: z.string(), + accessToken: z.string(), + organizationId: z.string(), +}); -type BrowserParamsDirectRequest = { - supabaseUser: User; - supabaseCookieKey: string; - accessToken: string; - organizationId: string; -}; +export type BrowserParamsContext = z.infer; -export type BrowserParamsContextOrDirectRequest = BrowserParamsContext | BrowserParamsDirectRequest; - -export type BrowserParams> = BrowserParamsContextOrDirectRequest & +export type BrowserParams> = BrowserParamsContext & BrowserParamsBase; export const browserLogin = async >({ @@ -31,15 +27,10 @@ export const browserLogin = async >({ height = DEFAULT_SCREENSHOT_CONFIG.height, fullPath, callback, - ...rest + supabaseUser, + supabaseCookieKey, + accessToken, }: BrowserParams) => { - const isContext = 'context' in rest; - const supabaseUser = isContext ? rest.context.get('supabaseUser') : rest.supabaseUser; - const supabaseCookieKey = isContext - ? rest.context.get('supabaseCookieKey') - : rest.supabaseCookieKey; - const accessToken = isContext ? rest.context.get('accessToken') : rest.accessToken; - if (!accessToken) { throw new Error('Missing Authorization header'); } diff --git a/apps/server/src/shared-helpers/create-href-from-link.test.ts b/packages/server-shared/src/screenshots/methods/create-href-from-link.test.ts similarity index 100% rename from apps/server/src/shared-helpers/create-href-from-link.test.ts rename to packages/server-shared/src/screenshots/methods/create-href-from-link.test.ts diff --git a/apps/server/src/shared-helpers/create-href-from-link.ts b/packages/server-shared/src/screenshots/methods/create-href-from-link.ts similarity index 100% rename from apps/server/src/shared-helpers/create-href-from-link.ts rename to packages/server-shared/src/screenshots/methods/create-href-from-link.ts diff --git a/packages/server-shared/src/screenshots/methods/get-chat-screenshot.ts b/packages/server-shared/src/screenshots/methods/get-chat-screenshot.ts new file mode 100644 index 000000000..bd1c0d95e --- /dev/null +++ b/packages/server-shared/src/screenshots/methods/get-chat-screenshot.ts @@ -0,0 +1,33 @@ +import { z } from 'zod'; +import { BrowserParamsContextSchema, browserLogin } from './browser-login'; +import { createHrefFromLink } from './create-href-from-link'; +import { DEFAULT_SCREENSHOT_CONFIG } from './screenshot-config'; + +export const GetChatScreenshotHandlerArgsSchema = z + .object({ + chatId: z.string().uuid('Chat ID must be a valid UUID'), + width: z.number().default(DEFAULT_SCREENSHOT_CONFIG.width).optional(), + height: z.number().default(DEFAULT_SCREENSHOT_CONFIG.height).optional(), + type: z.enum(['png', 'jpeg']).default(DEFAULT_SCREENSHOT_CONFIG.type).optional(), + }) + .extend(BrowserParamsContextSchema.shape); + +export type GetChatScreenshotHandlerArgs = z.infer; + +export const getChatScreenshot = async (args: GetChatScreenshotHandlerArgs) => { + const { type = DEFAULT_SCREENSHOT_CONFIG.type } = args; + + const { result: screenshotBuffer } = await browserLogin({ + ...args, + fullPath: createHrefFromLink({ + to: '/screenshots/chats/$chatId/content' as const, + params: { chatId: args.chatId }, + }), + callback: async ({ page }) => { + const screenshotBuffer = await page.screenshot({ type }); + return screenshotBuffer; + }, + }); + + return screenshotBuffer; +}; diff --git a/packages/server-shared/src/screenshots/methods/get-dashboard-screenshot.ts b/packages/server-shared/src/screenshots/methods/get-dashboard-screenshot.ts new file mode 100644 index 000000000..6c64898f4 --- /dev/null +++ b/packages/server-shared/src/screenshots/methods/get-dashboard-screenshot.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; +import { BrowserParamsContextSchema, browserLogin } from './browser-login'; +import { createHrefFromLink } from './create-href-from-link'; +import { DEFAULT_SCREENSHOT_CONFIG } from './screenshot-config'; + +export const GetDashboardScreenshotHandlerArgsSchema = z + .object({ + dashboardId: z.string().uuid('Dashboard ID must be a valid UUID'), + width: z.number().default(DEFAULT_SCREENSHOT_CONFIG.width).optional(), + height: z.number().default(DEFAULT_SCREENSHOT_CONFIG.height).optional(), + version_number: z.number().optional(), + type: z.enum(['png', 'jpeg']).default(DEFAULT_SCREENSHOT_CONFIG.type).optional(), + }) + .extend(BrowserParamsContextSchema.shape); + +export type GetDashboardScreenshotHandlerArgs = z.infer< + typeof GetDashboardScreenshotHandlerArgsSchema +>; + +export const getDashboardScreenshot = async (args: GetDashboardScreenshotHandlerArgs) => { + const { type = DEFAULT_SCREENSHOT_CONFIG.type } = args; + + const { result: screenshotBuffer } = await browserLogin({ + ...args, + fullPath: createHrefFromLink({ + to: '/screenshots/dashboards/$dashboardId/content' as const, + params: { dashboardId: args.dashboardId }, + search: { + version_number: args.version_number, + }, + }), + callback: async ({ page }) => { + const screenshotBuffer = await page.screenshot({ type }); + return screenshotBuffer; + }, + }); + + return screenshotBuffer; +}; diff --git a/packages/server-shared/src/screenshots/methods/get-metric-screenshot.ts b/packages/server-shared/src/screenshots/methods/get-metric-screenshot.ts new file mode 100644 index 000000000..1c3ba79e5 --- /dev/null +++ b/packages/server-shared/src/screenshots/methods/get-metric-screenshot.ts @@ -0,0 +1,39 @@ +import { z } from 'zod'; +import { BrowserParamsContextSchema, browserLogin } from './browser-login'; +import { createHrefFromLink } from './create-href-from-link'; +import { DEFAULT_SCREENSHOT_CONFIG } from './screenshot-config'; + +export const GetMetricScreenshotHandlerArgsSchema = z + .object({ + metricId: z.string().uuid('Metric ID must be a valid UUID'), + width: z.number().default(DEFAULT_SCREENSHOT_CONFIG.width).optional(), + height: z.number().default(DEFAULT_SCREENSHOT_CONFIG.height).optional(), + version_number: z.number().optional(), + type: z.enum(['png', 'jpeg']).default(DEFAULT_SCREENSHOT_CONFIG.type).optional(), + }) + .extend(BrowserParamsContextSchema.shape); + +export type GetMetricScreenshotHandlerArgs = z.infer; + +export const getMetricScreenshot = async ( + args: GetMetricScreenshotHandlerArgs +): Promise> => { + const { type = DEFAULT_SCREENSHOT_CONFIG.type } = args; + + const { result: screenshotBuffer } = await browserLogin({ + ...args, + fullPath: createHrefFromLink({ + to: '/screenshots/metrics/$metricId/content' as const, + params: { metricId: args.metricId }, + search: { + version_number: args.version_number, + }, + }), + callback: async ({ page }) => { + const screenshotBuffer = await page.screenshot({ type }); + return screenshotBuffer; + }, + }); + + return screenshotBuffer; +}; diff --git a/packages/server-shared/src/screenshots/methods/get-report-screenshot.ts b/packages/server-shared/src/screenshots/methods/get-report-screenshot.ts new file mode 100644 index 000000000..d9a80e65e --- /dev/null +++ b/packages/server-shared/src/screenshots/methods/get-report-screenshot.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; +import { BrowserParamsContextSchema, browserLogin } from './browser-login'; +import { createHrefFromLink } from './create-href-from-link'; +import { DEFAULT_SCREENSHOT_CONFIG } from './screenshot-config'; + +export const GetReportScreenshotHandlerArgsSchema = z + .object({ + reportId: z.string().uuid('Report ID must be a valid UUID'), + width: z.number().default(DEFAULT_SCREENSHOT_CONFIG.width).optional(), + height: z.number().default(DEFAULT_SCREENSHOT_CONFIG.height).optional(), + version_number: z.number().optional(), + type: z.enum(['png', 'jpeg']).default(DEFAULT_SCREENSHOT_CONFIG.type).optional(), + }) + .extend(BrowserParamsContextSchema.shape); + +export type GetReportScreenshotHandlerArgs = z.infer; + +export const getReportScreenshot = async (args: GetReportScreenshotHandlerArgs) => { + const { type = DEFAULT_SCREENSHOT_CONFIG.type } = args; + + const { result: screenshotBuffer } = await browserLogin({ + ...args, + fullPath: createHrefFromLink({ + to: '/screenshots/reports/$reportId/content' as const, + params: { reportId: args.reportId }, + search: { + version_number: args.version_number, + }, + }), + callback: async ({ page }) => { + const screenshotBuffer = await page.screenshot({ type }); + return screenshotBuffer; + }, + }); + + return screenshotBuffer; +}; diff --git a/packages/server-shared/src/screenshots/methods/index.ts b/packages/server-shared/src/screenshots/methods/index.ts new file mode 100644 index 000000000..373274788 --- /dev/null +++ b/packages/server-shared/src/screenshots/methods/index.ts @@ -0,0 +1,7 @@ +export { getMetricScreenshot, GetMetricScreenshotHandlerArgsSchema } from './get-metric-screenshot'; +export { + getDashboardScreenshot, + GetDashboardScreenshotHandlerArgsSchema, +} from './get-dashboard-screenshot'; +export { getChatScreenshot, GetChatScreenshotHandlerArgsSchema } from './get-chat-screenshot'; +export { getReportScreenshot, GetReportScreenshotHandlerArgsSchema } from './get-report-screenshot'; diff --git a/apps/server/src/shared-helpers/screenshot-config.ts b/packages/server-shared/src/screenshots/methods/screenshot-config.ts similarity index 100% rename from apps/server/src/shared-helpers/screenshot-config.ts rename to packages/server-shared/src/screenshots/methods/screenshot-config.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55a5c361d..24e6ef592 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,7 +19,7 @@ catalogs: specifier: ^1.0.12 version: 1.0.12 '@supabase/supabase-js': - specifier: ^2.57.4 + specifier: 2.57.4 version: 2.57.4 '@types/js-yaml': specifier: 4.0.9 @@ -243,6 +243,9 @@ importers: apps/server: dependencies: + '@buster-app/trigger': + specifier: workspace:* + version: link:../trigger '@buster/access-controls': specifier: workspace:* version: link:../../packages/access-controls @@ -396,6 +399,9 @@ importers: '@duckdb/node-bindings': specifier: 1.3.2-alpha.26 version: 1.3.2-alpha.26 + '@supabase/supabase-js': + specifier: 'catalog:' + version: 2.57.4 '@trigger.dev/sdk': specifier: 4.0.4 version: 4.0.4(ai@5.0.44(zod@3.25.76))(zod@3.25.76) @@ -408,12 +414,18 @@ importers: braintrust: specifier: 'catalog:' version: 0.3.7(@aws-sdk/credential-provider-web-identity@3.888.0)(zod@3.25.76) + dayjs: + specifier: ^1.11.18 + version: 1.11.18 drizzle-orm: specifier: 'catalog:' version: 0.44.5(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(bun-types@1.2.21(@types/react@19.1.13))(mysql2@3.14.1)(pg@8.16.3)(postgres@3.4.7) js-yaml: specifier: 'catalog:' version: 4.1.0 + playwright: + specifier: ^1.55.1 + version: 1.55.1 vitest: specifier: 'catalog:' version: 3.2.4(@edge-runtime/vm@3.2.0)(@types/debug@4.1.12)(@types/node@24.3.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(msw@2.11.3(@types/node@24.3.1)(typescript@5.9.2))(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) @@ -1347,6 +1359,12 @@ importers: '@buster/vitest-config': specifier: workspace:* version: link:../vitest-config + '@supabase/supabase-js': + specifier: 'catalog:' + version: 2.57.4 + playwright: + specifier: ^1.55.1 + version: 1.55.1 zod: specifier: 'catalog:' version: 3.25.76 @@ -19977,11 +19995,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/browser@3.2.4(msw@2.11.3(@types/node@22.18.1)(typescript@5.9.2))(playwright@1.55.1)(vite@7.1.4(@types/node@22.18.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4)': + '@vitest/browser@3.2.4(msw@2.11.3(@types/node@22.18.1)(typescript@5.9.2))(playwright@1.55.1)(vite@7.1.9(@types/node@22.18.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4)': dependencies: '@testing-library/dom': 10.4.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) - '@vitest/mocker': 3.2.4(msw@2.11.3(@types/node@22.18.1)(typescript@5.9.2))(vite@7.1.4(@types/node@22.18.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(msw@2.11.3(@types/node@22.18.1)(typescript@5.9.2))(vite@7.1.9(@types/node@22.18.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) '@vitest/utils': 3.2.4 magic-string: 0.30.17 sirv: 3.0.1 @@ -20006,25 +20024,6 @@ snapshots: magic-string: 0.30.17 sirv: 3.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@edge-runtime/vm@3.2.0)(@types/debug@4.1.12)(@types/node@24.3.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.11.3(@types/node@24.3.1)(typescript@5.9.2))(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) - ws: 8.18.3 - optionalDependencies: - playwright: 1.55.1 - transitivePeerDependencies: - - bufferutil - - msw - - utf-8-validate - - vite - - '@vitest/browser@3.2.4(msw@2.11.3(@types/node@24.3.1)(typescript@5.9.2))(playwright@1.55.1)(vite@7.1.9(@types/node@24.3.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4)': - dependencies: - '@testing-library/dom': 10.4.1 - '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) - '@vitest/mocker': 3.2.4(msw@2.11.3(@types/node@24.3.1)(typescript@5.9.2))(vite@7.1.9(@types/node@24.3.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1)) - '@vitest/utils': 3.2.4 - magic-string: 0.30.17 - sirv: 3.0.1 - tinyrainbow: 2.0.0 vitest: 3.2.4(@edge-runtime/vm@3.2.0)(@types/debug@4.1.12)(@types/node@24.3.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(msw@2.11.3(@types/node@24.3.1)(typescript@5.9.2))(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) ws: 8.18.3 optionalDependencies: @@ -20034,7 +20033,6 @@ snapshots: - msw - utf-8-validate - vite - optional: true '@vitest/coverage-v8@3.2.4(@vitest/browser@3.2.4)(vitest@3.2.4)': dependencies: @@ -20074,6 +20072,16 @@ snapshots: msw: 2.11.3(@types/node@22.18.1)(typescript@5.9.2) vite: 7.1.4(@types/node@22.18.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) + '@vitest/mocker@3.2.4(msw@2.11.3(@types/node@22.18.1)(typescript@5.9.2))(vite@7.1.9(@types/node@22.18.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + msw: 2.11.3(@types/node@22.18.1)(typescript@5.9.2) + vite: 7.1.9(@types/node@22.18.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) + optional: true + '@vitest/mocker@3.2.4(msw@2.11.3(@types/node@24.3.1)(typescript@5.9.2))(vite@7.1.4(@types/node@24.3.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 @@ -20083,16 +20091,6 @@ snapshots: msw: 2.11.3(@types/node@24.3.1)(typescript@5.9.2) vite: 7.1.4(@types/node@24.3.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) - '@vitest/mocker@3.2.4(msw@2.11.3(@types/node@24.3.1)(typescript@5.9.2))(vite@7.1.9(@types/node@24.3.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))': - dependencies: - '@vitest/spy': 3.2.4 - estree-walker: 3.0.3 - magic-string: 0.30.17 - optionalDependencies: - msw: 2.11.3(@types/node@24.3.1)(typescript@5.9.2) - vite: 7.1.9(@types/node@24.3.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) - optional: true - '@vitest/pretty-format@3.2.4': dependencies: tinyrainbow: 2.0.0 @@ -20122,7 +20120,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@edge-runtime/vm@3.2.0)(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(msw@2.11.3(@types/node@22.18.1)(typescript@5.9.2))(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) + vitest: 3.2.4(@edge-runtime/vm@3.2.0)(@types/debug@4.1.12)(@types/node@24.3.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(msw@2.11.3(@types/node@24.3.1)(typescript@5.9.2))(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) '@vitest/utils@3.2.4': dependencies: @@ -27383,6 +27381,25 @@ snapshots: tsx: 4.20.5 yaml: 2.8.1 + vite@7.1.9(@types/node@22.18.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1): + dependencies: + esbuild: 0.25.9 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.50.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 22.18.1 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.1 + sass: 1.93.2 + terser: 5.43.1 + tsx: 4.20.5 + yaml: 2.8.1 + optional: true + vite@7.1.9(@types/node@24.3.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1): dependencies: esbuild: 0.25.9 @@ -27444,7 +27461,7 @@ snapshots: '@edge-runtime/vm': 3.2.0 '@types/debug': 4.1.12 '@types/node': 22.18.1 - '@vitest/browser': 3.2.4(msw@2.11.3(@types/node@22.18.1)(typescript@5.9.2))(playwright@1.55.1)(vite@7.1.4(@types/node@22.18.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4) + '@vitest/browser': 3.2.4(msw@2.11.3(@types/node@22.18.1)(typescript@5.9.2))(playwright@1.55.1)(vite@7.1.9(@types/node@22.18.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4) '@vitest/ui': 3.2.4(vitest@3.2.4) jsdom: 27.0.0(postcss@8.5.6) transitivePeerDependencies: @@ -27536,7 +27553,7 @@ snapshots: '@edge-runtime/vm': 3.2.0 '@types/debug': 4.1.12 '@types/node': 24.3.1 - '@vitest/browser': 3.2.4(msw@2.11.3(@types/node@24.3.1)(typescript@5.9.2))(playwright@1.55.1)(vite@7.1.9(@types/node@24.3.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4) + '@vitest/browser': 3.2.4(msw@2.11.3(@types/node@24.3.1)(typescript@5.9.2))(playwright@1.55.1)(vite@7.1.4(@types/node@24.3.1)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1))(vitest@3.2.4) '@vitest/ui': 3.2.4(vitest@3.2.4) jsdom: 27.0.0(postcss@8.5.6) transitivePeerDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4ece27ea3..633b569fb 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -14,7 +14,7 @@ catalog: '@aws-sdk/s3-request-presigner': ^3.888.0 '@electric-sql/client': ^1.0.12 '@electric-sql/react': ^1.0.12 - '@supabase/supabase-js': ^2.57.4 + '@supabase/supabase-js': 2.57.4 '@trigger.dev/build': ^4.0.2 '@trigger.dev/sdk': ^4.0.2 '@types/js-yaml': 4.0.9