fix: add data-migration fixing invalid jump end jump destination (#2972)

This commit is contained in:
Matti Nannt
2024-08-07 16:57:13 +02:00
committed by GitHub
parent 53fb976fb6
commit 32b3a7d1d0
40 changed files with 235 additions and 1077 deletions

2
.gitignore vendored
View File

@@ -56,5 +56,5 @@ Zone.Identifier
packages/lib/uploads
# Vite Timestamps
vite.config.*.timestamp-*
*vite.config.*.timestamp-*

View File

@@ -191,12 +191,12 @@ docker compose up -d
<CodeGroup title="Migrate the data">
```bash
docker pull ghcr.io/formbricks/data-migrations:latest && \
docker pull ghcr.io/formbricks/data-migrations:v2.3.0 && \
docker run --rm \
--network=formbricks_default \
-e DATABASE_URL="postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" \
-e UPGRADE_TO_VERSION="v2.3" \
ghcr.io/formbricks/data-migrations:latest
ghcr.io/formbricks/data-migrations:v2.3.0
```
</CodeGroup>
@@ -286,12 +286,12 @@ docker compose up -d
<CodeGroup title="Migrate the data">
```bash
docker pull ghcr.io/formbricks/data-migrations:latest && \
docker pull ghcr.io/formbricks/data-migrations:v2.2 && \
docker run --rm \
--network=formbricks_default \
-e DATABASE_URL="postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" \
-e UPGRADE_TO_VERSION="v2.2" \
ghcr.io/formbricks/data-migrations:latest
ghcr.io/formbricks/data-migrations:v2.2
```
</CodeGroup>
@@ -390,12 +390,12 @@ docker compose up -d
<CodeGroup title="Migrate the data">
```bash
docker pull ghcr.io/formbricks/data-migrations:latest && \
docker pull ghcr.io/formbricks/data-migrations:v2.1.0 && \
docker run --rm \
--network=formbricks_default \
-e DATABASE_URL="postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" \
-e UPGRADE_TO_VERSION="v2.1" \
ghcr.io/formbricks/data-migrations:latest
ghcr.io/formbricks/data-migrations:v2.1.0
```
</CodeGroup>
@@ -504,12 +504,12 @@ docker compose up -d
<CodeGroup title="Migrate the data">
```bash
docker pull ghcr.io/formbricks/data-migrations:latest && \
docker pull ghcr.io/formbricks/data-migrations:v2.0.3 && \
docker run --rm \
--network=formbricks_default \
-e DATABASE_URL="postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" \
-e UPGRADE_TO_VERSION="v2.0" \
ghcr.io/formbricks/data-migrations:latest
ghcr.io/formbricks/data-migrations:v2.0.3
```
</CodeGroup>

View File

@@ -82,7 +82,6 @@ export const ConnectWithFormbricks = ({
</div>
<Button
id="finishOnboarding"
className="text-slate-400 hover:text-slate-700"
variant={widgetSetupCompleted ? "primary" : "minimal"}
onClick={handleFinishOnboarding}
EndIcon={ArrowRight}>

View File

@@ -14,7 +14,7 @@ import { createId } from "@paralleldrive/cuid2";
import React, { SetStateAction, useEffect, useMemo, useState } from "react";
import toast from "react-hot-toast";
import { MultiLanguageCard } from "@formbricks/ee/multi-language/components/multi-language-card";
import { extractLanguageCodes, getLocalizedValue, translateQuestion } from "@formbricks/lib/i18n/utils";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { structuredClone } from "@formbricks/lib/pollyfills/structuredClone";
import { getDefaultEndingCard } from "@formbricks/lib/templates";
import { checkForEmptyFallBackValue, extractRecallInfo } from "@formbricks/lib/utils/recall";
@@ -258,18 +258,16 @@ export const QuestionsView = ({
toast.success("Question duplicated.");
};
const addQuestion = (question: any, index?: number) => {
const addQuestion = (question: TSurveyQuestion, index?: number) => {
const updatedSurvey = { ...localSurvey };
if (backButtonLabel) {
question.backButtonLabel = backButtonLabel;
}
const languageSymbols = extractLanguageCodes(localSurvey.languages);
const translatedQuestion = translateQuestion(question, languageSymbols);
if (index) {
updatedSurvey.questions.splice(index, 0, { ...translatedQuestion, isDraft: true });
updatedSurvey.questions.splice(index, 0, { ...question, isDraft: true });
} else {
updatedSurvey.questions.push({ ...translatedQuestion, isDraft: true });
updatedSurvey.questions.push({ ...question, isDraft: true });
}
setLocalSurvey(updatedSurvey);

View File

@@ -21,11 +21,10 @@ import {
} from "@formbricks/lib/posthogServer";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
import { getSyncSurveys, transformToLegacySurvey } from "@formbricks/lib/survey/service";
import { getSyncSurveys } from "@formbricks/lib/survey/service";
import { transformToLegacySurvey } from "@formbricks/lib/survey/utils";
import { isVersionGreaterThanOrEqualTo } from "@formbricks/lib/utils/version";
import { TJsAppLegacyStateSync, TJsAppStateSync, ZJsPeopleUserIdInput } from "@formbricks/types/js";
import { TLegacySurvey } from "@formbricks/types/legacy-surveys";
import { TProductLegacy } from "@formbricks/types/product";
import { TJsAppStateSync, ZJsPeopleUserIdInput } from "@formbricks/types/js";
import { TSurvey } from "@formbricks/types/surveys/types";
export const OPTIONS = async (): Promise<Response> => {
@@ -181,7 +180,7 @@ export const GET = async (
throw new Error("Product not found");
}
const updatedProduct: TProductLegacy = {
const updatedProduct: any = {
...product,
brandColor: product.styling.brandColor?.light ?? COLOR_DEFAULTS.brandColor,
...(product.styling.highlightBorderColor?.light && {
@@ -194,10 +193,10 @@ export const GET = async (
// Scenario 1: Multi language and updated trigger action classes supported.
// Use the surveys as they are.
let transformedSurveys: TLegacySurvey[] | TSurvey[] = surveys;
let transformedSurveys: TSurvey[] = surveys;
// creating state object
let state: TJsAppStateSync | TJsAppLegacyStateSync = {
let state: TJsAppStateSync = {
surveys: !isMonthlyResponsesLimitReached
? transformedSurveys.map((survey) => replaceAttributeRecall(survey, attributes))
: [],
@@ -212,13 +211,13 @@ export const GET = async (
// Convert to legacy surveys with default language
// convert triggers to array of actionClasses Names
transformedSurveys = await Promise.all(
surveys.map((survey: TSurvey | TLegacySurvey) => {
surveys.map((survey) => {
const languageCode = "default";
return transformToLegacySurvey(survey as TSurvey, languageCode);
})
);
state = {
const legacyState: any = {
surveys: !isMonthlyResponsesLimitReached
? transformedSurveys.map((survey) => replaceAttributeRecallInLegacySurveys(survey, attributes))
: [],
@@ -227,6 +226,7 @@ export const GET = async (
language,
product: updatedProduct,
};
return responses.successResponse({ ...legacyState }, true);
}
return responses.successResponse({ ...state }, true);

View File

@@ -1,6 +1,5 @@
import { parseRecallInfo } from "@formbricks/lib/utils/recall";
import { TAttributes } from "@formbricks/types/attributes";
import { TLegacySurvey } from "@formbricks/types/legacy-surveys";
import { TSurvey } from "@formbricks/types/surveys/types";
export const replaceAttributeRecall = (survey: TSurvey, attributes: TAttributes): TSurvey => {
@@ -42,10 +41,7 @@ export const replaceAttributeRecall = (survey: TSurvey, attributes: TAttributes)
return surveyTemp;
};
export const replaceAttributeRecallInLegacySurveys = (
survey: TLegacySurvey,
attributes: TAttributes
): TLegacySurvey => {
export const replaceAttributeRecallInLegacySurveys = (survey: any, attributes: TAttributes): any => {
const surveyTemp = structuredClone(survey);
surveyTemp.questions.forEach((question) => {
if (question.headline.includes("recall:")) {

View File

@@ -14,11 +14,10 @@ import {
} from "@formbricks/lib/posthogServer";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
import { getSurveys, transformToLegacySurvey } from "@formbricks/lib/survey/service";
import { getSurveys } from "@formbricks/lib/survey/service";
import { transformToLegacySurvey } from "@formbricks/lib/survey/utils";
import { isVersionGreaterThanOrEqualTo } from "@formbricks/lib/utils/version";
import { TJsWebsiteLegacyStateSync, TJsWebsiteStateSync, ZJsWebsiteSyncInput } from "@formbricks/types/js";
import { TLegacySurvey } from "@formbricks/types/legacy-surveys";
import { TProductLegacy } from "@formbricks/types/product";
import { TJsWebsiteStateSync, ZJsWebsiteSyncInput } from "@formbricks/types/js";
import { TSurvey } from "@formbricks/types/surveys/types";
export const OPTIONS = async (): Promise<Response> => {
@@ -122,7 +121,7 @@ export const GET = async (
// && (!survey.segment || survey.segment.filters.length === 0)
);
const updatedProduct: TProductLegacy = {
const updatedProduct: any = {
...product,
brandColor: product.styling.brandColor?.light ?? COLOR_DEFAULTS.brandColor,
...(product.styling.highlightBorderColor?.light && {
@@ -133,8 +132,8 @@ export const GET = async (
const noCodeActionClasses = actionClasses.filter((actionClass) => actionClass.type === "noCode");
// Define 'transformedSurveys' which can be an array of either TLegacySurvey or TSurvey.
let transformedSurveys: TLegacySurvey[] | TSurvey[] = filteredSurveys;
let state: TJsWebsiteStateSync | TJsWebsiteLegacyStateSync = {
let transformedSurveys: TSurvey[] = filteredSurveys;
let state: TJsWebsiteStateSync = {
surveys: !isWebsiteSurveyResponseLimitReached ? transformedSurveys : [],
actionClasses,
product: updatedProduct,
@@ -152,11 +151,16 @@ export const GET = async (
})
);
state = {
const legacyState: any = {
surveys: isWebsiteSurveyResponseLimitReached ? [] : transformedSurveys,
noCodeActionClasses,
product: updatedProduct,
};
return responses.successResponse(
{ ...legacyState },
true,
"public, s-maxage=600, max-age=840, stale-while-revalidate=600, stale-if-error=600"
);
}
return responses.successResponse(

View File

@@ -1,7 +1,6 @@
import { authenticateRequest } from "@/app/api/v1/auth";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { translateSurvey } from "@formbricks/lib/i18n/utils";
import { createSurvey, getSurveys } from "@formbricks/lib/survey/service";
import { DatabaseError } from "@formbricks/types/errors";
import { ZSurveyCreateInput } from "@formbricks/types/surveys/types";
@@ -38,13 +37,6 @@ export const POST = async (request: Request): Promise<Response> => {
return responses.badRequestResponse("Malformed JSON input, please check your request body");
}
if (surveyInput?.questions && surveyInput.questions[0].headline) {
const questionHeadline = surveyInput.questions[0].headline;
if (typeof questionHeadline === "string") {
// its a legacy survey
surveyInput = translateSurvey(surveyInput, []);
}
}
const inputValidation = ZSurveyCreateInput.safeParse(surveyInput);
if (!inputValidation.success) {

View File

@@ -1,3 +1,4 @@
/* eslint-disable -- leacy support workaround for now to avoid rewrite after eslint rules have been changed */
// migration script to translate surveys where thankYouCard buttonLabel is a string or question subheaders are strings
import { PrismaClient } from "@prisma/client";
import { hasStringSubheaders, translateSurvey } from "./lib/i18n";
@@ -8,7 +9,7 @@ const main = async () => {
await prisma.$transaction(
async (tx) => {
// Translate Surveys
const surveys = await tx.survey.findMany({
const surveys: any = await tx.survey.findMany({
select: {
id: true,
questions: true,
@@ -17,11 +18,6 @@ const main = async () => {
},
});
if (!surveys) {
// stop the migration if there are no surveys
return;
}
for (const survey of surveys) {
if (typeof survey.thankYouCard.buttonLabel === "string" || hasStringSubheaders(survey.questions)) {
const translatedSurvey = translateSurvey(survey, []);

View File

@@ -1,3 +1,4 @@
/* eslint-disable -- leacy support workaround for now to avoid rewrite after eslint rules have been changed */
// migration script to convert range field in rating question from string to number
import { PrismaClient } from "@prisma/client";

View File

@@ -1,3 +1,4 @@
/* eslint-disable -- leacy support workaround for now to avoid rewrite after eslint rules have been changed */
// migration script to add empty strings to welcome card headline in default language, if it does not exist
// WelcomeCard.headline = {} -> WelcomeCard.headline = {"default":""}
import { PrismaClient } from "@prisma/client";

View File

@@ -1,3 +1,4 @@
/* eslint-disable -- leacy support workaround for now to avoid rewrite after eslint rules have been changed */
import { PrismaClient } from "@prisma/client";
import { AttributeType } from "@prisma/client";
import { translateSurvey } from "./lib/i18n";

View File

@@ -1,24 +1,17 @@
/* eslint-disable -- leacy support workaround for now to avoid rewrite after eslint rules have been changed */
import { type TLanguage } from "@formbricks/types/product";
import {
TLegacySurveyChoice,
TLegacySurveyQuestion,
TLegacySurveyThankYouCard,
TLegacySurveyWelcomeCard,
} from "@formbricks/types/legacy-surveys";
import { TLanguage } from "@formbricks/types/product";
import {
TI18nString,
TSurvey,
TSurveyCTAQuestion,
TSurveyChoice,
TSurveyConsentQuestion,
TSurveyMultipleChoiceQuestion,
TSurveyNPSQuestion,
TSurveyOpenTextQuestion,
TSurveyQuestion,
TSurveyQuestions,
TSurveyRatingQuestion,
TSurveyThankYouCard,
TSurveyWelcomeCard,
type TI18nString,
type TSurveyCTAQuestion,
type TSurveyChoice,
type TSurveyConsentQuestion,
type TSurveyMultipleChoiceQuestion,
type TSurveyNPSQuestion,
type TSurveyOpenTextQuestion,
type TSurveyQuestion,
type TSurveyQuestions,
type TSurveyRatingQuestion,
type TSurveyWelcomeCard,
ZSurveyCTAQuestion,
ZSurveyCalQuestion,
ZSurveyConsentQuestion,
@@ -29,7 +22,6 @@ import {
ZSurveyPictureSelectionQuestion,
ZSurveyQuestion,
ZSurveyRatingQuestion,
ZSurveyThankYouCard,
ZSurveyWelcomeCard,
} from "@formbricks/types/surveys/types";
@@ -71,7 +63,7 @@ export const createI18nString = (text: string | TI18nString, languages: string[]
};
// Function to translate a choice label
const translateChoice = (choice: TSurveyChoice | TLegacySurveyChoice, languages: string[]): TSurveyChoice => {
const translateChoice = (choice: TSurveyChoice, languages: string[]): TSurveyChoice => {
if (typeof choice.label !== "undefined") {
return {
...choice,
@@ -86,7 +78,7 @@ const translateChoice = (choice: TSurveyChoice | TLegacySurveyChoice, languages:
};
export const translateWelcomeCard = (
welcomeCard: TSurveyWelcomeCard | TLegacySurveyWelcomeCard,
welcomeCard: TSurveyWelcomeCard,
languages: string[]
): TSurveyWelcomeCard => {
const clonedWelcomeCard = structuredClone(welcomeCard);
@@ -103,10 +95,7 @@ export const translateWelcomeCard = (
return ZSurveyWelcomeCard.parse(clonedWelcomeCard);
};
const translateThankYouCard = (
thankYouCard: TSurveyThankYouCard | TLegacySurveyThankYouCard,
languages: string[]
): TSurveyThankYouCard => {
const translateThankYouCard = (thankYouCard: any, languages: string[]): any => {
const clonedThankYouCard = structuredClone(thankYouCard);
if (typeof thankYouCard.headline !== "undefined") {
@@ -120,14 +109,11 @@ const translateThankYouCard = (
if (typeof clonedThankYouCard.buttonLabel !== "undefined") {
clonedThankYouCard.buttonLabel = createI18nString(thankYouCard.buttonLabel ?? "", languages);
}
return ZSurveyThankYouCard.parse(clonedThankYouCard);
return clonedThankYouCard;
};
// Function that will translate a single question
const translateQuestion = (
question: TLegacySurveyQuestion | TSurveyQuestion,
languages: string[]
): TSurveyQuestion => {
const translateQuestion = (question: TSurveyQuestion, languages: string[]): TSurveyQuestion => {
// Clone the question to avoid mutating the original
const clonedQuestion = structuredClone(question);
@@ -250,12 +236,9 @@ export const extractLanguageIds = (languages: TLanguage[]): string[] => {
return languages.map((language) => language.id);
};
// Function to translate an entire survey
export const translateSurvey = (
survey: Pick<TSurvey, "questions" | "welcomeCard" | "thankYouCard">,
languageCodes: string[]
): Pick<TSurvey, "questions" | "welcomeCard" | "thankYouCard"> => {
const translatedQuestions = survey.questions.map((question) => {
// Function to translate an entire survey (from old survey format to new survey format)
export const translateSurvey = (survey: any, languageCodes: string[]) => {
const translatedQuestions = survey.questions.map((question: any) => {
return translateQuestion(question, languageCodes);
});
const translatedWelcomeCard = translateWelcomeCard(survey.welcomeCard, languageCodes);

View File

@@ -0,0 +1,111 @@
/* eslint-disable no-console -- logging is allowed in migration scripts */
import { PrismaClient } from "@prisma/client";
import { type TSurveyEnding, type TSurveyQuestion } from "@formbricks/types/surveys/types";
const prisma = new PrismaClient();
async function runMigration(): Promise<void> {
await prisma.$transaction(
async (tx) => {
const startTime = Date.now();
console.log("Starting data migration...");
// Fetch all surveys
const surveys: { id: string; questions: TSurveyQuestion[]; endings: TSurveyEnding[] }[] =
await tx.survey.findMany({
select: {
id: true,
questions: true,
endings: true,
},
});
if (surveys.length === 0) {
// Stop the migration if there are no surveys
console.log("No Surveys found");
return;
}
// Get all surveys that have a logic rule that has "end" as the destination
const surveysWithEndDestination = surveys.filter((survey) =>
survey.questions.some((question) => question.logic?.some((rule) => rule.destination === "end"))
);
console.log(`Total surveys to update found: ${surveysWithEndDestination.length.toString()}`);
let transformedSurveyCount = 0;
const updatePromises = surveysWithEndDestination.map((survey) => {
const updatedSurvey = structuredClone(survey);
// Remove logic rule if there are no endings
if (updatedSurvey.endings.length === 0) {
// remove logic rule if there are no endings
updatedSurvey.questions.forEach((question) => {
if (typeof question.logic === "undefined") {
return;
}
question.logic.forEach((rule, index) => {
if (rule.destination === "end") {
if (question.logic) question.logic.splice(index, 1);
}
});
});
}
// get id of first ending
const firstEnding = survey.endings[0];
// replace logic destination with ending id
updatedSurvey.questions.forEach((question) => {
if (typeof question.logic === "undefined") {
return;
}
question.logic.forEach((rule) => {
if (rule.destination === "end") {
rule.destination = firstEnding.id;
}
});
});
transformedSurveyCount++;
// Return the update promise
return tx.survey.update({
where: { id: survey.id },
data: {
questions: updatedSurvey.questions,
},
});
});
await Promise.all(updatePromises);
console.log(`${transformedSurveyCount.toString()} surveys transformed`);
const endTime = Date.now();
console.log(`Data migration completed. Total time: ${((endTime - startTime) / 1000).toString()}s`);
},
{
timeout: 180000, // 3 minutes
}
);
}
function handleError(error: unknown): void {
console.error("An error occurred during migration:", error);
process.exit(1);
}
function handleDisconnectError(): void {
console.error("Failed to disconnect Prisma client");
process.exit(1);
}
function main(): void {
runMigration()
.catch(handleError)
.finally(() => {
prisma.$disconnect().catch(handleDisconnectError);
});
}
main();

View File

@@ -11,7 +11,7 @@ import {
import { type TBaseFilters } from "@formbricks/types/segment";
import {
type TSurveyClosedMessage,
type TSurveyEndings,
type TSurveyEnding,
type TSurveyHiddenFields,
type TSurveyProductOverwrites,
type TSurveyQuestions,
@@ -32,7 +32,7 @@ declare global {
export type ResponsePersonAttributes = TResponsePersonAttributes;
export type SurveyWelcomeCard = TSurveyWelcomeCard;
export type SurveyQuestions = TSurveyQuestions;
export type SurveyEndings = TSurveyEndings;
export type SurveyEnding = TSurveyEnding;
export type SurveyHiddenFields = TSurveyHiddenFields;
export type SurveyProductOverwrites = TSurveyProductOverwrites;
export type SurveyStyling = TSurveyStyling;

View File

@@ -44,7 +44,8 @@
"data-migration:segments-cleanup": "ts-node ./data-migrations/20240712123456_segments_cleanup/data-migration.ts",
"data-migration:multiple-endings": "ts-node ./data-migrations/20240801120500_thankYouCard_to_endings/data-migration.ts",
"data-migration:simplified-email-verification": "ts-node ./data-migrations/20240726124100_replace_verifyEmail_with_isVerifyEmailEnabled/data-migration.ts",
"data-migration:v2.4": "pnpm data-migration:segments-cleanup && pnpm data-migration:multiple-endings && pnpm data-migration:simplified-email-verification"
"data-migration:fix-logic-end-destination": "ts-node ./data-migrations/20240806120500_fix-logic-end-destination/data-migration.ts",
"data-migration:v2.4": "pnpm data-migration:segments-cleanup && pnpm data-migration:multiple-endings && pnpm data-migration:simplified-email-verification && pnpm data-migration:fix-logic-end-destination"
},
"dependencies": {
"@prisma/client": "^5.17.0",

View File

@@ -271,6 +271,8 @@ model Survey {
/// @zod.custom(imports.ZSurveyQuestions)
/// [SurveyQuestions]
questions Json @default("[]")
/// @zod.custom(imports.ZSurveyEnding)
/// [SurveyEnding]
endings Json[] @default([])
thankYouCard Json? //deprecated
/// @zod.custom(imports.ZSurveyHiddenFields)

View File

@@ -20,6 +20,7 @@ export {
ZSurveyStyling,
ZSurveySingleUse,
ZSurveyInlineTriggers,
ZSurveyEnding,
} from "@formbricks/types/surveys/types";
export { ZSegmentFilters } from "@formbricks/types/segment";

View File

@@ -6,7 +6,6 @@ import Link from "next/link";
import type { FC } from "react";
import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { extractLanguageCodes, translateSurvey } from "@formbricks/lib/i18n/utils";
import type { TLanguage, TProduct } from "@formbricks/types/product";
import type { TSurvey, TSurveyLanguage } from "@formbricks/types/surveys/types";
import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
@@ -74,9 +73,7 @@ export const MultiLanguageCard: FC<MultiLanguageCardProps> = ({
};
const updateSurveyTranslations = (survey: TSurvey, updatedLanguages: TSurveyLanguage[]) => {
const translatedSurveyResult = translateSurvey(survey, extractLanguageCodes(updatedLanguages));
const updatedSurvey = { ...translatedSurveyResult, languages: updatedLanguages };
const updatedSurvey = { ...survey, languages: updatedLanguages };
setLocalSurvey(updatedSurvey as TSurvey);
};

View File

@@ -7,12 +7,8 @@ import {
TDisplay,
TDisplayCreateInput,
TDisplayFilters,
TDisplayLegacyCreateInput,
TDisplayLegacyUpdateInput,
TDisplayUpdateInput,
ZDisplayCreateInput,
ZDisplayLegacyCreateInput,
ZDisplayLegacyUpdateInput,
ZDisplayUpdateInput,
} from "@formbricks/types/displays";
import { ZId } from "@formbricks/types/environment";
@@ -115,47 +111,6 @@ export const updateDisplay = async (
}
};
export const updateDisplayLegacy = async (
displayId: string,
displayInput: TDisplayLegacyUpdateInput
): Promise<TDisplay> => {
validateInputs([displayInput, ZDisplayLegacyUpdateInput]);
try {
const data = {
...(displayInput.personId && {
person: {
connect: {
id: displayInput.personId,
},
},
}),
...(displayInput.responseId && {
responseId: displayInput.responseId,
}),
};
const display = await prisma.display.update({
where: {
id: displayId,
},
data,
select: selectDisplay,
});
displayCache.revalidate({
id: display.id,
surveyId: display.surveyId,
});
return display;
} catch (error) {
console.error(error);
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};
export const createDisplay = async (displayInput: TDisplayCreateInput): Promise<TDisplay> => {
validateInputs([displayInput, ZDisplayCreateInput]);
@@ -201,80 +156,6 @@ export const createDisplay = async (displayInput: TDisplayCreateInput): Promise<
}
};
export const createDisplayLegacy = async (displayInput: TDisplayLegacyCreateInput): Promise<TDisplay> => {
validateInputs([displayInput, ZDisplayLegacyCreateInput]);
try {
const display = await prisma.display.create({
data: {
survey: {
connect: {
id: displayInput.surveyId,
},
},
...(displayInput.personId && {
person: {
connect: {
id: displayInput.personId,
},
},
}),
},
select: selectDisplay,
});
displayCache.revalidate({
id: display.id,
personId: display.personId,
surveyId: display.surveyId,
});
return display;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};
export const markDisplayRespondedLegacy = async (displayId: string): Promise<TDisplay> => {
validateInputs([displayId, ZId]);
try {
if (!displayId) throw new Error("Display ID is required");
const display = await prisma.display.update({
where: {
id: displayId,
},
data: {
status: "responded",
},
select: selectDisplay,
});
if (!display) {
throw new ResourceNotFoundError("Display", displayId);
}
displayCache.revalidate({
id: display.id,
personId: display.personId,
surveyId: display.surveyId,
});
return display;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};
export const getDisplaysByPersonId = reactCache(
async (personId: string, page?: number): Promise<TDisplay[]> =>
cache(

View File

@@ -45,29 +45,8 @@ export const mockDisplayInputWithResponseId = {
responseId: mockResponseId,
};
export const mockDisplayLegacyInput = {
responseId: mockResponseId,
surveyId: mockSurveyId,
};
export const mockDisplayLegacyInputWithPersonId = {
...mockDisplayLegacyInput,
personId: mockPersonId,
};
export const mockDisplayUpdate = {
environmentId: mockEnvironmentId,
userId: mockUserId,
responseId: mockResponseId,
};
export const mockDisplayLegacyUpdateInput = {
personId: mockPersonId,
responseId: mockResponseId,
};
export const mockDisplayLegacyWithRespondedStatus: Prisma.DisplayGetPayload<{
select: typeof selectDisplay;
}> = {
...mockDisplayWithPersonId,
status: "responded",
};

View File

@@ -4,10 +4,6 @@ import {
mockDisplay,
mockDisplayInput,
mockDisplayInputWithUserId,
mockDisplayLegacyInput,
mockDisplayLegacyInputWithPersonId,
mockDisplayLegacyUpdateInput,
mockDisplayLegacyWithRespondedStatus,
mockDisplayUpdate,
mockDisplayWithPersonId,
mockDisplayWithResponseId,
@@ -20,14 +16,11 @@ import { testInputValidation } from "vitestSetup";
import { DatabaseError } from "@formbricks/types/errors";
import {
createDisplay,
createDisplayLegacy,
deleteDisplayByResponseId,
getDisplay,
getDisplayCountBySurveyId,
getDisplaysByPersonId,
markDisplayRespondedLegacy,
updateDisplay,
updateDisplayLegacy,
} from "../service";
beforeEach(() => {
@@ -173,88 +166,6 @@ describe("Tests for updateDisplay Service", () => {
});
});
describe("Tests for createDisplayLegacy service", () => {
describe("Happy Path", () => {
it("Creates a display when a person ID exist", async () => {
prisma.display.create.mockResolvedValue(mockDisplayWithPersonId);
const display = await createDisplayLegacy(mockDisplayLegacyInputWithPersonId);
expect(display).toEqual(mockDisplayWithPersonId);
});
it("Creates a display when a person ID does not exist", async () => {
prisma.display.create.mockResolvedValue(mockDisplay);
const display = await createDisplayLegacy(mockDisplayLegacyInput);
expect(display).toEqual(mockDisplay);
});
});
describe("Sad Path", () => {
testInputValidation(createDisplayLegacy, "123");
it("Throws DatabaseError on PrismaClientKnownRequestError occurrence", async () => {
const mockErrorMessage = "Mock error message";
const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, {
code: "P2002",
clientVersion: "0.0.1",
});
prisma.display.create.mockRejectedValue(errToThrow);
await expect(createDisplayLegacy(mockDisplayLegacyInputWithPersonId)).rejects.toThrow(DatabaseError);
});
it("Throws a generic Error for other exceptions", async () => {
const mockErrorMessage = "Mock error message";
prisma.display.create.mockRejectedValue(new Error(mockErrorMessage));
await expect(createDisplayLegacy(mockDisplayLegacyInputWithPersonId)).rejects.toThrow(Error);
});
});
});
describe("Tests for updateDisplayLegacy Service", () => {
describe("Happy Path", () => {
it("Updates a display", async () => {
prisma.display.update.mockResolvedValue(mockDisplayWithPersonId);
const display = await updateDisplayLegacy(mockDisplay.id, mockDisplayLegacyUpdateInput);
expect(display).toEqual(mockDisplayWithPersonId);
});
it("marks display as responded legacy", async () => {
prisma.display.update.mockResolvedValue(mockDisplayLegacyWithRespondedStatus);
const display = await markDisplayRespondedLegacy(mockDisplay.id);
expect(display).toEqual(mockDisplayLegacyWithRespondedStatus);
});
});
describe("Sad Path", () => {
testInputValidation(updateDisplayLegacy, "123", "123");
it("Throws DatabaseError on PrismaClientKnownRequestError", async () => {
const mockErrorMessage = "Mock error message";
const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, {
code: "P2002",
clientVersion: "0.0.1",
});
prisma.display.update.mockRejectedValue(errToThrow);
await expect(updateDisplayLegacy(mockDisplay.id, mockDisplayLegacyUpdateInput)).rejects.toThrow(
DatabaseError
);
});
it("Throws a generic Error for other unexpected issues", async () => {
const mockErrorMessage = "Mock error message";
prisma.display.update.mockRejectedValue(new Error(mockErrorMessage));
await expect(updateDisplayLegacy(mockDisplay.id, mockDisplayLegacyUpdateInput)).rejects.toThrow(Error);
});
});
});
describe("Tests for deleteDisplayByResponseId service", () => {
describe("Happy Path", () => {
it("Deletes a display when a response associated to it is deleted", async () => {
@@ -265,8 +176,6 @@ describe("Tests for deleteDisplayByResponseId service", () => {
});
});
describe("Sad Path", () => {
testInputValidation(createDisplayLegacy, "123");
it("Throws DatabaseError on PrismaClientKnownRequestError occurrence", async () => {
const mockErrorMessage = "Mock error message";
const errToThrow = new Prisma.PrismaClientKnownRequestError(mockErrorMessage, {

View File

@@ -1,13 +1,12 @@
import { mockSegment } from "segment/tests/__mocks__/segment.mock";
import { mockSurveyLanguages } from "survey/tests/__mock__/survey.mock";
import { TLegacySurveyThankYouCard } from "@formbricks/types/legacySurveys";
import {
TSurvey,
TSurveyCTAQuestion,
TSurveyCalQuestion,
TSurveyConsentQuestion,
TSurveyDateQuestion,
TSurveyEndings,
TSurveyEndScreenCard,
TSurveyFileUploadQuestion,
TSurveyMultipleChoiceQuestion,
TSurveyNPSQuestion,
@@ -139,6 +138,7 @@ export const mockRatingQuestion: TSurveyRatingQuestion = {
subheader: {
default: "Don't worry, be honest.",
},
isColorCodingEnabled: false,
scale: "star",
range: 5,
lowerLabel: {
@@ -166,6 +166,7 @@ export const mockNpsQuestion: TSurveyNPSQuestion = {
id: "m9pemgdih2p4exvkmeeqq6jf",
type: TSurveyQuestionTypeEnum.NPS,
isDraft: true,
isColorCodingEnabled: false,
};
export const mockCtaQuestion: TSurveyCTAQuestion = {
@@ -235,10 +236,10 @@ export const mockCalQuestion: TSurveyCalQuestion = {
isDraft: true,
};
export const mockEndings: TSurveyEndings = [
export const mockEndings = [
{
type: "endScreen",
id: "umyknohldc7w26ocjdhaa62c",
type: "endScreen",
headline: {
default: "Thank you!",
},
@@ -247,7 +248,7 @@ export const mockEndings: TSurveyEndings = [
},
buttonLink: "https://formbricks.com/signup",
buttonLabel: { default: "Create your own Survey" },
},
} as TSurveyEndScreenCard,
];
export const mockSurvey: TSurvey = {
@@ -506,7 +507,7 @@ export const mockTranslatedEndings = [
},
];
export const mockLegacyThankYouCard: TLegacySurveyThankYouCard = {
export const mockLegacyThankYouCard = {
buttonLink: "https://formbricks.com/signup",
enabled: true,
headline: "Thank you!",
@@ -553,4 +554,5 @@ export const mockLegacySurvey = {
welcomeCard: mockLegacyWelcomeCard,
thankYouCard: mockLegacyThankYouCard,
endings: undefined,
redirectUrl: null,
};

View File

@@ -1,15 +1,7 @@
import { describe, expect, it } from "vitest";
import {
mockEndings,
mockLegacySurvey,
mockSurvey,
mockTranslatedEndings,
mockTranslatedSurvey,
mockTranslatedWelcomeCard,
mockWelcomeCard,
} from "./i18n.mock";
import { mockLegacySurvey, mockTranslatedSurvey } from "./i18n.mock";
import { reverseTranslateSurvey } from "./reverseTranslation";
import { createI18nString, translateEndings, translateSurvey, translateWelcomeCard } from "./utils";
import { createI18nString } from "./utils";
describe("createI18nString", () => {
it("should create an i18n string from a regular string", () => {
@@ -43,30 +35,6 @@ describe("createI18nString", () => {
});
});
describe("translateWelcomeCard", () => {
it("should translate all text fields of a welcome card", () => {
const languages = ["default", "de"];
const translatedWelcomeCard = translateWelcomeCard(mockWelcomeCard, languages);
expect(translatedWelcomeCard).toEqual(mockTranslatedWelcomeCard);
});
});
describe("translateEndings", () => {
it("should translate all text fields of first endingCard", () => {
const languages = ["default", "de"];
const translatedEndings = translateEndings(mockEndings, languages);
expect(translatedEndings).toEqual(mockTranslatedEndings);
});
});
describe("translateSurvey", () => {
it("should translate all questions of a Survey", () => {
const languageCodes = ["default", "de"];
const translatedSurvey = translateSurvey(mockSurvey, languageCodes);
expect(translatedSurvey).toEqual(mockTranslatedSurvey);
});
});
describe("translate to Legacy Survey", () => {
it("should translate all questions of a normal survey to Legacy Survey", () => {
const translatedSurvey = reverseTranslateSurvey(mockTranslatedSurvey, "default");

View File

@@ -1,5 +1,4 @@
import "server-only";
import { TLegacySurvey, TLegacySurveyThankYouCard, ZLegacySurvey } from "@formbricks/types/legacy-surveys";
import { TI18nString, TSurvey } from "@formbricks/types/surveys/types";
import { structuredClone } from "../pollyfills/structuredClone";
import { getLocalizedValue, isI18nObject } from "./utils";
@@ -28,7 +27,7 @@ const reverseTranslateObject = <T extends Record<string, any>>(obj: T, languageC
return clonedObj;
};
const reverseTranslateEndings = (survey: TSurvey, languageCode: string): TLegacySurveyThankYouCard => {
const reverseTranslateEndings = (survey: TSurvey, languageCode: string): any => {
const firstEndingCard = survey.endings[0];
if (firstEndingCard && firstEndingCard.type === "endScreen") {
return {
@@ -43,7 +42,7 @@ const reverseTranslateEndings = (survey: TSurvey, languageCode: string): TLegacy
}
};
export const reverseTranslateSurvey = (survey: TSurvey, languageCode: string = "default"): TLegacySurvey => {
export const reverseTranslateSurvey = (survey: TSurvey, languageCode: string = "default"): any => {
const reversedSurvey = structuredClone(survey);
reversedSurvey.questions = reversedSurvey.questions.map((question) =>
reverseTranslateObject(question, languageCode)
@@ -67,5 +66,5 @@ export const reverseTranslateSurvey = (survey: TSurvey, languageCode: string = "
}
// @ts-expect-error
reversedSurvey.endings = undefined;
return ZLegacySurvey.parse(reversedSurvey);
return reversedSurvey;
};

View File

@@ -1,38 +1,5 @@
import {
TLegacySurveyChoice,
TLegacySurveyQuestion,
TLegacySurveyThankYouCard,
TLegacySurveyWelcomeCard,
} from "@formbricks/types/legacy-surveys";
import { TLanguage } from "@formbricks/types/product";
import {
TI18nString,
TSurvey,
TSurveyCTAQuestion,
TSurveyChoice,
TSurveyConsentQuestion,
TSurveyEndScreenCard,
TSurveyEndings,
TSurveyLanguage,
TSurveyMultipleChoiceQuestion,
TSurveyNPSQuestion,
TSurveyOpenTextQuestion,
TSurveyQuestion,
TSurveyRatingQuestion,
TSurveyWelcomeCard,
ZSurveyCTAQuestion,
ZSurveyCalQuestion,
ZSurveyConsentQuestion,
ZSurveyEndScreenCard,
ZSurveyFileUploadQuestion,
ZSurveyMultipleChoiceQuestion,
ZSurveyNPSQuestion,
ZSurveyOpenTextQuestion,
ZSurveyPictureSelectionQuestion,
ZSurveyQuestion,
ZSurveyRatingQuestion,
ZSurveyWelcomeCard,
} from "@formbricks/types/surveys/types";
import { TI18nString, TSurveyLanguage } from "@formbricks/types/surveys/types";
import { structuredClone } from "../pollyfills/structuredClone";
// Helper function to create an i18nString from a regular string.
@@ -109,225 +76,10 @@ export const getEnabledLanguages = (surveyLanguages: TSurveyLanguage[]) => {
return surveyLanguages.filter((surveyLanguage) => surveyLanguage.enabled);
};
const translateChoice = (choice: TSurveyChoice | TLegacySurveyChoice, languages: string[]): TSurveyChoice => {
if (typeof choice.label !== "undefined") {
return {
...choice,
label: createI18nString(choice.label, languages),
};
} else {
return {
...choice,
label: choice.label,
};
}
};
// LGEGACY
// Helper function to maintain backwards compatibility for old survey objects before Multi Language
export const translateWelcomeCard = (
welcomeCard: TSurveyWelcomeCard | TLegacySurveyWelcomeCard,
languages: string[]
): TSurveyWelcomeCard => {
const clonedWelcomeCard = structuredClone(welcomeCard);
if (typeof welcomeCard.headline !== "undefined") {
clonedWelcomeCard.headline = createI18nString(welcomeCard.headline ?? "", languages);
}
if (typeof welcomeCard.html !== "undefined") {
clonedWelcomeCard.html = createI18nString(welcomeCard.html ?? "", languages);
}
if (typeof welcomeCard.buttonLabel !== "undefined") {
clonedWelcomeCard.buttonLabel = createI18nString(clonedWelcomeCard.buttonLabel ?? "", languages);
}
return ZSurveyWelcomeCard.parse(clonedWelcomeCard);
};
// LGEGACY
// Helper function to maintain backwards compatibility for old survey objects before Multi Language
export const translateEndings = (
endings: TSurveyEndings | TLegacySurveyThankYouCard,
languages: string[]
): TSurveyEndings => {
const isEndingsArray = Array.isArray(endings);
if (isEndingsArray) {
return endings.map((ending) => {
if (ending.type === "redirectToUrl") return ending;
else {
const clonedEndingCard = structuredClone(ending);
if (typeof ending.headline !== "undefined") {
clonedEndingCard.headline = createI18nString(ending.headline ?? "", languages);
}
if (typeof ending.subheader !== "undefined") {
clonedEndingCard.subheader = createI18nString(ending.subheader ?? "", languages);
}
if (typeof ending.buttonLabel !== "undefined") {
clonedEndingCard.buttonLabel = createI18nString(ending.buttonLabel ?? "", languages);
}
return ZSurveyEndScreenCard.parse(clonedEndingCard);
}
});
} else {
const clonedEndingCard = structuredClone(endings) as unknown as TSurveyEndScreenCard;
if (typeof clonedEndingCard.headline !== "undefined") {
clonedEndingCard.headline = createI18nString(clonedEndingCard.headline ?? "", languages);
}
if (typeof clonedEndingCard.subheader !== "undefined") {
clonedEndingCard.subheader = createI18nString(clonedEndingCard.subheader ?? "", languages);
}
if (typeof clonedEndingCard.buttonLabel !== "undefined") {
clonedEndingCard.buttonLabel = createI18nString(clonedEndingCard.buttonLabel ?? "", languages);
}
return [ZSurveyEndScreenCard.parse(clonedEndingCard)];
}
};
// LGEGACY
// Helper function to maintain backwards compatibility for old survey objects before Multi Language
export const translateQuestion = (
question: TLegacySurveyQuestion | TSurveyQuestion,
languages: string[]
): TSurveyQuestion => {
// Clone the question to avoid mutating the original
const clonedQuestion = structuredClone(question);
//common question properties
if (typeof question.headline !== "undefined") {
clonedQuestion.headline = createI18nString(question.headline ?? "", languages);
}
if (typeof question.subheader !== "undefined") {
clonedQuestion.subheader = createI18nString(question.subheader ?? "", languages);
}
if (typeof question.buttonLabel !== "undefined") {
clonedQuestion.buttonLabel = createI18nString(question.buttonLabel ?? "", languages);
}
if (typeof question.backButtonLabel !== "undefined") {
clonedQuestion.backButtonLabel = createI18nString(question.backButtonLabel ?? "", languages);
}
switch (question.type) {
case "openText":
if (typeof question.placeholder !== "undefined") {
(clonedQuestion as TSurveyOpenTextQuestion).placeholder = createI18nString(
question.placeholder ?? "",
languages
);
}
return ZSurveyOpenTextQuestion.parse(clonedQuestion);
case "multipleChoiceSingle":
case "multipleChoiceMulti":
(clonedQuestion as TSurveyMultipleChoiceQuestion).choices = question.choices.map((choice) => {
return translateChoice(choice, languages);
});
if (typeof (clonedQuestion as TSurveyMultipleChoiceQuestion).otherOptionPlaceholder !== "undefined") {
(clonedQuestion as TSurveyMultipleChoiceQuestion).otherOptionPlaceholder = createI18nString(
question.otherOptionPlaceholder ?? "",
languages
);
}
return ZSurveyMultipleChoiceQuestion.parse(clonedQuestion);
case "cta":
if (typeof question.dismissButtonLabel !== "undefined") {
(clonedQuestion as TSurveyCTAQuestion).dismissButtonLabel = createI18nString(
question.dismissButtonLabel ?? "",
languages
);
}
if (typeof question.html !== "undefined") {
(clonedQuestion as TSurveyCTAQuestion).html = createI18nString(question.html ?? "", languages);
}
return ZSurveyCTAQuestion.parse(clonedQuestion);
case "consent":
if (typeof question.html !== "undefined") {
(clonedQuestion as TSurveyConsentQuestion).html = createI18nString(question.html ?? "", languages);
}
if (typeof question.label !== "undefined") {
(clonedQuestion as TSurveyConsentQuestion).label = createI18nString(question.label ?? "", languages);
}
return ZSurveyConsentQuestion.parse(clonedQuestion);
case "nps":
if (typeof question.lowerLabel !== "undefined") {
(clonedQuestion as TSurveyNPSQuestion).lowerLabel = createI18nString(
question.lowerLabel ?? "",
languages
);
}
if (typeof question.upperLabel !== "undefined") {
(clonedQuestion as TSurveyNPSQuestion).upperLabel = createI18nString(
question.upperLabel ?? "",
languages
);
}
return ZSurveyNPSQuestion.parse(clonedQuestion);
case "rating":
if (typeof question.lowerLabel !== "undefined") {
(clonedQuestion as TSurveyRatingQuestion).lowerLabel = createI18nString(
question.lowerLabel ?? "",
languages
);
}
if (typeof question.upperLabel !== "undefined") {
(clonedQuestion as TSurveyRatingQuestion).upperLabel = createI18nString(
question.upperLabel ?? "",
languages
);
}
return ZSurveyRatingQuestion.parse(clonedQuestion);
case "fileUpload":
return ZSurveyFileUploadQuestion.parse(clonedQuestion);
case "pictureSelection":
return ZSurveyPictureSelectionQuestion.parse(clonedQuestion);
case "cal":
return ZSurveyCalQuestion.parse(clonedQuestion);
default:
return ZSurveyQuestion.parse(clonedQuestion);
}
};
export const extractLanguageIds = (languages: TLanguage[]): string[] => {
return languages.map((language) => language.code);
};
// LGEGACY
// Helper function to maintain backwards compatibility for old survey objects before Multi Language
export const translateSurvey = (
survey: Pick<TSurvey, "questions" | "welcomeCard" | "endings">,
languageCodes: string[]
): Pick<TSurvey, "questions" | "welcomeCard" | "endings"> => {
const translatedQuestions = survey.questions.map((question) => {
return translateQuestion(question, languageCodes);
});
const translatedWelcomeCard = translateWelcomeCard(survey.welcomeCard, languageCodes);
const translatedEndings = translateEndings(survey.endings, languageCodes);
const translatedSurvey = structuredClone(survey);
return {
...translatedSurvey,
questions: translatedQuestions,
welcomeCard: translatedWelcomeCard,
endings: translatedEndings,
};
};
export const getLanguageCode = (surveyLanguages: TSurveyLanguage[], languageCode: string | null) => {
if (!surveyLanguages?.length || !languageCode) return "default";
const language = surveyLanguages.find((surveyLanguage) => surveyLanguage.language.code === languageCode);

View File

@@ -11,11 +11,9 @@ import {
TResponse,
TResponseFilterCriteria,
TResponseInput,
TResponseLegacyInput,
TResponseUpdateInput,
ZResponseFilterCriteria,
ZResponseInput,
ZResponseLegacyInput,
ZResponseUpdateInput,
} from "@formbricks/types/responses";
import { TSurveySummary } from "@formbricks/types/surveys/types";
@@ -26,7 +24,7 @@ import { IS_FORMBRICKS_CLOUD, ITEMS_PER_PAGE, WEBAPP_URL } from "../constants";
import { displayCache } from "../display/cache";
import { deleteDisplayByResponseId, getDisplayCountBySurveyId } from "../display/service";
import { getMonthlyOrganizationResponseCount, getOrganizationByEnvironmentId } from "../organization/service";
import { createPerson, getPerson, getPersonByUserId } from "../person/service";
import { createPerson, getPersonByUserId } from "../person/service";
import { sendPlanLimitsReachedEventToPosthogWeekly } from "../posthogServer";
import { responseNoteCache } from "../responseNote/cache";
import { getResponseNotes } from "../responseNote/service";
@@ -307,82 +305,6 @@ export const createResponse = async (responseInput: TResponseInput): Promise<TRe
}
};
export const createResponseLegacy = async (responseInput: TResponseLegacyInput): Promise<TResponse> => {
validateInputs([responseInput, ZResponseLegacyInput]);
captureTelemetry("response created");
try {
let person: TPerson | null = null;
let attributes: TAttributes | null = null;
if (responseInput.personId) {
person = await getPerson(responseInput.personId);
}
const ttcTemp = responseInput.ttc ?? {};
const questionId = Object.keys(ttcTemp)[0];
const ttc =
responseInput.finished && responseInput.ttc
? {
...ttcTemp,
_total: ttcTemp[questionId], // Add _total property with the same value
}
: ttcTemp;
if (person?.id) {
attributes = await getAttributes(person?.id as string);
}
const responsePrisma = await prisma.response.create({
data: {
survey: {
connect: {
id: responseInput.surveyId,
},
},
finished: responseInput.finished,
data: responseInput.data,
ttc,
...(responseInput.personId && {
person: {
connect: {
id: responseInput.personId,
},
},
personAttributes: attributes,
}),
...(responseInput.meta && ({ meta: responseInput?.meta } as Prisma.JsonObject)),
singleUseId: responseInput.singleUseId,
language: responseInput.language,
},
select: responseSelection,
});
const response: TResponse = {
...responsePrisma,
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
};
responseCache.revalidate({
id: response.id,
personId: response.person?.id,
surveyId: response.surveyId,
});
responseNoteCache.revalidate({
responseId: response.id,
});
return response;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};
export const getResponse = reactCache(
(responseId: string): Promise<TResponse | null> =>
cache(

View File

@@ -55,6 +55,9 @@ export const mockResponseNote: ResponseNoteMock = {
export const mockPerson = {
id: mockPersonId,
userId: mockUserId,
createdAt: new Date(2000, 1, 1, 19),
updatedAt: new Date(2000, 1, 1, 19),
environmentId: mockEnvironmentId,
};
export const mockTags = [

View File

@@ -6,7 +6,6 @@ import {
mockEnvironmentId,
mockMeta,
mockPerson,
mockPersonId,
mockResponse,
mockResponseData,
mockResponseNote,
@@ -18,15 +17,10 @@ import {
mockUserId,
} from "./__mocks__/data.mock";
import { Prisma } from "@prisma/client";
import { beforeEach, describe, expect, it, vitest } from "vitest";
import { beforeEach, describe, expect, it } from "vitest";
import { testInputValidation } from "vitestSetup";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import {
TResponse,
TResponseFilterCriteria,
TResponseInput,
TResponseLegacyInput,
} from "@formbricks/types/responses";
import { TResponse, TResponseFilterCriteria, TResponseInput } from "@formbricks/types/responses";
import { TTag } from "@formbricks/types/tags";
import { selectPerson } from "../../person/service";
import {
@@ -36,7 +30,6 @@ import {
} from "../../survey/tests/__mock__/survey.mock";
import {
createResponse,
createResponseLegacy,
deleteResponse,
getResponse,
getResponseBySingleUseId,
@@ -84,16 +77,6 @@ const mockResponseInputWithUserId: TResponseInput = {
userId: mockUserId,
};
const createMockResponseLegacyInput = (personId?: string): TResponseLegacyInput => ({
finished: constantsForTests.boolean,
personId: personId ?? null,
surveyId: mockSurveyId,
meta: mockMeta,
singleUseId: mockSingleUseId,
ttc: {},
data: {},
});
beforeEach(() => {
// @ts-expect-error
prisma.response.create.mockImplementation(async (args) => {
@@ -277,20 +260,6 @@ describe("Tests for createResponse service", () => {
});
});
describe("Tests for createResponseLegacy service", () => {
describe("Happy Path", () => {
it("Creates a response linked to an existing user", async () => {
const response = await createResponseLegacy(createMockResponseLegacyInput(mockPersonId));
expect(response).toEqual(expectedResponseWithPerson);
});
it("Creates a legacy response without an associated user ID", async () => {
const response = await createResponseLegacy(createMockResponseLegacyInput());
expect(response).toEqual(expectedResponseWithoutPerson);
});
});
});
describe("Tests for getResponse service", () => {
describe("Happy Path", () => {
it("Retrieves a specific response by its ID", async () => {

View File

@@ -7,7 +7,6 @@ import { TActionClass } from "@formbricks/types/action-classes";
import { ZOptionalNumber } from "@formbricks/types/common";
import { TEnvironment, ZId } from "@formbricks/types/environment";
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
import { TLegacySurvey } from "@formbricks/types/legacy-surveys";
import { TPerson } from "@formbricks/types/people";
import { TProduct } from "@formbricks/types/product";
import { TSegment, ZSegmentFilters } from "@formbricks/types/segment";
@@ -28,7 +27,6 @@ import { ITEMS_PER_PAGE } from "../constants";
import { displayCache } from "../display/cache";
import { getDisplaysByPersonId } from "../display/service";
import { getEnvironment } from "../environment/service";
import { reverseTranslateSurvey } from "../i18n/reverseTranslation";
import { subscribeOrganizationMembersToSurveyResponses } from "../organization/service";
import { personCache } from "../person/cache";
import { getPerson } from "../person/service";
@@ -324,23 +322,6 @@ export const getSurveys = reactCache(
)()
);
export const transformToLegacySurvey = async (
survey: TSurvey,
languageCode?: string
): Promise<TLegacySurvey> => {
const targetLanguage = languageCode ?? "default";
// workaround to handle triggers for legacy surveys
// because we dont wanna do this in the `reverseTranslateSurvey` function
const surveyToTransform: any = {
...structuredClone(survey),
triggers: survey.triggers.map((trigger) => trigger.actionClass.name),
};
const transformedSurvey = reverseTranslateSurvey(surveyToTransform as TSurvey, targetLanguage);
return transformedSurvey;
};
export const getSurveyCount = reactCache(
(environmentId: string): Promise<number> =>
cache(
@@ -984,7 +965,7 @@ export const getSyncSurveys = reactCache(
options?: {
version?: string;
}
): Promise<TSurvey[] | TLegacySurvey[]> =>
): Promise<TSurvey[]> =>
cache(
async () => {
validateInputs([environmentId, ZId]);
@@ -1001,7 +982,7 @@ export const getSyncSurveys = reactCache(
throw new Error("Person not found");
}
let surveys: TSurvey[] | TLegacySurvey[] = await getSurveys(environmentId);
let surveys = await getSurveys(environmentId);
// filtered surveys for running and web
surveys = surveys.filter((survey) => survey.status === "inProgress" && survey.type === "app");

View File

@@ -1,8 +1,8 @@
import "server-only";
import { Prisma } from "@prisma/client";
import { TLegacySurvey } from "@formbricks/types/legacy-surveys";
import { TSegment } from "@formbricks/types/segment";
import { TSurvey, TSurveyFilterCriteria } from "@formbricks/types/surveys/types";
import { reverseTranslateSurvey } from "../i18n/reverseTranslation";
export const transformPrismaSurvey = (surveyPrisma: any): TSurvey => {
let segment: TSegment | null = null;
@@ -81,7 +81,7 @@ export const buildOrderByClause = (
return [{ [sortBy]: "desc" }];
};
export const anySurveyHasFilters = (surveys: TSurvey[] | TLegacySurvey[]): boolean => {
export const anySurveyHasFilters = (surveys: TSurvey[]): boolean => {
return surveys.some((survey) => {
if ("segment" in survey && survey.segment) {
return survey.segment.filters && survey.segment.filters.length > 0;
@@ -89,3 +89,17 @@ export const anySurveyHasFilters = (surveys: TSurvey[] | TLegacySurvey[]): boole
return false;
});
};
export const transformToLegacySurvey = async (survey: TSurvey, languageCode?: string): Promise<any> => {
const targetLanguage = languageCode ?? "default";
// workaround to handle triggers for legacy surveys
// because we dont wanna do this in the `reverseTranslateSurvey` function
const surveyToTransform: any = {
...structuredClone(survey),
triggers: survey.triggers.map((trigger) => trigger.actionClass.name),
};
const transformedSurvey = reverseTranslateSurvey(surveyToTransform as TSurvey, targetLanguage);
return transformedSurvey;
};

View File

@@ -18,13 +18,3 @@ export const ZActionInput = z.object({
});
export type TActionInput = z.infer<typeof ZActionInput>;
export const ZActionLegacyInput = z.object({
environmentId: z.string().cuid2(),
personId: z.string().optional(),
sessionId: z.string().optional(),
name: z.string(),
properties: z.record(z.string()),
});
export type TActionLegacyInput = z.infer<typeof ZActionLegacyInput>;

View File

@@ -21,14 +21,6 @@ export const ZDisplayCreateInput = z.object({
export type TDisplayCreateInput = z.infer<typeof ZDisplayCreateInput>;
export const ZDisplayLegacyCreateInput = z.object({
surveyId: z.string().cuid(),
personId: z.string().cuid().optional(),
responseId: z.string().cuid().optional(),
});
export type TDisplayLegacyCreateInput = z.infer<typeof ZDisplayLegacyCreateInput>;
export const ZDisplayUpdateInput = z.object({
environmentId: z.string().cuid(),
userId: z.string().optional(),
@@ -37,13 +29,6 @@ export const ZDisplayUpdateInput = z.object({
export type TDisplayUpdateInput = z.infer<typeof ZDisplayUpdateInput>;
export const ZDisplayLegacyUpdateInput = z.object({
personId: z.string().cuid().optional(),
responseId: z.string().cuid().optional(),
});
export type TDisplayLegacyUpdateInput = z.infer<typeof ZDisplayLegacyUpdateInput>;
export const ZDisplaysWithSurveyName = ZDisplay.extend({
surveyName: z.string(),
});

View File

@@ -1,8 +1,6 @@
import { z } from "zod";
import { ZActionClass } from "./action-classes";
import { ZAttributes } from "./attributes";
import { ZLegacySurvey } from "./legacy-surveys";
import { ZPerson } from "./people";
import { ZProduct } from "./product";
import { ZResponseHiddenFieldValue } from "./responses";
import { ZSurvey } from "./surveys/types";
@@ -34,7 +32,7 @@ export type TJSWebsiteStateDisplay = z.infer<typeof ZJSWebsiteStateDisplay>;
export const ZJsAppStateSync = z.object({
person: ZJsPerson.nullish(),
userId: z.string().optional(),
surveys: z.union([z.array(ZSurvey), z.array(ZLegacySurvey)]),
surveys: z.array(ZSurvey),
actionClasses: z.array(ZActionClass),
product: ZProduct,
language: z.string().optional(),
@@ -65,32 +63,6 @@ export const ZJsWebsiteState = z.object({
export type TJsWebsiteState = z.infer<typeof ZJsWebsiteState>;
export const ZJsAppLegacyStateSync = z.object({
person: ZJsPerson.nullish(),
userId: z.string().optional(),
surveys: z.union([z.array(ZSurvey), z.array(ZLegacySurvey)]),
noCodeActionClasses: z.array(ZActionClass),
product: ZProduct,
language: z.string().optional(),
});
export type TJsAppLegacyStateSync = z.infer<typeof ZJsAppLegacyStateSync>;
export const ZJsWebsiteLegacyStateSync = ZJsAppLegacyStateSync.omit({ person: true });
export type TJsWebsiteLegacyStateSync = z.infer<typeof ZJsWebsiteLegacyStateSync>;
export const ZJsLegacyState = z.object({
person: ZPerson.nullable().or(z.object({})),
session: z.object({}),
surveys: z.array(ZSurveyWithTriggers),
noCodeActionClasses: z.array(ZActionClass),
product: ZProduct,
displays: z.array(ZJSWebsiteStateDisplay).optional(),
});
export type TJsLegacyState = z.infer<typeof ZJsLegacyState>;
export const ZJsWebsiteSyncInput = z.object({
environmentId: z.string().cuid(),
version: z.string().optional(),
@@ -180,14 +152,6 @@ export const ZJsPeopleAttributeInput = z.object({
export type TJsPeopleAttributeInput = z.infer<typeof ZJsPeopleAttributeInput>;
export const ZJsPeopleLegacyAttributeInput = z.object({
environmentId: z.string().cuid(),
key: z.string(),
value: z.string(),
});
export type TJsPeopleLegacyAttributeInput = z.infer<typeof ZJsPeopleLegacyAttributeInput>;
export const ZJsActionInput = z.object({
environmentId: z.string().cuid(),
userId: z.string().optional(),

View File

@@ -1,196 +0,0 @@
import { z } from "zod";
import { ZAllowedFileExtension } from "./common";
import {
TSurveyQuestionTypeEnum,
ZSurvey,
ZSurveyCTALogic,
ZSurveyCalLogic,
ZSurveyConsentLogic,
ZSurveyEndings,
ZSurveyFileUploadLogic,
ZSurveyMultipleChoiceLogic,
ZSurveyNPSLogic,
ZSurveyOpenTextLogic,
ZSurveyOpenTextQuestionInputType,
ZSurveyPictureChoice,
ZSurveyPictureSelectionLogic,
ZSurveyQuestionBase,
ZSurveyRatingLogic,
} from "./surveys/types";
const ZLegacySurveyQuestionBase = ZSurveyQuestionBase.extend({
headline: z.string(),
subheader: z.string().optional(),
buttonLabel: z.string().optional(),
backButtonLabel: z.string().optional(),
});
export const ZLegacySurveyOpenTextQuestion = ZLegacySurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionTypeEnum.OpenText),
placeholder: z.string().optional(),
longAnswer: z.boolean().optional(),
logic: z.array(ZSurveyOpenTextLogic).optional(),
inputType: ZSurveyOpenTextQuestionInputType.optional().default("text"),
});
export type TLegacySurveyOpenTextQuestion = z.infer<typeof ZLegacySurveyOpenTextQuestion>;
export const ZLegacySurveyConsentQuestion = ZLegacySurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionTypeEnum.Consent),
html: z.string().optional(),
label: z.string(),
placeholder: z.string().optional(),
logic: z.array(ZSurveyConsentLogic).optional(),
});
export type TLegacySurveyConsentQuestion = z.infer<typeof ZLegacySurveyConsentQuestion>;
export const ZLegacySurveyChoice = z.object({
id: z.string(),
label: z.string(),
});
export type TLegacySurveyChoice = z.infer<typeof ZLegacySurveyChoice>;
export const ZLegacySurveyMultipleChoiceSingleQuestion = ZLegacySurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionTypeEnum.MultipleChoiceSingle),
choices: z.array(ZLegacySurveyChoice),
logic: z.array(ZSurveyMultipleChoiceLogic).optional(),
shuffleOption: z.enum(["none", "all", "exceptLast"]).optional(),
otherOptionPlaceholder: z.string().optional(),
});
export type TLegacySurveyMultipleChoiceSingleQuestion = z.infer<
typeof ZLegacySurveyMultipleChoiceSingleQuestion
>;
export const ZLegacySurveyMultipleChoiceMultiQuestion = ZLegacySurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionTypeEnum.MultipleChoiceMulti),
choices: z.array(ZLegacySurveyChoice),
logic: z.array(ZSurveyMultipleChoiceLogic).optional(),
shuffleOption: z.enum(["none", "all", "exceptLast"]).optional(),
otherOptionPlaceholder: z.string().optional(),
});
export type TLegacySurveyMultipleChoiceMultiQuestion = z.infer<
typeof ZLegacySurveyMultipleChoiceMultiQuestion
>;
export const ZLegacySurveyNPSQuestion = ZLegacySurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionTypeEnum.NPS),
lowerLabel: z.string(),
upperLabel: z.string(),
logic: z.array(ZSurveyNPSLogic).optional(),
});
export type TLegacySurveyNPSQuestion = z.infer<typeof ZLegacySurveyNPSQuestion>;
export const ZLegacySurveyCTAQuestion = ZLegacySurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionTypeEnum.CTA),
html: z.string().optional(),
buttonUrl: z.string().optional(),
buttonExternal: z.boolean(),
dismissButtonLabel: z.string().optional(),
logic: z.array(ZSurveyCTALogic).optional(),
});
export type TLegacySurveyCTAQuestion = z.infer<typeof ZLegacySurveyCTAQuestion>;
export const ZLegacySurveyRatingQuestion = ZLegacySurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionTypeEnum.Rating),
scale: z.enum(["number", "smiley", "star"]),
range: z.union([z.literal(5), z.literal(3), z.literal(4), z.literal(7), z.literal(10)]),
lowerLabel: z.string(),
upperLabel: z.string(),
logic: z.array(ZSurveyRatingLogic).optional(),
});
export const ZLegacySurveyDateQuestion = ZLegacySurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionTypeEnum.Date),
html: z.string().optional(),
format: z.enum(["M-d-y", "d-M-y", "y-M-d"]),
});
export type TLegacySurveyDateQuestion = z.infer<typeof ZLegacySurveyDateQuestion>;
export type TLegacySurveyRatingQuestion = z.infer<typeof ZLegacySurveyRatingQuestion>;
export const ZLegacySurveyPictureSelectionQuestion = ZLegacySurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionTypeEnum.PictureSelection),
allowMulti: z.boolean().optional().default(false),
choices: z.array(ZSurveyPictureChoice),
logic: z.array(ZSurveyPictureSelectionLogic).optional(),
});
export type TLegacySurveyPictureSelectionQuestion = z.infer<typeof ZLegacySurveyPictureSelectionQuestion>;
export const ZLegacySurveyFileUploadQuestion = ZLegacySurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionTypeEnum.FileUpload),
allowMultipleFiles: z.boolean(),
maxSizeInMB: z.number().optional(),
allowedFileExtensions: z.array(ZAllowedFileExtension).optional(),
logic: z.array(ZSurveyFileUploadLogic).optional(),
});
export type TLegacySurveyFileUploadQuestion = z.infer<typeof ZLegacySurveyFileUploadQuestion>;
export const ZLegacySurveyCalQuestion = ZLegacySurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionTypeEnum.Cal),
calUserName: z.string(),
calHost: z.string().optional(),
logic: z.array(ZSurveyCalLogic).optional(),
});
export type TLegacySurveyCalQuestion = z.infer<typeof ZLegacySurveyCalQuestion>;
export const ZLegacySurveyQuestion = z.union([
ZLegacySurveyOpenTextQuestion,
ZLegacySurveyConsentQuestion,
ZLegacySurveyMultipleChoiceSingleQuestion,
ZLegacySurveyMultipleChoiceMultiQuestion,
ZLegacySurveyNPSQuestion,
ZLegacySurveyCTAQuestion,
ZLegacySurveyRatingQuestion,
ZLegacySurveyPictureSelectionQuestion,
ZLegacySurveyDateQuestion,
ZLegacySurveyFileUploadQuestion,
ZLegacySurveyCalQuestion,
]);
export const ZLegacySurveyThankYouCard = z.object({
enabled: z.boolean(),
headline: z.string().optional(),
subheader: z.string().optional(),
buttonLabel: z.optional(z.string()),
buttonLink: z.optional(z.string()),
imageUrl: z.string().optional(),
});
export type TLegacySurveyThankYouCard = z.infer<typeof ZLegacySurveyThankYouCard>;
export const ZLegacySurveyWelcomeCard = z.object({
enabled: z.boolean(),
headline: z.string(),
html: z.string().optional(),
fileUrl: z.string().optional(),
buttonLabel: z.string().optional(),
timeToFinish: z.boolean().default(true),
showResponseCount: z.boolean().default(false),
});
export type TLegacySurveyWelcomeCard = z.infer<typeof ZLegacySurveyWelcomeCard>;
export type TLegacySurveyQuestion = z.infer<typeof ZLegacySurveyQuestion>;
export const ZLegacySurveyQuestions = z.array(ZLegacySurveyQuestion);
// ZSurvey is a refinement, so to extend it to ZLegacySurvey, we need to extend the innerType and then apply the same refinements.
export const ZLegacySurvey = ZSurvey.innerType().extend({
questions: ZLegacySurveyQuestions,
thankYouCard: ZLegacySurveyThankYouCard,
welcomeCard: ZLegacySurveyWelcomeCard,
triggers: z.array(z.string()),
endings: ZSurveyEndings.optional(),
});
export type TLegacySurvey = z.infer<typeof ZLegacySurvey>;

View File

@@ -74,33 +74,6 @@ export const ZProduct = z.object({
export type TProduct = z.infer<typeof ZProduct>;
export const ZProductLegacy = z.object({
id: z.string().cuid2(),
createdAt: z.date(),
updatedAt: z.date(),
name: z.string().trim().min(1, { message: "Product name cannot be empty" }),
organizationId: z.string(),
styling: ZProductStyling,
recontactDays: z
.number({ message: "Recontact days is required" })
.int()
.min(0, { message: "Must be a positive number" })
.max(365, { message: "Must be less than 365" }),
inAppSurveyBranding: z.boolean(),
linkSurveyBranding: z.boolean(),
config: ZProductConfig,
placement: ZPlacement,
clickOutsideClose: z.boolean(),
darkOverlay: z.boolean(),
environments: z.array(ZEnvironment),
brandColor: ZColor.nullish(),
highlightBorderColor: ZColor.nullish(),
languages: z.array(ZLanguage),
logo: ZLogo.nullish(),
});
export type TProductLegacy = z.infer<typeof ZProductLegacy>;
export const ZProductUpdateInput = z.object({
name: z.string().trim().min(1, { message: "Product name cannot be empty" }).optional(),
organizationId: z.string().optional(),

View File

@@ -270,12 +270,6 @@ export const ZResponseInput = z.object({
export type TResponseInput = z.infer<typeof ZResponseInput>;
export const ZResponseLegacyInput = ZResponseInput.omit({ userId: true, environmentId: true }).extend({
personId: z.string().cuid2().nullable(),
});
export type TResponseLegacyInput = z.infer<typeof ZResponseLegacyInput>;
export const ZResponseUpdateInput = z.object({
finished: z.boolean(),
data: ZResponseData,

View File

@@ -20,11 +20,8 @@ export const ZI18nString = z.record(z.string()).refine((obj) => "default" in obj
export type TI18nString = z.infer<typeof ZI18nString>;
const ZEndScreenType = z.union([z.literal("endScreen"), z.literal("redirectToUrl")]);
const ZSurveyEndingBase = z.object({
id: z.string().cuid2(),
type: ZEndScreenType,
});
export const ZSurveyEndScreenCard = ZSurveyEndingBase.extend({
@@ -36,6 +33,7 @@ export const ZSurveyEndScreenCard = ZSurveyEndingBase.extend({
imageUrl: z.string().optional(),
videoUrl: z.string().optional(),
});
export type TSurveyEndScreenCard = z.infer<typeof ZSurveyEndScreenCard>;
export const ZSurveyRedirectUrlCard = ZSurveyEndingBase.extend({
@@ -43,9 +41,16 @@ export const ZSurveyRedirectUrlCard = ZSurveyEndingBase.extend({
url: z.string().url("Invalid redirect Url in Ending card").optional(),
label: z.string().optional(),
});
export type TSurveyRedirectUrlCard = z.infer<typeof ZSurveyRedirectUrlCard>;
export const ZSurveyEndings = z.array(z.union([ZSurveyEndScreenCard, ZSurveyRedirectUrlCard]));
export const ZSurveyEnding = z.union([ZSurveyEndScreenCard, ZSurveyRedirectUrlCard]);
export type TSurveyEnding = z.infer<typeof ZSurveyEnding>;
export const ZSurveyEndings = z.array(ZSurveyEnding);
export type TSurveyEndings = z.infer<typeof ZSurveyEnding>;
export enum TSurveyQuestionTypeEnum {
FileUpload = "fileUpload",
@@ -103,6 +108,8 @@ export const ZSurveyWelcomeCard = z
message: "Welcome card must have a headline",
});
export type TSurveyWelcomeCard = z.infer<typeof ZSurveyWelcomeCard>;
export const ZSurveyHiddenFields = z.object({
enabled: z.boolean(),
fieldIds: z.optional(
@@ -134,6 +141,8 @@ export const ZSurveyHiddenFields = z.object({
),
});
export type TSurveyHiddenFields = z.infer<typeof ZSurveyHiddenFields>;
export const ZSurveyProductOverwrites = z.object({
brandColor: ZColor.nullish(),
highlightBorderColor: ZColor.nullish(),
@@ -163,6 +172,8 @@ export const ZSurveyClosedMessage = z
.nullable()
.optional();
export type TSurveyClosedMessage = z.infer<typeof ZSurveyClosedMessage>;
export const ZSurveySingleUse = z
.object({
enabled: z.boolean(),
@@ -174,14 +185,6 @@ export const ZSurveySingleUse = z
export type TSurveySingleUse = z.infer<typeof ZSurveySingleUse>;
export type TSurveyWelcomeCard = z.infer<typeof ZSurveyWelcomeCard>;
export type TSurveyEndings = z.infer<typeof ZSurveyEndings>;
export type TSurveyHiddenFields = z.infer<typeof ZSurveyHiddenFields>;
export type TSurveyClosedMessage = z.infer<typeof ZSurveyClosedMessage>;
export const ZSurveyChoice = z.object({
id: z.string(),
label: ZI18nString,

View File

@@ -1,9 +1,4 @@
import { z } from "zod";
import {
ZLegacySurveyQuestions,
ZLegacySurveyThankYouCard,
ZLegacySurveyWelcomeCard,
} from "./legacy-surveys";
import { ZProductConfigChannel, ZProductConfigIndustry } from "./product";
import { ZSurveyEndings, ZSurveyHiddenFields, ZSurveyQuestions, ZSurveyWelcomeCard } from "./surveys/types";
import { ZUserObjective } from "./user";
@@ -30,18 +25,6 @@ export const ZTemplate = z.object({
export type TTemplate = z.infer<typeof ZTemplate>;
export const ZLegacyTemplate = ZTemplate.extend({
preset: z.object({
name: z.string(),
welcomeCard: ZLegacySurveyWelcomeCard,
questions: ZLegacySurveyQuestions,
thankYouCard: ZLegacySurveyThankYouCard,
hiddenFields: ZSurveyHiddenFields,
}),
});
export type TLegacyTemplate = z.infer<typeof ZLegacyTemplate>;
export const ZTemplateFilter = z.union([
ZProductConfigChannel,
ZProductConfigIndustry,

View File

@@ -1,5 +1,5 @@
{
"extends": "@formbricks/config-typescript/base.json",
"extends": "@formbricks/config-typescript/node16.json",
"include": ["."],
"exclude": ["dist", "build", "node_modules"],
"compilerOptions": {