fix: removes empty imageUrl and videoUrl keys from elements (#6950)

This commit is contained in:
Anshuman Pandey
2025-12-09 15:22:01 +05:30
committed by GitHub
parent a8eea306e5
commit be38d76ccf
3 changed files with 128 additions and 7 deletions

View File

@@ -0,0 +1,99 @@
import { Prisma } from "@prisma/client";
import { logger } from "@formbricks/logger";
import type { MigrationScript } from "../../src/scripts/migration-runner";
import { type SurveyRecord } from "./types";
export const removeEmptyImageAndVideoUrlsFromElements: MigrationScript = {
type: "data",
id: "ohw7fb1f64yfh2vax294agp0",
name: "20251208033316_remove_empty_image_and_video_urls_from_elements",
run: async ({ tx }) => {
// Find all surveys with empty imageUrl or videoUrl
const surveysFindQuery = `
SELECT s.id, s.blocks, s."welcomeCard", s.endings
FROM "Survey" AS s
WHERE EXISTS (
SELECT 1
FROM unnest(s.blocks) AS block
CROSS JOIN jsonb_array_elements(block->'elements') AS element
WHERE element->>'imageUrl' = ''
OR element->>'videoUrl' = ''
) OR s."welcomeCard"->>'fileUrl' = ''
OR s."welcomeCard"->>'videoUrl' = ''
OR EXISTS (
SELECT 1
FROM unnest(s.endings) AS ending
WHERE ending->>'imageUrl' = ''
OR ending->>'videoUrl' = ''
)
`;
const surveysWithEmptyUrls: SurveyRecord[] = await tx.$queryRaw`${Prisma.raw(surveysFindQuery)}`;
logger.info(`Found ${surveysWithEmptyUrls.length.toString()} surveys with empty imageUrl or videoUrl`);
// Process in batches to avoid overwhelming the connection pool
const BATCH_SIZE = 1000;
for (let i = 0; i < surveysWithEmptyUrls.length; i += BATCH_SIZE) {
const batch = surveysWithEmptyUrls.slice(i, i + BATCH_SIZE);
const batchPromises = batch.map((survey) => {
// Clean the blocks
const cleanedBlocks = survey.blocks.map((block) => {
const cleanedElements = block.elements.map((element) => {
const cleanedElement = { ...element };
if (cleanedElement.imageUrl === "") {
delete cleanedElement.imageUrl;
}
if (cleanedElement.videoUrl === "") {
delete cleanedElement.videoUrl;
}
return cleanedElement;
});
return { ...block, elements: cleanedElements };
});
const cleanedWelcomeCard = { ...survey.welcomeCard };
if (cleanedWelcomeCard.fileUrl === "") {
delete cleanedWelcomeCard.fileUrl;
}
if (cleanedWelcomeCard.videoUrl === "") {
delete cleanedWelcomeCard.videoUrl;
}
const cleanedEndings = survey.endings.map((ending) => {
const cleanedEnding = { ...ending };
if (cleanedEnding.imageUrl === "") {
delete cleanedEnding.imageUrl;
}
if (cleanedEnding.videoUrl === "") {
delete cleanedEnding.videoUrl;
}
return cleanedEnding;
});
// Convert JSON arrays to PostgreSQL jsonb[] using array_agg + jsonb_array_elements
const blocksJson = JSON.stringify(cleanedBlocks);
const endingsJson = JSON.stringify(cleanedEndings);
const welcomeCardJson = JSON.stringify(cleanedWelcomeCard);
return tx.$executeRaw`
UPDATE "Survey"
SET
blocks = (SELECT array_agg(elem) FROM jsonb_array_elements(${blocksJson}::jsonb) AS elem),
endings = (SELECT array_agg(elem) FROM jsonb_array_elements(${endingsJson}::jsonb) AS elem),
"welcomeCard" = ${welcomeCardJson}::jsonb
WHERE id = ${survey.id}
`;
});
await Promise.all(batchPromises);
logger.info(
`Processed batch ${(Math.floor(i / BATCH_SIZE) + 1).toString()}/${Math.ceil(surveysWithEmptyUrls.length / BATCH_SIZE).toString()}`
);
}
logger.info(`Successfully cleaned ${surveysWithEmptyUrls.length.toString()} surveys`);
},
};

View File

@@ -0,0 +1,22 @@
export interface SurveyElement {
id: string;
imageUrl?: string;
videoUrl?: string;
}
export interface Block {
id: string;
elements: SurveyElement[];
}
export interface SurveyRecord {
id: string;
blocks: Block[];
welcomeCard: {
fileUrl?: string;
videoUrl?: string;
};
endings: {
imageUrl?: string;
videoUrl?: string;
}[];
}

View File

@@ -1,7 +1,7 @@
import { type ZodIssue, z } from "zod";
import { ZSurveyFollowUp } from "@formbricks/database/types/survey-follow-up";
import { ZActionClass, ZActionClassNoCodeConfig } from "../action-classes";
import { ZColor, ZId, ZPlacement, getZSafeUrl } from "../common";
import { ZColor, ZId, ZPlacement, ZUrl, getZSafeUrl } from "../common";
import { ZContactAttributes } from "../contact-attribute";
import { type TI18nString, ZI18nString } from "../i18n";
import { ZLanguage } from "../project";
@@ -60,16 +60,16 @@ export const ZSurveyEndScreenCard = ZSurveyEndingBase.extend({
headline: ZI18nString.optional(),
subheader: ZI18nString.optional(),
buttonLabel: ZI18nString.optional(),
buttonLink: z.string().optional(),
imageUrl: z.string().optional(),
videoUrl: z.string().optional(),
buttonLink: ZUrl.optional(),
imageUrl: ZUrl.optional(),
videoUrl: ZUrl.optional(),
});
export type TSurveyEndScreenCard = z.infer<typeof ZSurveyEndScreenCard>;
export const ZSurveyRedirectUrlCard = ZSurveyEndingBase.extend({
type: z.literal("redirectToUrl"),
url: z.string().optional(),
url: ZUrl.optional(),
label: z.string().optional(),
});
@@ -143,11 +143,11 @@ export const ZSurveyWelcomeCard = z
enabled: z.boolean(),
headline: ZI18nString.optional(),
subheader: ZI18nString.optional(),
fileUrl: z.string().optional(),
fileUrl: ZUrl.optional(),
buttonLabel: ZI18nString.optional(),
timeToFinish: z.boolean().default(true),
showResponseCount: z.boolean().default(false),
videoUrl: z.string().optional(),
videoUrl: ZUrl.optional(),
})
.refine((schema) => !(schema.enabled && !schema.headline), {
message: "Welcome card must have a headline",