chore: remove unused fields and tables from prisma schema (#6531)

Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
This commit is contained in:
Matti Nannt
2025-09-12 11:01:03 +02:00
committed by GitHub
parent 96031822a6
commit 839144d338
66 changed files with 440 additions and 733 deletions

View File

@@ -23,12 +23,12 @@ describe("ConnectWithFormbricks", () => {
const webAppUrl = "http://app";
const channel = {} as any;
test("renders waiting state when widgetSetupCompleted is false", () => {
test("renders waiting state when appSetupCompleted is false", () => {
render(
<ConnectWithFormbricks
environment={environment}
publicDomain={webAppUrl}
widgetSetupCompleted={false}
appSetupCompleted={false}
channel={channel}
/>
);
@@ -36,12 +36,12 @@ describe("ConnectWithFormbricks", () => {
expect(screen.getByText("environments.connect.waiting_for_your_signal")).toBeInTheDocument();
});
test("renders success state when widgetSetupCompleted is true", () => {
test("renders success state when appSetupCompleted is true", () => {
render(
<ConnectWithFormbricks
environment={environment}
publicDomain={webAppUrl}
widgetSetupCompleted={true}
appSetupCompleted={true}
channel={channel}
/>
);
@@ -54,7 +54,7 @@ describe("ConnectWithFormbricks", () => {
<ConnectWithFormbricks
environment={environment}
publicDomain={webAppUrl}
widgetSetupCompleted={true}
appSetupCompleted={true}
channel={channel}
/>
);
@@ -68,7 +68,7 @@ describe("ConnectWithFormbricks", () => {
<ConnectWithFormbricks
environment={environment}
publicDomain={webAppUrl}
widgetSetupCompleted={false}
appSetupCompleted={false}
channel={channel}
/>
);

View File

@@ -13,14 +13,14 @@ import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions";
interface ConnectWithFormbricksProps {
environment: TEnvironment;
publicDomain: string;
widgetSetupCompleted: boolean;
appSetupCompleted: boolean;
channel: TProjectConfigChannel;
}
export const ConnectWithFormbricks = ({
environment,
publicDomain,
widgetSetupCompleted,
appSetupCompleted,
channel,
}: ConnectWithFormbricksProps) => {
const { t } = useTranslate();
@@ -51,15 +51,15 @@ export const ConnectWithFormbricks = ({
environmentId={environment.id}
publicDomain={publicDomain}
channel={channel}
widgetSetupCompleted={widgetSetupCompleted}
appSetupCompleted={appSetupCompleted}
/>
</div>
<div
className={cn(
"flex h-[30rem] w-1/2 flex-col items-center justify-center rounded-lg border text-center",
widgetSetupCompleted ? "border-green-500 bg-green-100" : "border-slate-300 bg-slate-200"
appSetupCompleted ? "border-green-500 bg-green-100" : "border-slate-300 bg-slate-200"
)}>
{widgetSetupCompleted ? (
{appSetupCompleted ? (
<div>
<p className="text-3xl">{t("environments.connect.congrats")}</p>
<p className="pt-4 text-sm font-medium text-slate-600">
@@ -81,9 +81,9 @@ export const ConnectWithFormbricks = ({
</div>
<Button
id="finishOnboarding"
variant={widgetSetupCompleted ? "default" : "ghost"}
variant={appSetupCompleted ? "default" : "ghost"}
onClick={handleFinishOnboarding}>
{widgetSetupCompleted
{appSetupCompleted
? t("environments.connect.finish_onboarding")
: t("environments.connect.do_it_later")}
<ArrowRight />

View File

@@ -35,7 +35,7 @@ describe("OnboardingSetupInstructions", () => {
environmentId: "env-123",
publicDomain: "https://example.com",
channel: "app" as const, // Assuming channel is either "app" or "website"
widgetSetupCompleted: false,
appSetupCompleted: false,
};
test("renders HTML tab content by default", () => {

View File

@@ -20,14 +20,14 @@ interface OnboardingSetupInstructionsProps {
environmentId: string;
publicDomain: string;
channel: TProjectConfigChannel;
widgetSetupCompleted: boolean;
appSetupCompleted: boolean;
}
export const OnboardingSetupInstructions = ({
environmentId,
publicDomain,
channel,
widgetSetupCompleted,
appSetupCompleted,
}: OnboardingSetupInstructionsProps) => {
const { t } = useTranslate();
const [activeTab, setActiveTab] = useState(tabs[0].id);
@@ -137,7 +137,7 @@ export const OnboardingSetupInstructions = ({
<div className="mt-4 flex justify-between space-x-2">
<Button
id="onboarding-inapp-connect-copy-code"
variant={widgetSetupCompleted ? "secondary" : "default"}
variant={appSetupCompleted ? "secondary" : "default"}
onClick={() => {
navigator.clipboard.writeText(
channel === "app" ? htmlSnippetForAppSurveys : htmlSnippetForWebsiteSurveys

View File

@@ -42,7 +42,7 @@ const Page = async (props: ConnectPageProps) => {
<ConnectWithFormbricks
environment={environment}
publicDomain={publicDomain}
widgetSetupCompleted={environment.appSetupCompleted}
appSetupCompleted={environment.appSetupCompleted}
channel={channel}
/>
<Button

View File

@@ -36,8 +36,6 @@ describe("PosthogIdentify", () => {
{
name: "Test User",
email: "test@example.com",
role: "engineer",
objective: "increase_conversion",
} as TUser
}
environmentId="env-456"
@@ -57,8 +55,6 @@ describe("PosthogIdentify", () => {
expect(mockIdentify).toHaveBeenCalledWith("user-123", {
name: "Test User",
email: "test@example.com",
role: "engineer",
objective: "increase_conversion",
});
// environment + organization groups
@@ -142,8 +138,6 @@ describe("PosthogIdentify", () => {
expect(mockIdentify).toHaveBeenCalledWith("user-123", {
name: "Test User",
email: "test@example.com",
role: undefined,
objective: undefined,
});
// No environmentId or organizationId => no group calls
expect(mockGroup).not.toHaveBeenCalled();

View File

@@ -32,8 +32,6 @@ export const PosthogIdentify = ({
posthog.identify(session.user.id, {
name: user.name,
email: user.email,
role: user.role,
objective: user.objective,
});
if (environmentId) {
posthog.group("environment", environmentId, { name: environmentId });
@@ -56,8 +54,6 @@ export const PosthogIdentify = ({
organizationBilling,
user.name,
user.email,
user.role,
user.objective,
isPosthogEnabled,
]);

View File

@@ -226,7 +226,7 @@ describe("Integrations Page", () => {
expect(screen.getByTestId("card-Activepieces")).toHaveTextContent("5 common.integrations");
});
test("renders not connected status when widgetSetupCompleted is false", async () => {
test("renders not connected status when appSetupCompleted is false", async () => {
vi.mocked(getEnvironmentAuth).mockResolvedValue({
environment: { ...mockEnvironment, appSetupCompleted: false },
isReadOnly: false,

View File

@@ -62,7 +62,7 @@ const Page = async (props) => {
const isN8nIntegrationConnected = isIntegrationConnected("n8n");
const isSlackIntegrationConnected = isIntegrationConnected("slack");
const widgetSetupCompleted = !!environment?.appSetupCompleted;
const appSetupCompleted = !!environment?.appSetupCompleted;
const integrationCards = [
{
docsHref: "https://formbricks.com/docs/xm-and-surveys/core-features/integrations/zapier",
@@ -202,8 +202,8 @@ const Page = async (props) => {
label: "Javascript SDK",
description: t("environments.integrations.website_or_app_integration_description"),
icon: <Image src={JsLogo} alt="Javascript Logo" />,
connected: widgetSetupCompleted,
statusText: widgetSetupCompleted ? t("common.connected") : t("common.not_connected"),
connected: appSetupCompleted,
statusText: appSetupCompleted ? t("common.connected") : t("common.not_connected"),
disabled: false,
});

View File

@@ -63,7 +63,6 @@ const mockSurvey = {
id: "survey1",
name: "Test Survey",
questions: [],
thankYouCard: { enabled: true, headline: "Thank You!" },
hiddenFields: { enabled: true, fieldIds: [] },
displayOption: "displayOnce",
recontactDays: 0,

View File

@@ -151,7 +151,6 @@ const mockSurvey: TSurvey = {
status: "inProgress",
type: "web",
questions: [],
thankYouCard: { enabled: false },
endings: [],
languages: [],
triggers: [],

View File

@@ -19,19 +19,19 @@ export const SuccessMessage = ({ environment, survey }: SummaryMetadataProps) =>
const [confetti, setConfetti] = useState(false);
const isAppSurvey = survey.type === "app";
const widgetSetupCompleted = environment.appSetupCompleted;
const appSetupCompleted = environment.appSetupCompleted;
useEffect(() => {
const newSurveyParam = searchParams?.get("success");
if (newSurveyParam && survey && environment) {
setConfetti(true);
toast.success(
isAppSurvey && !widgetSetupCompleted
isAppSurvey && !appSetupCompleted
? t("environments.surveys.summary.almost_there")
: t("environments.surveys.summary.congrats"),
{
id: "survey-publish-success-toast",
icon: isAppSurvey && !widgetSetupCompleted ? "🤏" : "🎉",
icon: isAppSurvey && !appSetupCompleted ? "🤏" : "🎉",
duration: 5000,
position: "bottom-right",
}
@@ -47,7 +47,7 @@ export const SuccessMessage = ({ environment, survey }: SummaryMetadataProps) =>
window.history.replaceState({}, "", url.toString());
}
}, [environment, isAppSurvey, searchParams, survey, widgetSetupCompleted, t]);
}, [environment, isAppSurvey, searchParams, survey, appSetupCompleted, t]);
return <>{confetti && <Confetti />}</>;
};

View File

@@ -69,7 +69,7 @@ export const SurveyAnalysisCTA = ({
const { organizationId, project } = useEnvironment();
const { refreshSingleUseId } = useSingleUseId(survey, isReadOnly);
const widgetSetupCompleted = survey.type === "app" && environment.appSetupCompleted;
const appSetupCompleted = survey.type === "app" && environment.appSetupCompleted;
useEffect(() => {
setModalState((prev) => ({
@@ -186,7 +186,7 @@ export const SurveyAnalysisCTA = ({
return (
<div className="hidden justify-end gap-x-1.5 sm:flex">
{!isReadOnly && (widgetSetupCompleted || survey.type === "link") && survey.status !== "draft" && (
{!isReadOnly && (appSetupCompleted || survey.type === "link") && survey.status !== "draft" && (
<SurveyStatusDropdown environment={environment} survey={survey} />
)}

View File

@@ -146,7 +146,6 @@ describe("AnonymousLinksTab", () => {
createdBy: null,
status: "draft" as const,
questions: [],
thankYouCard: { enabled: false },
welcomeCard: { enabled: false },
hiddenFields: { enabled: false },
singleUse: {

View File

@@ -88,7 +88,6 @@ const baseSurvey: TSurvey = {
segment: null,
surveyClosedMessage: null,
singleUse: null,
verifyEmail: null,
pin: null,
productOverwrites: null,
analytics: {

View File

@@ -59,7 +59,6 @@ const mockDisplay = {
contactId,
surveyId,
responseId: null,
status: null,
createdAt: new Date(),
updatedAt: new Date(),
};
@@ -69,7 +68,6 @@ const mockDisplayWithoutContact = {
contactId: null,
surveyId,
responseId: null,
status: null,
createdAt: new Date(),
updatedAt: new Date(),
};

View File

@@ -28,7 +28,7 @@ export const GET = async () => {
createdAt: true,
updatedAt: true,
projectId: true,
widgetSetupCompleted: true,
appSetupCompleted: true,
project: {
select: {
id: true,
@@ -62,7 +62,7 @@ export const GET = async () => {
type: apiKeyData.apiKeyEnvironments[0].environment.type,
createdAt: apiKeyData.apiKeyEnvironments[0].environment.createdAt,
updatedAt: apiKeyData.apiKeyEnvironments[0].environment.updatedAt,
widgetSetupCompleted: apiKeyData.apiKeyEnvironments[0].environment.widgetSetupCompleted,
appSetupCompleted: apiKeyData.apiKeyEnvironments[0].environment.appSetupCompleted,
project: {
id: apiKeyData.apiKeyEnvironments[0].environment.projectId,
name: apiKeyData.apiKeyEnvironments[0].environment.project.name,

View File

@@ -47,7 +47,6 @@ const mockDisplay = {
contactId,
surveyId,
responseId: null,
status: null,
createdAt: new Date(),
updatedAt: new Date(),
};
@@ -57,7 +56,6 @@ const mockDisplayWithoutContact = {
contactId: null,
surveyId,
responseId: null,
status: null,
createdAt: new Date(),
updatedAt: new Date(),
};

View File

@@ -565,7 +565,6 @@ describe("Helper Functions", () => {
test("buildSurvey returns built survey with overridden preset properties", () => {
const config = {
name: "Custom Survey",
role: "productManager" as TTemplateRole,
industries: ["eCommerce"] as string[],
channels: ["link"],
description: "Test survey",
@@ -595,7 +594,6 @@ describe("Helper Functions", () => {
const survey = buildSurvey(config as any, mockT);
expect(survey.name).toBe(config.name);
expect(survey.role).toBe(config.role);
expect(survey.industries).toEqual(config.industries);
expect(survey.channels).toEqual(config.channels);
expect(survey.description).toBe(config.description);

View File

@@ -19,7 +19,7 @@ import {
TSurveyRatingQuestion,
TSurveyWelcomeCard,
} from "@formbricks/types/surveys/types";
import { TTemplate, TTemplateRole } from "@formbricks/types/templates";
import { TTemplate } from "@formbricks/types/templates";
const getDefaultButtonLabel = (label: string | undefined, t: TFnType) =>
createI18nString(label || t("common.next"), []);
@@ -389,7 +389,6 @@ export const getDefaultSurveyPreset = (t: TFnType): TTemplate["preset"] => {
export const buildSurvey = (
config: {
name: string;
role: TTemplateRole;
industries: ("eCommerce" | "saas" | "other")[];
channels: ("link" | "app" | "website")[];
description: string;
@@ -402,7 +401,6 @@ export const buildSurvey = (
const localSurvey = getDefaultSurveyPreset(t);
return {
name: config.name,
role: config.role,
industries: config.industries,
channels: config.channels,
description: config.description,

View File

@@ -24,7 +24,6 @@ const cartAbandonmentSurvey = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.card_abandonment_survey"),
role: "productManager",
industries: ["eCommerce"],
channels: ["app", "website", "link"],
description: t("templates.card_abandonment_survey_description"),
@@ -125,7 +124,6 @@ const siteAbandonmentSurvey = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.site_abandonment_survey"),
role: "productManager",
industries: ["eCommerce"],
channels: ["app", "website"],
description: t("templates.site_abandonment_survey_description"),
@@ -223,7 +221,6 @@ const productMarketFitSuperhuman = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.product_market_fit_superhuman"),
role: "productManager",
industries: ["saas"],
channels: ["app", "link"],
description: t("templates.product_market_fit_superhuman_description"),
@@ -298,7 +295,6 @@ const onboardingSegmentation = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.onboarding_segmentation"),
role: "productManager",
industries: ["saas"],
channels: ["app", "link"],
description: t("templates.onboarding_segmentation_description"),
@@ -362,7 +358,6 @@ const churnSurvey = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.churn_survey"),
role: "sales",
industries: ["saas", "eCommerce", "other"],
channels: ["app", "link"],
description: t("templates.churn_survey_description"),
@@ -452,7 +447,6 @@ const earnedAdvocacyScore = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.earned_advocacy_score_name"),
role: "customerSuccess",
industries: ["saas", "eCommerce", "other"],
channels: ["app", "link"],
description: t("templates.earned_advocacy_score_description"),
@@ -525,7 +519,6 @@ const usabilityScoreRatingSurvey = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.usability_score_name"),
role: "customerSuccess",
industries: ["saas"],
channels: ["app", "link"],
description: t("templates.usability_rating_description"),
@@ -651,7 +644,6 @@ const improveTrialConversion = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.improve_trial_conversion_name"),
role: "sales",
industries: ["saas"],
channels: ["link", "app"],
description: t("templates.improve_trial_conversion_description"),
@@ -753,7 +745,6 @@ const reviewPrompt = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.review_prompt_name"),
role: "marketing",
industries: ["saas", "eCommerce", "other"],
channels: ["link", "app"],
description: t("templates.review_prompt_description"),
@@ -832,7 +823,6 @@ const interviewPrompt = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.interview_prompt_name"),
role: "productManager",
industries: ["saas"],
channels: ["app"],
description: t("templates.interview_prompt_description"),
@@ -860,7 +850,6 @@ const improveActivationRate = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.improve_activation_rate_name"),
role: "productManager",
industries: ["saas"],
channels: ["link"],
description: t("templates.improve_activation_rate_description"),
@@ -951,7 +940,6 @@ const employeeSatisfaction = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.employee_satisfaction_name"),
role: "peopleManager",
industries: ["saas", "eCommerce", "other"],
channels: ["app", "link"],
description: t("templates.employee_satisfaction_description"),
@@ -1029,7 +1017,6 @@ const uncoverStrengthsAndWeaknesses = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.uncover_strengths_and_weaknesses_name"),
role: "productManager",
industries: ["saas", "other"],
channels: ["app", "link"],
description: t("templates.uncover_strengths_and_weaknesses_description"),
@@ -1083,7 +1070,6 @@ const productMarketFitShort = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.product_market_fit_short_name"),
role: "productManager",
industries: ["saas"],
channels: ["app", "link"],
description: t("templates.product_market_fit_short_description"),
@@ -1120,7 +1106,6 @@ const marketAttribution = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.market_attribution_name"),
role: "marketing",
industries: ["saas", "eCommerce"],
channels: ["website", "app", "link"],
description: t("templates.market_attribution_description"),
@@ -1151,7 +1136,6 @@ const changingSubscriptionExperience = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.changing_subscription_experience_name"),
role: "productManager",
industries: ["saas"],
channels: ["app"],
description: t("templates.changing_subscription_experience_description"),
@@ -1194,7 +1178,6 @@ const identifyCustomerGoals = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.identify_customer_goals_name"),
role: "productManager",
industries: ["saas", "other"],
channels: ["app", "website"],
description: t("templates.identify_customer_goals_description"),
@@ -1224,7 +1207,6 @@ const featureChaser = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.feature_chaser_name"),
role: "productManager",
industries: ["saas"],
channels: ["app"],
description: t("templates.feature_chaser_description"),
@@ -1263,7 +1245,6 @@ const fakeDoorFollowUp = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.fake_door_follow_up_name"),
role: "productManager",
industries: ["saas", "eCommerce"],
channels: ["app", "website"],
description: t("templates.fake_door_follow_up_description"),
@@ -1307,7 +1288,6 @@ const feedbackBox = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.feedback_box_name"),
role: "productManager",
industries: ["saas"],
channels: ["app"],
description: t("templates.feedback_box_description"),
@@ -1377,7 +1357,6 @@ const integrationSetupSurvey = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.integration_setup_survey_name"),
role: "productManager",
industries: ["saas"],
channels: ["app"],
description: t("templates.integration_setup_survey_description"),
@@ -1450,7 +1429,6 @@ const newIntegrationSurvey = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.new_integration_survey_name"),
role: "productManager",
industries: ["saas"],
channels: ["app"],
description: t("templates.new_integration_survey_description"),
@@ -1482,7 +1460,6 @@ const docsFeedback = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.docs_feedback_name"),
role: "productManager",
industries: ["saas"],
channels: ["app", "website", "link"],
description: t("templates.docs_feedback_description"),
@@ -1522,7 +1499,6 @@ const nps = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.nps_name"),
role: "customerSuccess",
industries: ["saas", "eCommerce", "other"],
channels: ["app", "link", "website"],
description: t("templates.nps_description"),
@@ -1563,7 +1539,6 @@ const customerSatisfactionScore = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.csat_name"),
role: "customerSuccess",
industries: ["saas", "eCommerce", "other"],
channels: ["app", "link", "website"],
description: t("templates.csat_description"),
@@ -1732,7 +1707,6 @@ const collectFeedback = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.collect_feedback_name"),
role: "productManager",
industries: ["other", "eCommerce"],
channels: ["website", "link"],
description: t("templates.collect_feedback_description"),
@@ -1879,7 +1853,6 @@ const identifyUpsellOpportunities = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.identify_upsell_opportunities_name"),
role: "sales",
industries: ["saas"],
channels: ["app", "link"],
description: t("templates.identify_upsell_opportunities_description"),
@@ -1909,7 +1882,6 @@ const prioritizeFeatures = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.prioritize_features_name"),
role: "productManager",
industries: ["saas"],
channels: ["app"],
description: t("templates.prioritize_features_description"),
@@ -1962,7 +1934,6 @@ const gaugeFeatureSatisfaction = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.gauge_feature_satisfaction_name"),
role: "productManager",
industries: ["saas"],
channels: ["app"],
description: t("templates.gauge_feature_satisfaction_description"),
@@ -1996,7 +1967,6 @@ const marketSiteClarity = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.market_site_clarity_name"),
role: "marketing",
industries: ["saas", "eCommerce", "other"],
channels: ["website"],
description: t("templates.market_site_clarity_description"),
@@ -2038,7 +2008,6 @@ const customerEffortScore = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.customer_effort_score_name"),
role: "productManager",
industries: ["saas"],
channels: ["app"],
description: t("templates.customer_effort_score_description"),
@@ -2070,7 +2039,6 @@ const careerDevelopmentSurvey = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.career_development_survey_name"),
role: "productManager",
industries: ["saas", "eCommerce", "other"],
channels: ["link"],
description: t("templates.career_development_survey_description"),
@@ -2157,7 +2125,6 @@ const professionalDevelopmentSurvey = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.professional_development_survey_name"),
role: "productManager",
industries: ["saas", "eCommerce", "other"],
channels: ["link"],
description: t("templates.professional_development_survey_description"),
@@ -2245,7 +2212,6 @@ const rateCheckoutExperience = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.rate_checkout_experience_name"),
role: "productManager",
industries: ["eCommerce"],
channels: ["website", "app"],
description: t("templates.rate_checkout_experience_description"),
@@ -2322,7 +2288,6 @@ const measureSearchExperience = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.measure_search_experience_name"),
role: "productManager",
industries: ["saas", "eCommerce"],
channels: ["app", "website"],
description: t("templates.measure_search_experience_description"),
@@ -2399,7 +2364,6 @@ const evaluateContentQuality = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.evaluate_content_quality_name"),
role: "marketing",
industries: ["other"],
channels: ["website"],
description: t("templates.evaluate_content_quality_description"),
@@ -2477,7 +2441,6 @@ const measureTaskAccomplishment = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.measure_task_accomplishment_name"),
role: "productManager",
industries: ["saas"],
channels: ["app", "website"],
description: t("templates.measure_task_accomplishment_description"),
@@ -2660,7 +2623,6 @@ const identifySignUpBarriers = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.identify_sign_up_barriers_name"),
role: "marketing",
industries: ["saas", "eCommerce", "other"],
channels: ["website"],
description: t("templates.identify_sign_up_barriers_description"),
@@ -2812,7 +2774,6 @@ const buildProductRoadmap = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.build_product_roadmap_name"),
role: "productManager",
industries: ["saas"],
channels: ["app", "link"],
description: t("templates.build_product_roadmap_description"),
@@ -2847,7 +2808,6 @@ const understandPurchaseIntention = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.understand_purchase_intention_name"),
role: "sales",
industries: ["eCommerce"],
channels: ["website", "link", "app"],
description: t("templates.understand_purchase_intention_description"),
@@ -2903,7 +2863,6 @@ const improveNewsletterContent = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.improve_newsletter_content_name"),
role: "marketing",
industries: ["eCommerce", "saas", "other"],
channels: ["link"],
description: t("templates.improve_newsletter_content_description"),
@@ -2994,7 +2953,6 @@ const evaluateAProductIdea = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.evaluate_a_product_idea_name"),
role: "productManager",
industries: ["saas", "other"],
channels: ["link", "app"],
description: t("templates.evaluate_a_product_idea_description"),
@@ -3097,7 +3055,6 @@ const understandLowEngagement = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.understand_low_engagement_name"),
role: "productManager",
industries: ["saas"],
channels: ["link"],
description: t("templates.understand_low_engagement_description"),
@@ -3183,7 +3140,6 @@ const employeeWellBeing = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.employee_well_being_name"),
role: "peopleManager",
industries: ["saas", "eCommerce", "other"],
channels: ["link"],
description: t("templates.employee_well_being_description"),
@@ -3233,7 +3189,6 @@ const longTermRetentionCheckIn = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.long_term_retention_check_in_name"),
role: "peopleManager",
industries: ["saas", "other"],
channels: ["app", "link"],
description: t("templates.long_term_retention_check_in_description"),
@@ -3342,7 +3297,6 @@ const professionalDevelopmentGrowth = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.professional_development_growth_survey_name"),
role: "peopleManager",
industries: ["saas", "eCommerce", "other"],
channels: ["link"],
description: t("templates.professional_development_growth_survey_description"),
@@ -3392,7 +3346,6 @@ const recognitionAndReward = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.recognition_and_reward_survey_name"),
role: "peopleManager",
industries: ["saas", "eCommerce", "other"],
channels: ["link"],
description: t("templates.recognition_and_reward_survey_description"),
@@ -3441,7 +3394,6 @@ const alignmentAndEngagement = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.alignment_and_engagement_survey_name"),
role: "peopleManager",
industries: ["saas", "eCommerce", "other"],
channels: ["link"],
description: t("templates.alignment_and_engagement_survey_description"),
@@ -3490,7 +3442,6 @@ const supportiveWorkCulture = (t: TFnType): TTemplate => {
return buildSurvey(
{
name: t("templates.supportive_work_culture_survey_name"),
role: "peopleManager",
industries: ["saas", "eCommerce", "other"],
channels: ["link"],
description: t("templates.supportive_work_culture_survey_description"),

View File

@@ -13,7 +13,6 @@ export const selectDisplay = {
updatedAt: true,
surveyId: true,
contactId: true,
status: true,
} satisfies Prisma.DisplaySelect;
export const getDisplayCountBySurveyId = reactCache(

View File

@@ -17,7 +17,6 @@ const createMockDisplay = (overrides = {}) => {
surveyId: mockSurveyId,
responseId: null,
personId: null,
status: null,
...overrides,
};
};

View File

@@ -34,7 +34,6 @@ describe("Environment Service", () => {
createdAt: new Date(),
updatedAt: new Date(),
appSetupCompleted: false,
widgetSetupCompleted: false,
};
vi.mocked(prisma.environment.findUnique).mockResolvedValue(mockEnvironment);
@@ -95,11 +94,9 @@ describe("Environment Service", () => {
environments: [
{
...mockEnvironments[0],
widgetSetupCompleted: false,
},
{
...mockEnvironments[1],
widgetSetupCompleted: true,
},
],
});
@@ -143,7 +140,6 @@ describe("Environment Service", () => {
createdAt: new Date(),
updatedAt: new Date(),
appSetupCompleted: false,
widgetSetupCompleted: false,
};
vi.mocked(prisma.environment.update).mockResolvedValue(mockEnvironment);

View File

@@ -508,11 +508,3 @@ export const mockTranslatedEndings = [
buttonLabel: { default: "Create your own Survey", de: "" },
},
];
export const mockLegacyThankYouCard = {
buttonLink: "https://formbricks.com",
enabled: true,
headline: "Thank you!",
subheader: "We appreciate your feedback.",
buttonLabel: "Create your own Survey",
};

View File

@@ -244,7 +244,6 @@ describe("Response Processing", () => {
displayPercentage: 100,
styling: null,
projectOverwrites: null,
verifyEmail: null,
inlineTriggers: [],
pin: null,
triggers: [],

View File

@@ -126,13 +126,11 @@ export const mockUser: TUser = {
updatedAt: currentDate,
twoFactorEnabled: false,
identityProvider: "google",
objective: "improve_user_retention",
notificationSettings: {
alert: {},
unsubscribedOrganizationIds: [],
},
role: "other",
locale: "en-US",
lastLoginAt: new Date(),
isActive: true,
@@ -262,8 +260,6 @@ export const mockSyncSurveyOutput: SurveyMock = {
followUps: [],
variables: [],
showLanguageSwitch: null,
thankYouCard: null,
verifyEmail: null,
metadata: {},
};
@@ -287,8 +283,6 @@ export const mockSurveyOutput: SurveyMock = {
followUps: [],
variables: [],
showLanguageSwitch: null,
thankYouCard: null,
verifyEmail: null,
...baseSurveyProperties,
};

View File

@@ -532,8 +532,6 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise<TSurvey> =>
};
}
// TODO: Fix this, this happens because the survey type "web" is no longer in the zod types but its required in the schema for migration
// @ts-expect-error
const modifiedSurvey: TSurvey = {
...prismaSurvey, // Properties from prismaSurvey
displayPercentage: Number(prismaSurvey.displayPercentage) || null,

View File

@@ -1,5 +1,5 @@
import { deleteOrganization, getOrganizationsWhereUserIsSingleOwner } from "@/lib/organization/service";
import { IdentityProvider, Objective, Prisma, Role } from "@prisma/client";
import { IdentityProvider, Prisma } from "@prisma/client";
import { afterEach, describe, expect, test, vi } from "vitest";
import { prisma } from "@formbricks/database";
import { PrismaErrorType } from "@formbricks/database/types/error";
@@ -37,10 +37,8 @@ describe("User Service", () => {
emailVerified: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
role: Role.project_manager,
twoFactorEnabled: false,
identityProvider: IdentityProvider.email,
objective: Objective.increase_conversion,
notificationSettings: {
alert: {},

View File

@@ -18,10 +18,8 @@ const responseSelection = {
emailVerified: true,
createdAt: true,
updatedAt: true,
role: true,
twoFactorEnabled: true,
identityProvider: true,
objective: true,
notificationSettings: true,
locale: true,
lastLoginAt: true,

View File

@@ -2,7 +2,6 @@ import * as services from "@/lib/utils/services";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { ResourceNotFoundError } from "@formbricks/types/errors";
import {
getEnvironmentIdFromInsightId,
getEnvironmentIdFromResponseId,
getEnvironmentIdFromSegmentId,
getEnvironmentIdFromSurveyId,
@@ -11,9 +10,7 @@ import {
getOrganizationIdFromActionClassId,
getOrganizationIdFromApiKeyId,
getOrganizationIdFromContactId,
getOrganizationIdFromDocumentId,
getOrganizationIdFromEnvironmentId,
getOrganizationIdFromInsightId,
getOrganizationIdFromIntegrationId,
getOrganizationIdFromInviteId,
getOrganizationIdFromLanguageId,
@@ -28,9 +25,7 @@ import {
getProductIdFromContactId,
getProjectIdFromActionClassId,
getProjectIdFromContactId,
getProjectIdFromDocumentId,
getProjectIdFromEnvironmentId,
getProjectIdFromInsightId,
getProjectIdFromIntegrationId,
getProjectIdFromLanguageId,
getProjectIdFromQuotaId,
@@ -58,8 +53,6 @@ vi.mock("@/lib/utils/services", () => ({
getInvite: vi.fn(),
getLanguage: vi.fn(),
getTeam: vi.fn(),
getInsight: vi.fn(),
getDocument: vi.fn(),
getTag: vi.fn(),
}));
@@ -369,46 +362,6 @@ describe("Helper Utilities", () => {
await expect(getOrganizationIdFromTeamId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getOrganizationIdFromInsightId returns organization ID correctly", async () => {
vi.mocked(services.getInsight).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
vi.mocked(services.getProject).mockResolvedValueOnce({
organizationId: "org1",
});
const orgId = await getOrganizationIdFromInsightId("insight1");
expect(orgId).toBe("org1");
});
test("getOrganizationIdFromInsightId throws error when insight not found", async () => {
vi.mocked(services.getInsight).mockResolvedValueOnce(null);
await expect(getOrganizationIdFromInsightId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getOrganizationIdFromDocumentId returns organization ID correctly", async () => {
vi.mocked(services.getDocument).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
vi.mocked(services.getProject).mockResolvedValueOnce({
organizationId: "org1",
});
const orgId = await getOrganizationIdFromDocumentId("doc1");
expect(orgId).toBe("org1");
});
test("getOrganizationIdFromDocumentId throws error when document not found", async () => {
vi.mocked(services.getDocument).mockResolvedValueOnce(null);
await expect(getOrganizationIdFromDocumentId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getOrganizationIdFromQuotaId returns organization ID correctly", async () => {
vi.mocked(services.getQuota).mockResolvedValueOnce({
surveyId: "survey1",
@@ -481,23 +434,6 @@ describe("Helper Utilities", () => {
await expect(getProjectIdFromContactId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getProjectIdFromInsightId returns project ID correctly", async () => {
vi.mocked(services.getInsight).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
const projectId = await getProjectIdFromInsightId("insight1");
expect(projectId).toBe("project1");
});
test("getProjectIdFromInsightId throws error when insight not found", async () => {
vi.mocked(services.getInsight).mockResolvedValueOnce(null);
await expect(getProjectIdFromInsightId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getProjectIdFromSegmentId returns project ID correctly", async () => {
vi.mocked(services.getSegment).mockResolvedValueOnce({
environmentId: "env1",
@@ -600,23 +536,6 @@ describe("Helper Utilities", () => {
await expect(getProductIdFromContactId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getProjectIdFromDocumentId returns project ID correctly", async () => {
vi.mocked(services.getDocument).mockResolvedValueOnce({
environmentId: "env1",
});
vi.mocked(services.getEnvironment).mockResolvedValueOnce({
projectId: "project1",
});
const projectId = await getProjectIdFromDocumentId("doc1");
expect(projectId).toBe("project1");
});
test("getProjectIdFromDocumentId throws error when document not found", async () => {
vi.mocked(services.getDocument).mockResolvedValueOnce(null);
await expect(getProjectIdFromDocumentId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getProjectIdFromIntegrationId returns project ID correctly", async () => {
vi.mocked(services.getIntegration).mockResolvedValueOnce({
environmentId: "env1",
@@ -699,20 +618,6 @@ describe("Helper Utilities", () => {
await expect(getEnvironmentIdFromResponseId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getEnvironmentIdFromInsightId returns environment ID directly", async () => {
vi.mocked(services.getInsight).mockResolvedValueOnce({
environmentId: "env1",
});
const environmentId = await getEnvironmentIdFromInsightId("insight1");
expect(environmentId).toBe("env1");
});
test("getEnvironmentIdFromInsightId throws error when insight not found", async () => {
vi.mocked(services.getInsight).mockResolvedValueOnce(null);
await expect(getEnvironmentIdFromInsightId("nonexistent")).rejects.toThrow(ResourceNotFoundError);
});
test("getEnvironmentIdFromSegmentId returns environment ID directly", async () => {
vi.mocked(services.getSegment).mockResolvedValueOnce({
environmentId: "env1",

View File

@@ -2,9 +2,7 @@ import {
getActionClass,
getApiKey,
getContact,
getDocument,
getEnvironment,
getInsight,
getIntegration,
getInvite,
getLanguage,
@@ -176,24 +174,6 @@ export const getOrganizationIdFromTeamId = async (teamId: string) => {
return team.organizationId;
};
export const getOrganizationIdFromInsightId = async (insightId: string) => {
const insight = await getInsight(insightId);
if (!insight) {
throw new ResourceNotFoundError("insight", insightId);
}
return await getOrganizationIdFromEnvironmentId(insight.environmentId);
};
export const getOrganizationIdFromDocumentId = async (documentId: string) => {
const document = await getDocument(documentId);
if (!document) {
throw new ResourceNotFoundError("document", documentId);
}
return await getOrganizationIdFromEnvironmentId(document.environmentId);
};
export const getOrganizationIdFromQuotaId = async (quotaId: string) => {
const quota = await getQuota(quotaId);
@@ -219,15 +199,6 @@ export const getProjectIdFromSurveyId = async (surveyId: string) => {
return await getProjectIdFromEnvironmentId(survey.environmentId);
};
export const getProjectIdFromInsightId = async (insightId: string) => {
const insight = await getInsight(insightId);
if (!insight) {
throw new ResourceNotFoundError("insight", insightId);
}
return await getProjectIdFromEnvironmentId(insight.environmentId);
};
export const getProjectIdFromSegmentId = async (segmentId: string) => {
const segment = await getSegment(segmentId);
if (!segment) {
@@ -282,15 +253,6 @@ export const getProductIdFromContactId = async (contactId: string) => {
return await getProjectIdFromEnvironmentId(contact.environmentId);
};
export const getProjectIdFromDocumentId = async (documentId: string) => {
const document = await getDocument(documentId);
if (!document) {
throw new ResourceNotFoundError("document", documentId);
}
return await getProjectIdFromEnvironmentId(document.environmentId);
};
export const getProjectIdFromIntegrationId = async (integrationId: string) => {
const integration = await getIntegration(integrationId);
if (!integration) {
@@ -334,15 +296,6 @@ export const getEnvironmentIdFromResponseId = async (responseId: string) => {
return await getEnvironmentIdFromSurveyId(response.surveyId);
};
export const getEnvironmentIdFromInsightId = async (insightId: string) => {
const insight = await getInsight(insightId);
if (!insight) {
throw new ResourceNotFoundError("insight", insightId);
}
return insight.environmentId;
};
export const getEnvironmentIdFromSegmentId = async (segmentId: string) => {
const segment = await getSegment(segmentId);
if (!segment) {

View File

@@ -9,9 +9,7 @@ import {
getActionClass,
getApiKey,
getContact,
getDocument,
getEnvironment,
getInsight,
getIntegration,
getInvite,
getLanguage,
@@ -451,62 +449,6 @@ describe("Service Functions", () => {
});
});
describe("getInsight", () => {
const insightId = "insight123";
test("returns the insight when found", async () => {
const mockInsight = { environmentId: "env123" };
vi.mocked(prisma.insight.findUnique).mockResolvedValue(mockInsight);
const result = await getInsight(insightId);
expect(validateInputs).toHaveBeenCalled();
expect(prisma.insight.findUnique).toHaveBeenCalledWith({
where: { id: insightId },
select: { environmentId: true },
});
expect(result).toEqual(mockInsight);
});
test("throws DatabaseError when database operation fails", async () => {
vi.mocked(prisma.insight.findUnique).mockRejectedValue(
new Prisma.PrismaClientKnownRequestError("Error", {
code: "P2002",
clientVersion: "4.7.0",
})
);
await expect(getInsight(insightId)).rejects.toThrow(DatabaseError);
});
});
describe("getDocument", () => {
const documentId = "doc123";
test("returns the document when found", async () => {
const mockDocument = { environmentId: "env123" };
vi.mocked(prisma.document.findUnique).mockResolvedValue(mockDocument);
const result = await getDocument(documentId);
expect(validateInputs).toHaveBeenCalled();
expect(prisma.document.findUnique).toHaveBeenCalledWith({
where: { id: documentId },
select: { environmentId: true },
});
expect(result).toEqual(mockDocument);
});
test("throws DatabaseError when database operation fails", async () => {
vi.mocked(prisma.document.findUnique).mockRejectedValue(
new Prisma.PrismaClientKnownRequestError("Error", {
code: "P2002",
clientVersion: "4.7.0",
})
);
await expect(getDocument(documentId)).rejects.toThrow(DatabaseError);
});
});
describe("isProjectPartOfOrganization", () => {
const projectId = "proj123";
const organizationId = "org123";

View File

@@ -272,54 +272,6 @@ export const getTeam = reactCache(async (teamId: string): Promise<{ organization
}
});
export const getInsight = reactCache(async (insightId: string): Promise<{ environmentId: string } | null> => {
validateInputs([insightId, ZId]);
try {
const insight = await prisma.insight.findUnique({
where: {
id: insightId,
},
select: {
environmentId: true,
},
});
return insight;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
});
export const getDocument = reactCache(
async (documentId: string): Promise<{ environmentId: string } | null> => {
validateInputs([documentId, ZId]);
try {
const document = await prisma.document.findUnique({
where: {
id: documentId,
},
select: {
environmentId: true,
},
});
return document;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
}
);
export const isProjectPartOfOrganization = async (
organizationId: string,
projectId: string

View File

@@ -98,7 +98,6 @@ const survey: TSurvey = {
singleUse: null,
productOverwrites: null,
pin: null,
verifyEmail: null,
attributeFilters: [],
autoComplete: null,
hiddenFields: { enabled: true },

View File

@@ -34,7 +34,6 @@ export const ZSurveyInput = ZSurveyWithoutQuestionType.pick({
environmentId: true,
questions: true,
endings: true,
thankYouCard: true,
hiddenFields: true,
variables: true,
displayOption: true,
@@ -47,7 +46,6 @@ export const ZSurveyInput = ZSurveyWithoutQuestionType.pick({
isVerifyEmailEnabled: true,
isSingleResponsePerEmailEnabled: true,
inlineTriggers: true,
verifyEmail: true,
displayPercentage: true,
welcomeCard: true,
surveyClosedMessage: true,
@@ -58,7 +56,6 @@ export const ZSurveyInput = ZSurveyWithoutQuestionType.pick({
.partial({
redirectUrl: true,
endings: true,
thankYouCard: true,
variables: true,
recontactDays: true,
displayLimit: true,
@@ -69,7 +66,6 @@ export const ZSurveyInput = ZSurveyWithoutQuestionType.pick({
projectOverwrites: true,
showLanguageSwitch: true,
inlineTriggers: true,
verifyEmail: true,
displayPercentage: true,
})
.openapi({

View File

@@ -9,13 +9,11 @@ export const mockUser: TUser = {
updatedAt: new Date("2024-01-01T00:00:00.000Z"),
twoFactorEnabled: false,
identityProvider: "google",
objective: "improve_user_retention",
notificationSettings: {
alert: {},
unsubscribedOrganizationIds: [],
},
role: "other",
locale: "en-US",
lastLoginAt: new Date("2024-01-01T00:00:00.000Z"),
isActive: true,

View File

@@ -56,10 +56,6 @@ describe("ResponseFeed", () => {
headline: "",
html: "",
},
verifyEmail: {
name: "",
subheading: "",
},
displayLimit: null,
autoComplete: null,
productOverwrites: null,
@@ -69,11 +65,6 @@ describe("ResponseFeed", () => {
hiddenFields: {},
variables: [],
followUps: [],
thankYouCard: {
enabled: false,
headline: "",
subheader: "",
},
delay: 0,
displayPercentage: 100,
surveyClosedMessage: "",

View File

@@ -16,10 +16,8 @@ export const mockUser: TUser = {
twoFactorEnabled: false,
identityProvider: "google",
locale: "en-US",
role: "other",
createdAt: new Date(),
updatedAt: new Date(),
objective: "improve_user_retention",
lastLoginAt: new Date(),
isActive: true,
};

View File

@@ -25,10 +25,8 @@ describe("updateUser", () => {
email: "test@example.com",
createdAt: new Date(),
updatedAt: new Date(),
role: "project_manager",
twoFactorEnabled: false,
identityProvider: "email",
objective: null,
locale: "en-US",
lastLoginAt: new Date(),
isActive: true,
@@ -54,10 +52,8 @@ describe("updateUser", () => {
emailVerified: true,
createdAt: true,
updatedAt: true,
role: true,
twoFactorEnabled: true,
identityProvider: true,
objective: true,
notificationSettings: true,
locale: true,
lastLoginAt: true,

View File

@@ -19,10 +19,8 @@ export const updateUser = async (personId: string, data: TUserUpdateInput): Prom
emailVerified: true,
createdAt: true,
updatedAt: true,
role: true,
twoFactorEnabled: true,
identityProvider: true,
objective: true,
notificationSettings: true,
locale: true,
lastLoginAt: true,

View File

@@ -118,7 +118,6 @@ const mockSurvey = {
status: "inProgress",
questions: [mockQuestion],
languages: [{ code: "en", default: true, enabled: true }],
thankYouCard: { enabled: true },
welcomeCard: { enabled: false },
autoClose: null,
triggers: [],
@@ -135,7 +134,6 @@ const mockSurvey = {
variables: [],
productOverwrites: null,
singleUse: null,
verifyEmail: null,
delay: 0,
displayPercentage: null,
inlineTriggers: null,

View File

@@ -143,7 +143,6 @@ const baseSurvey = {
productOverwrites: null,
singleUse: null,
surveyClosedMessage: null,
verifyEmail: null,
projectOverwrites: null,
hiddenFields: { enabled: false },
} as unknown as TSurvey;

View File

@@ -263,8 +263,6 @@ export const updateSurvey = async (updatedSurvey: TSurvey): Promise<TSurvey> =>
};
}
// TODO: Fix this, this happens because the survey type "web" is no longer in the zod types but its required in the schema for migration
// @ts-expect-error
const modifiedSurvey: TSurvey = {
...prismaSurvey, // Properties from prismaSurvey
displayPercentage: Number(prismaSurvey.displayPercentage) || null,

View File

@@ -218,12 +218,6 @@ const createMockSurvey = (): TSurvey =>
displayOption: "displayOnce",
recontactDays: null,
displayLimit: null,
thankYouCard: {
enabled: true,
title: { default: "Thank you" },
buttonLabel: { default: "Close" },
buttonLink: "",
},
questions: [
{
id: "question1",

View File

@@ -489,7 +489,6 @@ describe("validation.isSurveyValid", () => {
delay: 0,
displayOption: "displayOnce",
displayLimit: null,
thankYouCard: { enabled: true, title: { default: "Thank you" } }, // Minimal for type check
createdAt: new Date(),
updatedAt: new Date(),
segment: null,

View File

@@ -51,7 +51,6 @@ const mockProjectPrisma = {
segment: null,
surveyClosedMessage: null,
singleUseId: null,
verifyEmail: null,
productOverwrites: null,
brandColor: null,
highlightBorderColor: null,

View File

@@ -108,15 +108,8 @@ describe("data", () => {
triggers: [],
segment: null,
followUps: [],
thankYouCard: {
enabled: false,
headline: { default: "Thank you!" },
subheader: { default: "" },
buttonLabel: { default: "Close" },
},
inlineTriggers: [],
segmentId: null,
verifyEmail: null,
};
const mockTransformedSurvey = {
@@ -231,12 +224,6 @@ describe("data", () => {
triggers: [],
segment: null,
followUps: [],
thankYouCard: {
enabled: false,
headline: { default: "Thank you!" },
subheader: { default: "" },
buttonLabel: { default: "Close" },
},
};
vi.mocked(prisma.survey.findUnique).mockResolvedValue(mockSurveyData as any);

View File

@@ -140,12 +140,6 @@ describe("survey link utils", () => {
html: { default: "" },
buttonLabel: { default: "Start" },
},
thankYouCard: {
enabled: true,
headline: { default: "Thank You" },
html: { default: "" },
buttonLabel: { default: "Close" },
},
hiddenFields: {},
languages: {
default: "default",

View File

@@ -119,8 +119,6 @@ export const PreviewSurvey = ({
const darkOverlay = surveyDarkOverlay ?? project.darkOverlay;
const clickOutsideClose = surveyClickOutsideClose ?? project.clickOutsideClose;
const widgetSetupCompleted = appSetupCompleted;
const styling: TSurveyStyling | TProjectStyling = useMemo(() => {
// allow style overwrite is disabled from the project
if (!project.styling.allowStyleOverwrite) {
@@ -211,7 +209,7 @@ export const PreviewSurvey = ({
};
if (!previewType) {
previewType = widgetSetupCompleted ? "modal" : "fullwidth";
previewType = appSetupCompleted ? "modal" : "fullwidth";
if (!questionId) {
return <></>;

View File

@@ -101,9 +101,6 @@ describe("QuestionToggleTable", () => {
welcomeCard: {
enabled: false,
},
thankYouCard: {
enabled: false,
},
displayProgress: false,
progressBar: {
display: false,

View File

@@ -235,10 +235,6 @@ test.describe("Survey Create & Submit Response without logic", async () => {
await page.locator("#questionCard-12").getByRole("button", { name: "Next" }).click();
// loading spinner -> wait for it to disappear
await page.getByTestId("loading-spinner").waitFor({ state: "hidden" });
// Thank You Card
await expect(page.getByText(surveys.createAndSubmit.thankYouCard.headline)).toBeVisible();
await expect(page.getByText(surveys.createAndSubmit.thankYouCard.description)).toBeVisible();
});
});
});
@@ -611,18 +607,16 @@ test.describe("Multi Language Survey Create", async () => {
// Fill Thank you card in german
await page.getByText("Ending card").first().click();
await page.getByPlaceholder("Your question here. Recall").click();
await page
.getByPlaceholder("Your question here. Recall")
.fill(surveys.germanCreate.thankYouCard.headline);
await page.getByPlaceholder("Your question here. Recall").fill(surveys.germanCreate.endingCard.headline);
await page.getByPlaceholder("Your description here. Recall").click();
await page
.getByPlaceholder("Your description here. Recall")
.fill(surveys.germanCreate.thankYouCard.description);
.fill(surveys.germanCreate.endingCard.description);
await page.locator("#showButton").check();
await page.getByPlaceholder("Create your own Survey").click();
await page.getByPlaceholder("Create your own Survey").fill(surveys.germanCreate.thankYouCard.buttonLabel);
await page.getByPlaceholder("Create your own Survey").fill(surveys.germanCreate.endingCard.buttonLabel);
// TODO: @pandeymangg - figure out if this is required
await page.getByRole("button", { name: "Settings", exact: true }).click();
@@ -892,8 +886,8 @@ test.describe("Testing Survey with advanced logic", async () => {
await page.getByTestId("loading-spinner").waitFor({ state: "hidden" });
// Thank You Card
await expect(page.getByText(surveys.createWithLogicAndSubmit.thankYouCard.headline)).toBeVisible();
await expect(page.getByText(surveys.createWithLogicAndSubmit.thankYouCard.description)).toBeVisible();
await expect(page.getByText(surveys.createWithLogicAndSubmit.endingCard.headline)).toBeVisible();
await expect(page.getByText(surveys.createWithLogicAndSubmit.endingCard.description)).toBeVisible();
});
await test.step("Verify Survey Response", async () => {

View File

@@ -354,15 +354,6 @@ export const createSurvey = async (page: Page, params: CreateSurveyParams) => {
await page.getByRole("button", { name: "Add option" }).click();
await page.getByPlaceholder("Option 5").click();
await page.getByPlaceholder("Option 5").fill(params.ranking.choices[4]);
// Thank You Card
await page
.locator("div")
.filter({ hasText: /^Thank you!Ending card$/ })
.nth(1)
.click();
await page.getByLabel("Note*").fill(params.thankYouCard.headline);
await page.locator('input[name="subheader"]').fill(params.thankYouCard.description);
};
export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWithLogicParams) => {
@@ -1012,13 +1003,4 @@ export const createSurveyWithLogic = async (page: Page, params: CreateSurveyWith
await page.getByRole("option", { name: "Multiply *" }).click();
await page.locator("#action-2-value-input").click();
await page.locator("#action-2-value-input").fill("2");
// Thank You Card
await page
.locator("div")
.filter({ hasText: /^Thank you!Ending card$/ })
.nth(1)
.click();
await page.getByLabel("Note*").fill(params.thankYouCard.headline);
await page.locator('input[name="subheader"]').fill(params.thankYouCard.description);
};

View File

@@ -174,10 +174,6 @@ export const surveys = {
question: "What is most important for you in life?",
choices: ["Work", "Money", "Travel", "Family", "Friends"],
},
thankYouCard: {
headline: "This is my Thank You Card Headline!",
description: "This is my Thank you Card Description!",
},
},
createWithLogicAndSubmit: {
welcomeCard: {
@@ -249,9 +245,9 @@ export const surveys = {
question: "This is my Ranking Question",
choices: ["Work", "Money", "Travel", "Family", "Friends"],
},
thankYouCard: {
headline: "This is my Thank You Card Headline!",
description: "This is my Thank you Card Description!",
endingCard: {
headline: "Thank you!",
description: "We appreciate your feedback.",
},
},
germanCreate: {
@@ -326,15 +322,15 @@ export const surveys = {
country: "Adresse",
},
},
ranking: {
question: "Was ist für Sie im Leben am wichtigsten?",
choices: ["Arbeit", "Geld", "Reisen", "Familie", "Freunde"],
},
thankYouCard: {
endingCard: {
headline: "Dies ist meine Dankeskarte Überschrift!", // German translation
description: "Dies ist meine Beschreibung zur Dankeskarte!", // German translation
buttonLabel: "Erstellen Sie Ihre eigene Umfrage",
},
ranking: {
question: "Was ist für Sie im Leben am wichtigsten?",
choices: ["Arbeit", "Geld", "Reisen", "Familie", "Freunde"],
},
},
};

View File

@@ -4067,6 +4067,7 @@
"content": {
"application/json": {
"example": {
"appSetupCompleted": true,
"createdAt": "2024-04-09T04:53:29.577Z",
"id": "clurwouax000azffxt7n5unn3",
"product": {
@@ -4074,8 +4075,7 @@
"name": "My Product"
},
"type": "production",
"updatedAt": "2024-04-09T14:14:49.256Z",
"widgetSetupCompleted": true
"updatedAt": "2024-04-09T14:14:49.256Z"
},
"schema": {
"type": "object"

View File

@@ -3958,19 +3958,6 @@ components:
- type
default: []
description: The endings of the survey
thankYouCard:
type:
- object
- "null"
properties:
enabled:
type: boolean
message:
type: string
required:
- enabled
- message
description: The thank you card of the survey (deprecated)
hiddenFields:
type: object
properties:
@@ -4302,17 +4289,6 @@ components:
isBackButtonHidden:
type: boolean
description: Whether the back button is hidden
verifyEmail:
type: object
properties:
enabled:
type: boolean
message:
type: string
required:
- enabled
- message
description: Email verification configuration (deprecated)
recaptcha:
type:
- object

View File

@@ -161,10 +161,7 @@ Formbricks stores all data in PostgreSQL tables. Here's a comprehensive list of
| ContactAttributeKey | Defines available attribute types for contacts |
| DataMigration | Tracks the status of database schema migrations |
| Display | Records when and to whom surveys were shown |
| Document | Stores processed survey responses for analysis |
| DocumentInsight | Links analyzed documents to derived insights |
| Environment | Manages production/development environments within projects |
| Insight | Contains analyzed patterns and information from responses |
| Integration | Stores configuration for third-party service integrations |
| Invite | Manages pending invitations to join organizations |
| Language | Defines supported languages for multi-lingual surveys |

241
packages/database/README.md Normal file
View File

@@ -0,0 +1,241 @@
# @formbricks/database
The database package for the Formbricks monorepo, providing centralized database schema management, migration handling, and type definitions for the entire platform.
## Overview
This package serves as the central database layer for Formbricks, containing:
- **Prisma Schema**: Complete database schema definition with PostgreSQL support
- **Migration System**: Custom migration management for both schema and data migrations
- **Type Definitions**: Zod schemas and TypeScript types for database models
- **Database Client**: Configured Prisma client with extensions and generators
- **Migration Scripts**: Automated tools for creating and applying migrations
## Package Structure
```
packages/database/
├── src/
│ ├── client.ts # Prisma client configuration
│ ├── index.ts # Package exports
│ └── scripts/ # Migration management scripts
│ ├── apply-migrations.ts
│ ├── create-migration.ts
│ ├── generate-data-migration.ts
│ ├── migration-runner.ts
│ └── create-saml-database.ts
├── migration/ # Custom migrations directory
│ ├── [timestamp_name]/ # Schema migration folder
│ │ └── migration.sql # Schema migration file
│ └── [timestamp_name]/ # Data migration folder
│ └── migration.ts # Data migration file
├── migrations/ # Prisma internal migrations
├── types/ # Custom TypeScript types
├── zod/ # Zod schema definitions
├── schema.prisma # Main Prisma schema file
└── package.json
```
## Migration System
### Key Features
- **Custom Migrations Directory**: Schema and data migrations are managed in the `packages/database/migration` directory
- **Separation of Concerns**: Each migration is classified as either:
- **Schema Migration**: Contains a `migration.sql` file
- **Data Migration**: Contains a `migration.ts` file
- **Single Type per Subdirectory**: A migration subdirectory can only contain one file type—either `migration.sql` or `migration.ts`
- **Custom Naming Convention**: Subdirectories follow the format `timestamp_name_of_the_migration` (e.g., `20241214112456_add_users_table`)
- **Order of Execution**: Migrations are executed sequentially based on their timestamps, enabling precise control over the execution sequence
### Database Tracking
- **Schema Migrations**: Continue to be tracked by Prisma in the `_prisma_migrations` table
- **Data Migrations**: Are tracked in the new `DataMigration` table to avoid reapplying already executed migrations
### Directory Structure Example
```
packages/database/migration/
├── 20241214112456_xm_user_identification/
│ └── migration.sql
├── 20241214113000_xm_user_identification/
│ └── migration.ts
└── 20241215120000_add_new_feature/
└── migration.sql
```
Each subdirectory under `packages/database/migration` represents a single migration and must:
- Have a **14-digit UTC timestamp** followed by an underscore and the migration name (similar to Prisma)
- Contain only one file, either `migration.sql` (for schema migrations) or `migration.ts` (for data migrations)
## Scripts and Commands
### Root Level Commands
Run these commands from the root directory of the Formbricks monorepo:
- **`pnpm fb-migrate-dev`**: Create and apply schema migrations
- Prompts for migration name
- Generates new `migration.sql` in the custom directory
- Copies migration to Prisma's internal directory
- Applies all pending migrations to the database
### Package Level Commands
Run these commands from the `packages/database` directory:
- **`pnpm generate-data-migration`**: Create data migrations
- Prompts for data migration name
- Creates new subdirectory with appropriate timestamp
- Generates `migration.ts` file with pre-configured ID and name
- **Note**: Only use Prisma raw queries in data migrations for better performance and to avoid type errors
### Available Scripts
```json
{
"build": "pnpm generate && vite build",
"create-migration": "Create new schema migration",
"db:migrate:deploy": "Apply migrations in production",
"db:migrate:dev": "Apply migrations in development",
"db:push": "prisma db push --accept-data-loss",
"db:setup": "pnpm db:migrate:dev && pnpm db:create-saml-database:dev",
"dev": "vite build --watch",
"generate": "prisma generate",
"generate-data-migration": "Create new data migration"
}
```
## Migration Workflow
### Adding a Schema Migration
1. Modify your Prisma schema in `schema.prisma`
2. Run `pnpm fb-migrate-dev` from the root of the monorepo
3. Follow the prompts to name your migration
4. The script automatically:
- Generates the new `migration.sql` file in the custom directory
- Copies the file to Prisma's internal directory
- Applies the migration to the database
### Adding a Data Migration
1. Navigate to the `packages/database` directory
2. Run `pnpm generate-data-migration`
3. Follow the prompts to name your migration
4. Implement the required data changes in the generated `migration.ts` file
5. Use only Prisma raw queries for optimal performance
### Example Data Migration Structure
```typescript
import { createId } from "@paralleldrive/cuid2";
import { Prisma } from "@prisma/client";
import { logger } from "@formbricks/logger";
import type { MigrationScript } from "../../src/scripts/migration-runner";
export const myDataMigration: MigrationScript = {
type: "data",
id: "unique_migration_id",
name: "20241214113000_my_data_migration",
run: async ({ tx }) => {
// Use raw SQL queries for data transformations
const result = await tx.$queryRaw`
UPDATE "MyTable" SET "newField" = 'defaultValue' WHERE "newField" IS NULL
`;
logger.info(`Updated ${result} records`);
},
};
```
## Database Schema
The package uses PostgreSQL with the following key features:
- **Extensions**: pgvector for vector operations
- **Generators**:
- Prisma Client with PostgreSQL extensions
- JSON types generator for enhanced type safety
- **Models**: Comprehensive schema covering users, organizations, surveys, responses, webhooks, and more
### Key Models
- **User**: User accounts with authentication and profile data
- **Organization**: Multi-tenant organization structure
- **Project**: Project-level configuration and settings
- **Survey**: Survey definitions with advanced targeting and styling
- **Response**: Survey response data with metadata
- **Contact**: Contact management and attributes
- **Webhook**: Event-driven integrations
- **ApiKey**: API authentication and access control
## Type Definitions
### Zod Schemas
Located in the `zod/` directory, providing runtime validation for:
- API keys
- Contacts and contact attributes
- Organizations and teams
- Surveys and responses
- Webhooks and integrations
- User management
### TypeScript Types
Custom types in the `types/` directory for:
- Error handling
- Survey follow-up logic
- Complex data structures
## Development
### Prerequisites
- PostgreSQL database
- Redis (for caching)
- Node.js and pnpm
### Setup
1. Ensure database is running: `pnpm db:up` (from root)
2. Install dependencies: `pnpm install`
3. Generate Prisma client: `pnpm generate`
4. Apply migrations: `pnpm db:setup`
### Building
```bash
# Development build with watch
pnpm dev
# Production build
pnpm build
```
## Key Benefits
- **Unified Management**: Schema and data migrations are managed together in a single directory
- **Controlled Execution**: Timestamp-based sorting ensures migrations run in the desired sequence
- **Automation**: Simplifies the process of creating, copying, and applying migrations with custom scripts
- **Tracking**: Separate tracking for schema and data migrations prevents duplicate executions
- **Type Safety**: Comprehensive Zod schemas and TypeScript types for all database operations
- **Performance**: Optimized queries and proper indexing for production workloads
## Contributing
When making changes to the database schema:
1. Always create migrations for schema changes
2. Use data migrations for data transformations
3. Follow the naming conventions for migration directories
4. Test migrations thoroughly in development before applying to production
5. Document any breaking changes or special considerations
For more information about the Formbricks project structure, see the main repository README.

View File

@@ -12,7 +12,7 @@ CREATE TABLE "SurveyQuota" (
"surveyId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"limit" INTEGER NOT NULL,
"logic" JSONB NOT NULL,
"logic" JSONB NOT NULL DEFAULT '{}'::jsonb,
"action" "SurveyQuotaAction" NOT NULL,
"endingCardId" TEXT,
"countPartialSubmissions" BOOLEAN NOT NULL DEFAULT false,

View File

@@ -0,0 +1,107 @@
/*
Warnings:
- The values [web,website] on the enum `SurveyType` will be removed. If these variants are still used in the database, this will fail.
- You are about to drop the column `responseId` on the `Display` table. All the data in the column will be lost.
- You are about to drop the column `status` on the `Display` table. All the data in the column will be lost.
- You are about to drop the column `widgetSetupCompleted` on the `Environment` table. All the data in the column will be lost.
- You are about to drop the column `deprecatedRole` on the `Invite` table. All the data in the column will be lost.
- You are about to drop the column `deprecatedRole` on the `Membership` table. All the data in the column will be lost.
- You are about to drop the column `brandColor` on the `Project` table. All the data in the column will be lost.
- You are about to drop the column `highlightBorderColor` on the `Project` table. All the data in the column will be lost.
- You are about to drop the column `thankYouCard` on the `Survey` table. All the data in the column will be lost.
- You are about to drop the column `verifyEmail` on the `Survey` table. All the data in the column will be lost.
- You are about to drop the column `objective` on the `User` table. All the data in the column will be lost.
- You are about to drop the column `role` on the `User` table. All the data in the column will be lost.
- You are about to drop the `Document` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `DocumentInsight` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `Insight` table. If the table is not empty, all the data it contains will be lost.
*/
-- AlterEnum
BEGIN;
CREATE TYPE "public"."SurveyType_new" AS ENUM ('link', 'app');
ALTER TABLE "public"."Survey" ALTER COLUMN "type" DROP DEFAULT;
ALTER TABLE "public"."Survey" ALTER COLUMN "type" TYPE "public"."SurveyType_new" USING ("type"::text::"public"."SurveyType_new");
ALTER TYPE "public"."SurveyType" RENAME TO "SurveyType_old";
ALTER TYPE "public"."SurveyType_new" RENAME TO "SurveyType";
DROP TYPE "public"."SurveyType_old";
ALTER TABLE "public"."Survey" ALTER COLUMN "type" SET DEFAULT 'app';
COMMIT;
-- DropForeignKey
ALTER TABLE "public"."Document" DROP CONSTRAINT "Document_environmentId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Document" DROP CONSTRAINT "Document_responseId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Document" DROP CONSTRAINT "Document_surveyId_fkey";
-- DropForeignKey
ALTER TABLE "public"."DocumentInsight" DROP CONSTRAINT "DocumentInsight_documentId_fkey";
-- DropForeignKey
ALTER TABLE "public"."DocumentInsight" DROP CONSTRAINT "DocumentInsight_insightId_fkey";
-- DropForeignKey
ALTER TABLE "public"."Insight" DROP CONSTRAINT "Insight_environmentId_fkey";
-- DropIndex
DROP INDEX "public"."Display_responseId_key";
-- AlterTable
ALTER TABLE "public"."Display" DROP COLUMN "responseId",
DROP COLUMN "status";
-- AlterTable
ALTER TABLE "public"."Environment" DROP COLUMN "widgetSetupCompleted";
-- AlterTable
ALTER TABLE "public"."Invite" DROP COLUMN "deprecatedRole";
-- AlterTable
ALTER TABLE "public"."Membership" DROP COLUMN "deprecatedRole";
-- AlterTable
ALTER TABLE "public"."Project" DROP COLUMN "brandColor",
DROP COLUMN "highlightBorderColor";
-- AlterTable
ALTER TABLE "public"."Survey" DROP COLUMN "thankYouCard",
DROP COLUMN "verifyEmail",
ALTER COLUMN "type" SET DEFAULT 'app';
-- AlterTable
ALTER TABLE "public"."User" DROP COLUMN "objective",
DROP COLUMN "role";
-- DropTable
DROP TABLE "public"."Document";
-- DropTable
DROP TABLE "public"."DocumentInsight";
-- DropTable
DROP TABLE "public"."Insight";
-- DropEnum
DROP TYPE "public"."DisplayStatus";
-- DropEnum
DROP TYPE "public"."InsightCategory";
-- DropEnum
DROP TYPE "public"."Intention";
-- DropEnum
DROP TYPE "public"."MembershipRole";
-- DropEnum
DROP TYPE "public"."Objective";
-- DropEnum
DROP TYPE "public"."Role";
-- DropEnum
DROP TYPE "public"."Sentiment";

View File

@@ -165,7 +165,6 @@ model Response {
// singleUseId, used to prevent multiple responses
singleUseId String?
language String?
documents Document[]
displayId String? @unique
display Display? @relation(fields: [displayId], references: [id])
@@ -218,30 +217,22 @@ enum SurveyStatus {
completed
}
enum DisplayStatus {
seen
responded
}
/// Records when a survey is shown to a user.
/// Tracks survey display history and response status.
///
/// @property id - Unique identifier for the display event
/// @property survey - The survey that was displayed
/// @property contact - The contact who saw the survey
/// @property status - Whether the survey was just seen or responded to
/// @property response - The associated response if one exists
model Display {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
survey Survey @relation(fields: [surveyId], references: [id], onDelete: Cascade)
surveyId String
contact Contact? @relation(fields: [contactId], references: [id], onDelete: Cascade)
contactId String?
responseId String? @unique //deprecated
status DisplayStatus?
response Response?
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
survey Survey @relation(fields: [surveyId], references: [id], onDelete: Cascade)
surveyId String
contact Contact? @relation(fields: [contactId], references: [id], onDelete: Cascade)
contactId String?
response Response?
@@index([surveyId])
@@index([contactId, createdAt])
@@ -311,8 +302,6 @@ model SurveyAttributeFilter {
enum SurveyType {
link
web
website
app
}
@@ -341,7 +330,7 @@ model Survey {
updatedAt DateTime @updatedAt @map(name: "updated_at")
name String
redirectUrl String?
type SurveyType @default(web)
type SurveyType @default(app)
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
environmentId String
creator User? @relation(fields: [createdBy], references: [id])
@@ -353,7 +342,6 @@ model Survey {
questions Json @default("[]")
/// [SurveyEnding]
endings Json[] @default([])
thankYouCard Json? //deprecated
/// [SurveyHiddenFields]
hiddenFields Json @default("{\"enabled\": false}")
/// [SurveyVariables]
@@ -385,8 +373,6 @@ model Survey {
/// [SurveySingleUse]
singleUse Json? @default("{\"enabled\": false, \"isEncrypted\": true}")
/// [SurveyVerifyEmail]
verifyEmail Json? // deprecated
isVerifyEmailEnabled Boolean @default(false)
isSingleResponsePerEmailEnabled Boolean @default(false)
isBackButtonHidden Boolean @default(false)
@@ -394,7 +380,6 @@ model Survey {
displayPercentage Decimal?
languages SurveyLanguage[]
showLanguageSwitch Boolean?
documents Document[]
followUps SurveyFollowUp[]
/// [SurveyRecaptcha]
recaptcha Json? @default("{\"enabled\": false, \"threshold\":0.1}")
@@ -565,31 +550,27 @@ model DataMigration {
/// @property id - Unique identifier for the environment
/// @property type - Either 'production' or 'development'
/// @property project - Reference to parent project
/// @property widgetSetupCompleted - Tracks initial widget setup status
/// @property surveys - Collection of surveys in this environment
/// @property contacts - Collection of contacts/users tracked
/// @property actionClasses - Defined actions that can trigger surveys
/// @property attributeKeys - Custom attributes configuration
model Environment {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
type EnvironmentType
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
projectId String
widgetSetupCompleted Boolean @default(false)
appSetupCompleted Boolean @default(false)
surveys Survey[]
contacts Contact[]
actionClasses ActionClass[]
attributeKeys ContactAttributeKey[]
webhooks Webhook[]
tags Tag[]
segments Segment[]
integration Integration[]
documents Document[]
insights Insight[]
ApiKeyEnvironment ApiKeyEnvironment[]
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
type EnvironmentType
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
projectId String
appSetupCompleted Boolean @default(false)
surveys Survey[]
contacts Contact[]
actionClasses ActionClass[]
attributeKeys ContactAttributeKey[]
webhooks Webhook[]
tags Tag[]
segments Segment[]
integration Integration[]
ApiKeyEnvironment ApiKeyEnvironment[]
@@index([projectId])
}
@@ -614,29 +595,27 @@ enum WidgetPlacement {
/// @property recontactDays - Default recontact delay for surveys
/// @property placement - Default widget placement for in-app surveys
model Project {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
name String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
organizationId String
environments Environment[]
brandColor String? // deprecated; use styling.brandColor instead
highlightBorderColor String? // deprecated
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
name String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
organizationId String
environments Environment[]
/// [Styling]
styling Json @default("{\"allowStyleOverwrite\":true}")
styling Json @default("{\"allowStyleOverwrite\":true}")
/// [ProjectConfig]
config Json @default("{}")
recontactDays Int @default(7)
linkSurveyBranding Boolean @default(true) // Determines if the survey branding should be displayed in link surveys
inAppSurveyBranding Boolean @default(true) // Determines if the survey branding should be displayed in in-app surveys
placement WidgetPlacement @default(bottomRight)
clickOutsideClose Boolean @default(true)
darkOverlay Boolean @default(false)
languages Language[]
config Json @default("{}")
recontactDays Int @default(7)
linkSurveyBranding Boolean @default(true) // Determines if the survey branding should be displayed in link surveys
inAppSurveyBranding Boolean @default(true) // Determines if the survey branding should be displayed in in-app surveys
placement WidgetPlacement @default(bottomRight)
clickOutsideClose Boolean @default(true)
darkOverlay Boolean @default(false)
languages Language[]
/// [Logo]
logo Json?
projectTeams ProjectTeam[]
logo Json?
projectTeams ProjectTeam[]
@@unique([organizationId, name])
@@index([organizationId])
@@ -677,14 +656,6 @@ enum OrganizationRole {
billing
}
enum MembershipRole {
owner
admin
editor
developer
viewer
}
/// Links users to organizations with specific roles.
/// Manages organization membership and permissions.
/// Core model for managing user access within organizations.
@@ -699,7 +670,6 @@ model Membership {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
accepted Boolean @default(false)
deprecatedRole MembershipRole? //deprecated
role OrganizationRole @default(member)
@@id([userId, organizationId])
@@ -732,7 +702,6 @@ model Invite {
acceptorId String?
createdAt DateTime @default(now())
expiresAt DateTime
deprecatedRole MembershipRole? //deprecated
role OrganizationRole @default(member)
teamIds String[] @default([])
@@ -835,39 +804,12 @@ model Account {
@@index([userId])
}
enum Role {
project_manager
engineer
founder
marketing_specialist
other
}
enum Objective {
increase_conversion
improve_user_retention
increase_user_adoption
sharpen_marketing_messaging
support_sales
other
}
enum Intention {
survey_user_segments
survey_at_specific_point_in_user_journey
enrich_customer_profiles
collect_all_user_feedback_on_one_platform
other
}
/// Represents a user in the Formbricks system.
/// Central model for user authentication and profile management.
///
/// @property id - Unique identifier for the user
/// @property name - Display name of the user
/// @property email - User's email address
/// @property role - User's professional role
/// @property objective - User's main goal with Formbricks
/// @property twoFactorEnabled - Whether 2FA is active
/// @property memberships - Organizations the user belongs to
/// @property notificationSettings - User's notification preferences
@@ -890,8 +832,6 @@ model User {
groupId String?
invitesCreated Invite[] @relation("inviteCreatedBy")
invitesAccepted Invite[] @relation("inviteAcceptedBy")
role Role?
objective Objective?
/// [UserNotificationSettings]
notificationSettings Json @default("{}")
/// [Locale]
@@ -969,85 +909,6 @@ model SurveyLanguage {
@@index([languageId])
}
enum InsightCategory {
featureRequest
complaint
praise
other
}
/// Stores analyzed insights from survey responses.
/// Used for tracking patterns and extracting meaningful information.
///
/// @property id - Unique identifier for the insight
/// @property category - Type of insight (feature request, complaint, etc.)
/// @property title - Summary of the insight
/// @property description - Detailed explanation
/// @property vector - Embedding vector for similarity search
model Insight {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
environmentId String
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
category InsightCategory
title String
description String
vector Unsupported("vector(512)")?
documentInsights DocumentInsight[]
}
/// Links insights to source documents.
/// Enables tracing insights back to original responses.
///
/// @property document - The source document
/// @property insight - The derived insight
model DocumentInsight {
documentId String
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
insightId String
insight Insight @relation(fields: [insightId], references: [id], onDelete: Cascade)
@@id([documentId, insightId])
@@index([insightId])
}
enum Sentiment {
positive
negative
neutral
}
/// Represents a processed text document from survey responses.
/// Used for analysis and insight generation.
///
/// @property id - Unique identifier for the document
/// @property survey - The associated survey
/// @property response - The source response
/// @property sentiment - Analyzed sentiment (positive, negative, neutral)
/// @property text - The document content
/// @property vector - Embedding vector for similarity search
model Document {
id String @id @default(cuid())
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
environmentId String
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
surveyId String?
survey Survey? @relation(fields: [surveyId], references: [id], onDelete: Cascade)
responseId String?
response Response? @relation(fields: [responseId], references: [id], onDelete: Cascade)
questionId String?
sentiment Sentiment
isSpam Boolean
text String
vector Unsupported("vector(512)")?
documentInsights DocumentInsight[]
@@unique([responseId, questionId])
@@index([createdAt])
}
/// Represents a team within an organization.
/// Enables group-based access control and collaboration.
///

View File

@@ -99,15 +99,6 @@ const ZSurveyBase = z.object({
endings: z.array(ZSurveyEnding).default([]).openapi({
description: "The endings of the survey",
}),
thankYouCard: z
.object({
enabled: z.boolean(),
message: z.string(),
})
.nullable()
.openapi({
description: "The thank you card of the survey (deprecated)",
}),
hiddenFields: z
.object({
enabled: z.boolean(),
@@ -202,14 +193,6 @@ const ZSurveyBase = z.object({
isBackButtonHidden: z.boolean().openapi({
description: "Whether the back button is hidden",
}),
verifyEmail: z
.object({
enabled: z.boolean(),
message: z.string(),
})
.openapi({
description: "Email verification configuration (deprecated)",
}),
recaptcha: ZSurveyRecaptcha.openapi({
description: "Google reCAPTCHA configuration",
}),

View File

@@ -210,8 +210,6 @@ const mockSurvey = {
isSingleUse: false, // Explicitly set isSingleUse
recaptcha: { enabled: false },
autoClose: null,
thankYouCard: null,
verifyEmail: null,
triggers: [],
redirectUrl: "",
surveyClosedMessage: { default: "" },

View File

@@ -6,7 +6,6 @@ export const ZDisplay = z.object({
updatedAt: z.date(),
contactId: z.string().cuid().nullable(),
surveyId: z.string().cuid(),
status: z.enum(["seen", "responded"]).nullable(),
});
export type TDisplay = z.infer<typeof ZDisplay>;

View File

@@ -7,7 +7,6 @@ import {
ZSurveyStyling,
ZSurveyWelcomeCard,
} from "./surveys/types";
import { ZUserObjective } from "./user";
export const ZTemplateRole = z.enum([
"productManager",
@@ -25,7 +24,6 @@ export const ZTemplate = z.object({
role: ZTemplateRole.optional(),
channels: z.array(z.enum(["link", "app", "website"])).optional(),
industries: z.array(z.enum(["eCommerce", "saas", "other"])).optional(),
objectives: z.array(ZUserObjective).optional(),
preset: z.object({
name: z.string(),
welcomeCard: ZSurveyWelcomeCard,

View File

@@ -1,7 +1,5 @@
import { z } from "zod";
const ZRole = z.enum(["project_manager", "engineer", "founder", "marketing_specialist", "other"]);
export const ZUserLocale = z.enum([
"en-US",
"de-DE",
@@ -15,16 +13,6 @@ export const ZUserLocale = z.enum([
]);
export type TUserLocale = z.infer<typeof ZUserLocale>;
export const ZUserObjective = z.enum([
"increase_conversion",
"improve_user_retention",
"increase_user_adoption",
"sharpen_marketing_messaging",
"support_sales",
"other",
]);
export type TUserObjective = z.infer<typeof ZUserObjective>;
export const ZUserNotificationSettings = z.object({
alert: z.record(z.boolean()),
@@ -62,8 +50,6 @@ export const ZUser = z.object({
identityProvider: ZUserIdentityProvider,
createdAt: z.date(),
updatedAt: z.date(),
role: ZRole.nullable(),
objective: ZUserObjective.nullable(),
notificationSettings: ZUserNotificationSettings,
locale: ZUserLocale,
lastLoginAt: z.date().nullable(),
@@ -77,8 +63,6 @@ export const ZUserUpdateInput = z.object({
email: ZUserEmail.optional(),
emailVerified: z.date().nullish(),
password: ZUserPassword.optional(),
role: ZRole.optional(),
objective: ZUserObjective.nullish(),
notificationSettings: ZUserNotificationSettings.optional(),
locale: ZUserLocale.optional(),
lastLoginAt: z.date().nullish(),
@@ -92,8 +76,6 @@ export const ZUserCreateInput = z.object({
email: ZUserEmail,
password: ZUserPassword.optional(),
emailVerified: z.date().optional(),
role: ZRole.optional(),
objective: ZUserObjective.nullish(),
identityProvider: ZUserIdentityProvider.optional(),
identityProviderAccountId: z.string().optional(),
locale: ZUserLocale.optional(),