mirror of https://github.com/buster-so/buster.git
get screenshot from a get request
This commit is contained in:
parent
28c7cbebb2
commit
6d18a031c5
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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<string>();
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
|
@ -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: {
|
||||
|
|
|
@ -102,6 +102,7 @@ describe('metric-helpers', () => {
|
|||
deletedAt: null,
|
||||
screenshotBucketKey: null,
|
||||
savedToLibrary: false,
|
||||
screenshotTakenAt: null,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
|
|
|
@ -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:"
|
||||
},
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './tasks/screenshots/task-keys';
|
|
@ -0,0 +1,5 @@
|
|||
//SCREENSHOTS
|
||||
export {
|
||||
TakeMetricScreenshotTriggerSchema,
|
||||
type TakeMetricScreenshotTrigger,
|
||||
} from './tasks/screenshots/schemas';
|
|
@ -0,0 +1 @@
|
|||
export { takeMetricScreenshotHandlerTask } from './take-metric-screenshot-handler';
|
|
@ -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<typeof TakeMetricScreenshotTriggerSchema>;
|
|
@ -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;
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
export const screenshots_task_keys = {
|
||||
take_metric_screenshot: 'take-metric-screenshot',
|
||||
} as const;
|
|
@ -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<typeof UploadScreenshotParamsSchema>;
|
||||
|
||||
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:(?<mime>[^;]+);base64,(?<data>.+)$/;
|
||||
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<PutScreenshotResponse> {
|
||||
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,
|
||||
});
|
||||
}
|
|
@ -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": {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -4,3 +4,4 @@ export * from './requests.dashboards';
|
|||
export * from './requests.reports';
|
||||
export * from './requests.chats';
|
||||
export * from './requests.base';
|
||||
export * from './methods';
|
||||
|
|
|
@ -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<T> = {
|
||||
width: number | undefined;
|
||||
height: number | undefined;
|
||||
width?: number | undefined;
|
||||
height?: number | undefined;
|
||||
fullPath: string;
|
||||
callback: ({ page, browser }: { page: Page; browser: Browser }) => Promise<T>;
|
||||
};
|
||||
|
||||
type BrowserParamsContext = {
|
||||
context: Context;
|
||||
};
|
||||
export const BrowserParamsContextSchema = z.object({
|
||||
supabaseUser: z.any() as z.ZodType<User>,
|
||||
supabaseCookieKey: z.string(),
|
||||
accessToken: z.string(),
|
||||
organizationId: z.string(),
|
||||
});
|
||||
|
||||
type BrowserParamsDirectRequest = {
|
||||
supabaseUser: User;
|
||||
supabaseCookieKey: string;
|
||||
accessToken: string;
|
||||
organizationId: string;
|
||||
};
|
||||
export type BrowserParamsContext = z.infer<typeof BrowserParamsContextSchema>;
|
||||
|
||||
export type BrowserParamsContextOrDirectRequest = BrowserParamsContext | BrowserParamsDirectRequest;
|
||||
|
||||
export type BrowserParams<T = Buffer<ArrayBufferLike>> = BrowserParamsContextOrDirectRequest &
|
||||
export type BrowserParams<T = Buffer<ArrayBufferLike>> = BrowserParamsContext &
|
||||
BrowserParamsBase<T>;
|
||||
|
||||
export const browserLogin = async <T = Buffer<ArrayBufferLike>>({
|
||||
|
@ -31,15 +27,10 @@ export const browserLogin = async <T = Buffer<ArrayBufferLike>>({
|
|||
height = DEFAULT_SCREENSHOT_CONFIG.height,
|
||||
fullPath,
|
||||
callback,
|
||||
...rest
|
||||
supabaseUser,
|
||||
supabaseCookieKey,
|
||||
accessToken,
|
||||
}: BrowserParams<T>) => {
|
||||
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');
|
||||
}
|
|
@ -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<typeof GetChatScreenshotHandlerArgsSchema>;
|
||||
|
||||
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;
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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<typeof GetMetricScreenshotHandlerArgsSchema>;
|
||||
|
||||
export const getMetricScreenshot = async (
|
||||
args: GetMetricScreenshotHandlerArgs
|
||||
): Promise<Buffer<ArrayBufferLike>> => {
|
||||
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;
|
||||
};
|
|
@ -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<typeof GetReportScreenshotHandlerArgsSchema>;
|
||||
|
||||
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;
|
||||
};
|
|
@ -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';
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue