diff --git a/apps/web/app/api/v1/users/route.ts b/apps/web/app/api/v1/users/route.ts index 73f3b25ccd..c88984d329 100644 --- a/apps/web/app/api/v1/users/route.ts +++ b/apps/web/app/api/v1/users/route.ts @@ -101,7 +101,13 @@ export const POST = async (request: Request) => { if (isMultiOrgEnabled) { const organization = await createOrganization({ name: user.name + "'s Organization" }); await createMembership(organization.id, user.id, { role: "owner", accepted: true }); - const product = await createProduct(organization.id, { name: "My Product" }); + const product = await createProduct(organization.id, { + name: "My Product", + config: { + channel: null, + industry: null, + }, + }); const updatedNotificationSettings = { ...user.notificationSettings, diff --git a/packages/database/data-migrations/20240612115151_adds_product_config/data-migration.ts b/packages/database/data-migrations/20240612115151_adds_product_config/data-migration.ts new file mode 100644 index 0000000000..b97426c4d3 --- /dev/null +++ b/packages/database/data-migrations/20240612115151_adds_product_config/data-migration.ts @@ -0,0 +1,109 @@ +import { PrismaClient, SurveyType } from "@prisma/client"; +import { TProductConfigChannel } from "@formbricks/types/product"; + +const prisma = new PrismaClient(); + +const main = async () => { + await prisma.$transaction( + async (tx) => { + const startTime = Date.now(); + console.log("Starting data migration..."); + + // Fetch all products + const products = await tx.product.findMany({ + include: { + environments: { + select: { + surveys: { + select: { + type: true, + }, + }, + }, + }, + }, + }); + + console.log(`Found ${products.length} products to migrate...\n`); + + const channelStatusCounts = { + [SurveyType.app]: 0, + [SurveyType.link]: 0, + [SurveyType.website]: 0, + null: 0, + }; + + const updatePromises = products.map((product) => { + const surveyTypes = new Set(); + + // Collect all unique survey types for the product + for (const environment of product.environments) { + for (const survey of environment.surveys) { + surveyTypes.add(survey.type); + } + } + + // Determine the channel based on the survey types, default to null + let channel: TProductConfigChannel = null; + + if (surveyTypes.size === 0 || surveyTypes.size === 3) { + // if there are no surveys or all 3 types of surveys (website, app, and link) are present, set channel to null + channel = null; + } else if (surveyTypes.size === 1) { + // if there is only one type of survey, set channel to that type + const type = Array.from(surveyTypes)[0]; + if (type === SurveyType.web) { + // if the survey type is web, set channel to null, since web is a legacy type and will be removed + channel = null; + } else { + // if the survey type is not web, set channel to that type + channel = type; + } + } else if (surveyTypes.has(SurveyType.link) && surveyTypes.has(SurveyType.app)) { + // if both link and app surveys are present, set channel to app + channel = SurveyType.app; + } else if (surveyTypes.has(SurveyType.link) && surveyTypes.has(SurveyType.website)) { + // if both link and website surveys are present, set channel to website + channel = SurveyType.website; + } + + // Increment the count for the determined channel + channelStatusCounts[channel ?? "null"]++; + + // Update the product with the determined channel and set industry to null + return tx.product.update({ + where: { id: product.id }, + data: { + config: { + channel, + industry: null, + }, + }, + }); + }); + + await Promise.all(updatePromises); + + console.log( + `Channel status counts: ${Object.entries(channelStatusCounts).map( + ([channel, count]) => `\n${channel}: ${count}` + )}\n` + ); + + const endTime = Date.now(); + console.log(`Data migration completed. Total time: ${(endTime - startTime) / 1000}s`); + }, + { + timeout: 180000, // 3 minutes + } + ); +}; + +main() + .catch((e: Error) => { + console.error("Error during migration: ", e.message); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/packages/database/jsonTypes.ts b/packages/database/jsonTypes.ts index 727cf28b0b..331c5db9dd 100644 --- a/packages/database/jsonTypes.ts +++ b/packages/database/jsonTypes.ts @@ -1,7 +1,7 @@ import { TActionClassNoCodeConfig } from "@formbricks/types/actionClasses"; import { TIntegrationConfig } from "@formbricks/types/integration"; import { TOrganizationBilling } from "@formbricks/types/organizations"; -import { TProductStyling } from "@formbricks/types/product"; +import { TProductConfig, TProductStyling } from "@formbricks/types/product"; import { TResponseData, TResponseMeta, TResponsePersonAttributes } from "@formbricks/types/responses"; import { TBaseFilters } from "@formbricks/types/segment"; import { @@ -22,6 +22,7 @@ declare global { export type ActionProperties = { [key: string]: string }; export type ActionClassNoCodeConfig = TActionClassNoCodeConfig; export type IntegrationConfig = TIntegrationConfig; + export type ProductConfig = TProductConfig; export type ResponseData = TResponseData; export type ResponseMeta = TResponseMeta; export type ResponsePersonAttributes = TResponsePersonAttributes; diff --git a/packages/database/migrations/20240612115151_adds_product_config/migration.sql b/packages/database/migrations/20240612115151_adds_product_config/migration.sql new file mode 100644 index 0000000000..d30aa1b737 --- /dev/null +++ b/packages/database/migrations/20240612115151_adds_product_config/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Product" ADD COLUMN "config" JSONB NOT NULL DEFAULT '{}'; diff --git a/packages/database/package.json b/packages/database/package.json index d9d9bc299a..e7a53b8814 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -34,7 +34,8 @@ "data-migration:mls-welcomeCard-fix": "ts-node ./data-migrations/20240318050527_add_languages_and_survey_languages/data-migration-welcomeCard-fix.ts", "data-migration:v2.0": "pnpm data-migration:mls && pnpm data-migration:styling && pnpm data-migration:styling-fix && pnpm data-migration:website-surveys && pnpm data-migration:userId && pnpm data-migration:mls-welcomeCard-fix && pnpm data-migration:refactor-actions", "data-migration:extended-noCodeActions": "ts-node ./data-migrations/20240524053239_extends_no_code_action_schema/data-migration.ts", - "data-migration:v2.1": "pnpm data-migration:extended-noCodeActions" + "data-migration:v2.1": "pnpm data-migration:extended-noCodeActions", + "data-migration:product-config": "ts-node ./data-migrations/20240612115151_adds_product_config/data-migration.ts" }, "dependencies": { "@prisma/client": "^5.14.0", diff --git a/packages/database/schema.prisma b/packages/database/schema.prisma index bfe0f22cc3..0c1987fd84 100644 --- a/packages/database/schema.prisma +++ b/packages/database/schema.prisma @@ -433,6 +433,9 @@ model Product { /// @zod.custom(imports.ZProductStyling) /// [Styling] styling Json @default("{\"allowStyleOverwrite\":true}") + /// @zod.custom(imports.ZProductConfig) + /// [ProductConfig] + 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 diff --git a/packages/lib/product/service.ts b/packages/lib/product/service.ts index 787fab9b70..4f82e4dd36 100644 --- a/packages/lib/product/service.ts +++ b/packages/lib/product/service.ts @@ -25,6 +25,7 @@ const selectProduct = { recontactDays: true, linkSurveyBranding: true, inAppSurveyBranding: true, + config: true, placement: true, clickOutsideClose: true, darkOverlay: true, diff --git a/packages/types/product.ts b/packages/types/product.ts index 032522ef56..f0efa7bca2 100644 --- a/packages/types/product.ts +++ b/packages/types/product.ts @@ -9,6 +9,16 @@ export const ZProductStyling = ZBaseStyling.extend({ export type TProductStyling = z.infer; +export const ZProductConfigChannel = z.enum(["link", "app", "website"]).nullable(); +export type TProductConfigChannel = z.infer; + +export const ZProductConfig = z.object({ + channel: ZProductConfigChannel, + industry: z.enum(["eCommerce", "saas"]).nullable(), +}); + +export type TProductConfig = z.infer; + export const ZLanguage = z.object({ id: z.string().cuid2(), createdAt: z.date(), @@ -50,6 +60,7 @@ export const ZProduct = z.object({ .max(365, { message: "Must be less than 365" }), inAppSurveyBranding: z.boolean(), linkSurveyBranding: z.boolean(), + config: ZProductConfig, placement: ZPlacement, clickOutsideClose: z.boolean(), darkOverlay: z.boolean(), @@ -70,6 +81,7 @@ export const ZProductUpdateInput = z.object({ recontactDays: z.number().int().optional(), inAppSurveyBranding: z.boolean().optional(), linkSurveyBranding: z.boolean().optional(), + config: ZProductConfig.optional(), placement: ZPlacement.optional(), clickOutsideClose: z.boolean().optional(), darkOverlay: z.boolean().optional(),