diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 1f0bb5cfcc..823ec92c00 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -60,7 +60,8 @@ jobs: - name: Apply Prisma Migrations run: | - pnpm prisma migrate deploy + # pnpm prisma migrate deploy + pnpm db:migrate:dev - name: Run App run: | diff --git a/.gitignore b/.gitignore index 69f8b1eeaf..d2baa57b3a 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,5 @@ packages/lib/uploads # js compiled assets apps/web/public/js + +packages/database/migrations \ No newline at end of file diff --git a/apps/docs/app/self-hosting/migration-guide/page.mdx b/apps/docs/app/self-hosting/migration-guide/page.mdx index 9bda1f5cec..12a130f096 100644 --- a/apps/docs/app/self-hosting/migration-guide/page.mdx +++ b/apps/docs/app/self-hosting/migration-guide/page.mdx @@ -8,6 +8,96 @@ export const metadata = { # Migration Guide +## v3.0 + + + **Don't upgrade to 3.0 if you need SSO and user identification** + +With Formbricks 3.0, we're making some strategic changes to ensure long-term sustainability while keeping our core commitment to open source. While the Community Edition got more powerful, we're moving some advanced features to the Enterprise Edition. + +If you update to 3.0 and run the data migration, there is no way back to 2.7.2 - if you need to use SSO or one of the other previously free features, either stick with 2.7.x or reach out for a custom quote. + + + + + This major release introduces a new improved approach for data migrations. If you are on a version older + than v2.7, you need to migrate step-by-step through the earlier versions first (e.g. 2.4 → 2.5 → 2.6 → 2.7). + After you have reached v2.7, you can upgrade directly to any v3.x and future release without performing each + intermediate migration. + + +### Steps to Migrate + +This guide is for users who are self-hosting Formbricks using our one-click setup. If you are using a different setup, you might adjust the commands accordingly. + +To run all these steps, please navigate to the `formbricks` folder where your `docker-compose.yml` file is located. + +1. **Backup your Database**: This is a crucial step. Please make sure to backup your database before proceeding with the upgrade. You can use the following command to backup your database: + + + + +```bash +docker exec formbricks-postgres-1 pg_dump -Fc -U postgres -d formbricks > formbricks_pre_v3.0_$(date +%Y%m%d_%H%M%S).dump +``` + + + + + + If you run into “No such container”, use `docker ps` to find your container name, e.g. + `formbricks_postgres_1`. + + + + If you prefer storing the backup as an `*.sql` file remove the `-Fc` (custom format) option. In case of a + restore scenario you will need to use `psql` then with an empty `formbricks` database. + + +2. Pull the latest version of Formbricks: + + + + +```bash +docker compose pull +``` + + + + +3. Stop the running Formbricks instance & remove the related containers: + + + + +```bash +docker compose down +``` + + + + +4. Restarting the containers with the latest version of Formbricks: + + + + +```bash +docker compose up -d +``` + + + + +When you start the latest version of Formbricks, it will automatically detect and run any necessary data migrations during startup. There is no need to run any manual migration steps or pull any separate migration images. + +5. Access your updated instance + +Once the containers are up and running, simply navigate to the same URL as before to access your fully migrated Formbricks instance. + +That’s it! The new workflow ensures that your Formbricks instance will always remain up-to-date with the latest schema changes as soon as you run the updated container. + ## v2.7 diff --git a/apps/docs/package.json b/apps/docs/package.json index dc00fb6474..d66baf252d 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "clean": "rimraf .turbo node_modules .next", - "dev": "next dev --turbopack --port 3001", + "dev": "next dev --port 3001", "build": "next build", "start": "next start", "lint": "next lint", diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index 5f7aecc803..188f7710a2 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22-alpine AS base +FROM node:22-alpine3.20 AS base # ## step 1: Prune monorepo @@ -78,12 +78,21 @@ COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/static ./apps/we COPY --from=installer --chown=nextjs:nextjs /app/apps/web/public ./apps/web/public COPY --from=installer --chown=nextjs:nextjs /app/packages/database/schema.prisma ./packages/database/schema.prisma COPY --from=installer --chown=nextjs:nextjs /app/packages/database/package.json ./packages/database/package.json -COPY --from=installer --chown=nextjs:nextjs /app/packages/database/migrations ./packages/database/migrations +COPY --from=installer --chown=nextjs:nextjs /app/packages/database/migration ./packages/database/migration +COPY --from=installer --chown=nextjs:nextjs /app/packages/database/src ./packages/database/src + +# Copy Prisma-specific generated files +COPY --from=installer --chown=nextjs:nextjs /app/node_modules/@prisma/client ./node_modules/@prisma/client +COPY --from=installer --chown=nextjs:nextjs /app/node_modules/.prisma ./node_modules/.prisma + COPY --from=installer --chown=nextjs:nextjs /prisma_version.txt . COPY /docker/cronjobs /app/docker/cronjobs -# Install Prisma globally -RUN PRISMA_VERSION=$(cat prisma_version.txt) && npm install -g prisma@$PRISMA_VERSION +# Copy only @paralleldrive/cuid2 and @noble/hashes +COPY --from=installer /app/node_modules/@paralleldrive/cuid2 ./node_modules/@paralleldrive/cuid2 +COPY --from=installer /app/node_modules/@noble/hashes ./node_modules/@noble/hashes + +RUN npm install -g tsx typescript prisma EXPOSE 3000 ENV HOSTNAME "0.0.0.0" diff --git a/package.json b/package.json index 671258840c..be2e5f7bd6 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "build:dev": "turbo run build:dev", "db:migrate:dev": "turbo run db:migrate:dev", "db:migrate:deploy": "turbo run db:migrate:deploy", - "db:migrate:vercel": "turbo run db:migrate:vercel", "db:start": "turbo run db:start", "db:push": "turbo run db:push", "go": "turbo run go --concurrency 20", @@ -29,7 +28,8 @@ "test": "turbo run test --no-cache", "test:e2e": "playwright test", "prepare": "husky install", - "storybook": "turbo run storybook" + "storybook": "turbo run storybook", + "fb-migrate-dev": "pnpm --filter @formbricks/database create-migration && pnpm prisma generate" }, "devDependencies": { "@formbricks/eslint-config": "workspace:*", diff --git a/packages/database/.eslintrc.js b/packages/database/.eslintrc.js index 63c8f7518e..8094b00694 100644 --- a/packages/database/.eslintrc.js +++ b/packages/database/.eslintrc.js @@ -4,4 +4,7 @@ module.exports = { project: "tsconfig.json", tsconfigRootDir: __dirname, }, + rules: { + "no-console": "off", + }, }; diff --git a/packages/database/data-migrations/20240207041922_advanced_targeting/data-migration.ts b/packages/database/data-migrations/20240207041922_advanced_targeting/data-migration.ts deleted file mode 100644 index 6f7eaf9671..0000000000 --- a/packages/database/data-migrations/20240207041922_advanced_targeting/data-migration.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { createId } from "@paralleldrive/cuid2"; -import { PrismaClient } from "@prisma/client"; -import { - TBaseFilter, - TBaseFilters, - TSegmentAttributeFilter, - TSegmentPersonFilter, -} from "@formbricks/types/segment"; - -const prisma = new PrismaClient(); - -const main = async () => { - await prisma.$transaction(async (tx) => { - const allSurveysWithAttributeFilters = await prisma.survey.findMany({ - where: { - attributeFilters: { - some: {}, - }, - }, - include: { - attributeFilters: { include: { attributeClass: true } }, - }, - }); - - if (!allSurveysWithAttributeFilters?.length) { - // stop the migration if there are no surveys with attribute filters - return; - } - - allSurveysWithAttributeFilters.forEach(async (survey) => { - const { attributeFilters } = survey; - // if there are no attribute filters, we can skip this survey - if (!attributeFilters?.length) { - return; - } - // from these attribute filters, we need to create user segments - // each attribute filter will be a filter in the user segment - // all the filters will be joined by AND - // the user segment will be private - - const filters: TBaseFilters = attributeFilters.map((filter, idx) => { - const { attributeClass } = filter; - let resource: TSegmentAttributeFilter | TSegmentPersonFilter; - // if the attribute class is userId, we need to create a user segment with the person filter - if (attributeClass.name === "userId" && attributeClass.type === "automatic") { - resource = { - id: createId(), - root: { - type: "person", - personIdentifier: "userId", - }, - qualifier: { - operator: filter.condition, - }, - value: filter.value, - }; - } else { - resource = { - id: createId(), - root: { - type: "attribute", - attributeClassName: attributeClass.name, - }, - qualifier: { - operator: filter.condition, - }, - value: filter.value, - }; - } - - const attributeSegment: TBaseFilter = { - id: filter.id, - connector: idx === 0 ? null : "and", - resource, - }; - - return attributeSegment; - }); - - await tx.segment.create({ - data: { - title: `${survey.id}`, - description: "", - isPrivate: true, - filters, - surveys: { - connect: { - id: survey.id, - }, - }, - environment: { - connect: { - id: survey.environmentId, - }, - }, - }, - }); - }); - - // delete all attribute filters - await tx.surveyAttributeFilter.deleteMany({}); - }); -}; - -main() - .catch(async (e) => { - console.error(e); - process.exit(1); - }) - .finally(async () => await prisma.$disconnect()); diff --git a/packages/database/data-migrations/20240318050527_add_languages_and_survey_languages/data-migration-fix.ts b/packages/database/data-migrations/20240318050527_add_languages_and_survey_languages/data-migration-fix.ts deleted file mode 100644 index 4edb991f6c..0000000000 --- a/packages/database/data-migrations/20240318050527_add_languages_and_survey_languages/data-migration-fix.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* 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"; - -const prisma = new PrismaClient(); - -const main = async () => { - await prisma.$transaction( - async (tx) => { - // Translate Surveys - const surveys: any = await tx.survey.findMany({ - select: { - id: true, - questions: true, - thankYouCard: true, - welcomeCard: true, - }, - }); - - for (const survey of surveys) { - if (typeof survey.thankYouCard.buttonLabel === "string" || hasStringSubheaders(survey.questions)) { - const translatedSurvey = translateSurvey(survey, []); - - // Save the translated survey - await tx.survey.update({ - where: { id: survey.id }, - data: { ...translatedSurvey }, - }); - } - } - }, - { - timeout: 50000, - } - ); -}; - -main() - .catch(async (e) => { - console.error(e); - process.exit(1); - }) - .finally(async () => await prisma.$disconnect()); diff --git a/packages/database/data-migrations/20240318050527_add_languages_and_survey_languages/data-migration-range-fix.ts b/packages/database/data-migrations/20240318050527_add_languages_and_survey_languages/data-migration-range-fix.ts deleted file mode 100644 index fc7b6f36a8..0000000000 --- a/packages/database/data-migrations/20240318050527_add_languages_and_survey_languages/data-migration-range-fix.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* 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"; - -const prisma = new PrismaClient(); - -const main = async () => { - await prisma.$transaction( - async (tx) => { - const surveys = await tx.survey.findMany({ - select: { - id: true, - questions: true, - }, - }); - - if (surveys.length === 0) { - // stop the migration if there are no surveys - return; - } - - for (const survey of surveys) { - let updateNeeded = false; - const updatedSurvey = structuredClone(survey) as any; - if (updatedSurvey.questions.length > 0) { - for (const question of updatedSurvey.questions) { - if (question.type === "rating" && typeof question.range === "string") { - const parsedRange = parseInt(question.range); - if (!isNaN(parsedRange)) { - updateNeeded = true; - question.range = parsedRange; - } else { - throw new Error(`Invalid range value for question Id ${question.id}: ${question.range}`); - } - } - } - } - if (updateNeeded) { - // Save the translated survey - await tx.survey.update({ - where: { id: survey.id }, - data: { ...updatedSurvey }, - }); - } - } - }, - { - timeout: 50000, - } - ); -}; - -main() - .catch(async (e) => { - console.error(e); - process.exit(1); - }) - .finally(async () => await prisma.$disconnect()); diff --git a/packages/database/data-migrations/20240318050527_add_languages_and_survey_languages/data-migration-welcomeCard-fix.ts b/packages/database/data-migrations/20240318050527_add_languages_and_survey_languages/data-migration-welcomeCard-fix.ts deleted file mode 100644 index af3a9bf94d..0000000000 --- a/packages/database/data-migrations/20240318050527_add_languages_and_survey_languages/data-migration-welcomeCard-fix.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* 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"; - -const prisma = new PrismaClient(); - -const main = async () => { - await prisma.$transaction( - async (tx) => { - const surveys = await tx.survey.findMany({ - select: { - id: true, - welcomeCard: true, - }, - }); - - if (surveys.length === 0) { - // stop the migration if there are no surveys - console.log("Stopping migration, no surveys found"); - return; - } - let count = 0; - for (const survey of surveys) { - const updatedSurvey = structuredClone(survey) as any; - if ( - updatedSurvey.welcomeCard && - updatedSurvey.welcomeCard.headline && - Object.keys(updatedSurvey.welcomeCard.headline).length === 0 - ) { - updatedSurvey.welcomeCard.headline["default"] = ""; - count++; - await tx.survey.update({ - where: { id: survey.id }, - data: { ...updatedSurvey }, - }); - } - } - console.log(count, "surveys transformed"); - }, - { - timeout: 50000, - } - ); -}; - -main() - .catch(async (e) => { - console.error(e); - process.exit(1); - }) - .finally(async () => await prisma.$disconnect()); diff --git a/packages/database/data-migrations/20240318050527_add_languages_and_survey_languages/data-migration.ts b/packages/database/data-migrations/20240318050527_add_languages_and_survey_languages/data-migration.ts deleted file mode 100644 index 969869352c..0000000000 --- a/packages/database/data-migrations/20240318050527_add_languages_and_survey_languages/data-migration.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* 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"; - -const prisma = new PrismaClient(); - -const main = async () => { - await prisma.$transaction( - async (tx) => { - // Translate Surveys - const surveys = await tx.survey.findMany({ - select: { - id: true, - questions: true, - thankYouCard: true, - welcomeCard: true, - }, - }); - - if (!surveys) { - // stop the migration if there are no surveys - console.log("No survey found"); - return; - } - console.log("Translating surveys"); - for (const survey of surveys) { - if (survey.questions.length > 0 && typeof survey.questions[0].headline === "string") { - const translatedSurvey = translateSurvey(survey, []); - - // Save the translated survey - await tx.survey.update({ - where: { id: survey.id }, - data: { ...translatedSurvey }, - }); - } - } - console.log("Survey translation completed"); - - // Add language attributeClass - const environments = await tx.environment.findMany({ - select: { - id: true, - attributeClasses: true, - }, - }); - - if (!environments) { - console.log("No environments found"); - // stop the migration if there are no environments - return; - } - - console.log("Adding language attribute class"); - for (const environment of environments) { - const languageAttributeClass = environment.attributeClasses.find((attributeClass) => { - return attributeClass.name === "language"; - }); - if (languageAttributeClass) { - // Update existing attributeClass if needed - if ( - languageAttributeClass.type === AttributeType.automatic && - languageAttributeClass.description === "The language used by the person" - ) { - continue; - } - - await tx.attributeClass.update({ - where: { id: languageAttributeClass.id }, - data: { - type: AttributeType.automatic, - description: "The language used by the person", - }, - }); - } else { - // Create new attributeClass - await tx.attributeClass.create({ - data: { - name: "language", - type: AttributeType.automatic, - description: "The language used by the person", - environment: { - connect: { id: environment.id }, - }, - }, - }); - } - } - console.log("Adding language attribute class finished"); - }, - { - timeout: 50000, - } - ); -}; - -main() - .catch(async (e) => { - console.error(e); - process.exit(1); - }) - .finally(async () => await prisma.$disconnect()); diff --git a/packages/database/data-migrations/20240318050527_add_languages_and_survey_languages/lib/i18n.ts b/packages/database/data-migrations/20240318050527_add_languages_and_survey_languages/lib/i18n.ts deleted file mode 100644 index 85f0ee3e72..0000000000 --- a/packages/database/data-migrations/20240318050527_add_languages_and_survey_languages/lib/i18n.ts +++ /dev/null @@ -1,262 +0,0 @@ -/* eslint-disable -- leacy support workaround for now to avoid rewrite after eslint rules have been changed */ -import { type TLanguage } from "@formbricks/types/project"; -import { - type TI18nString, - type TSurveyCTAQuestion, - type TSurveyConsentQuestion, - type TSurveyMultipleChoiceQuestion, - type TSurveyNPSQuestion, - type TSurveyOpenTextQuestion, - type TSurveyQuestion, - type TSurveyQuestionChoice, - type TSurveyQuestions, - type TSurveyRatingQuestion, - type TSurveyWelcomeCard, - ZSurveyCTAQuestion, - ZSurveyCalQuestion, - ZSurveyConsentQuestion, - ZSurveyFileUploadQuestion, - ZSurveyMultipleChoiceQuestion, - ZSurveyNPSQuestion, - ZSurveyOpenTextQuestion, - ZSurveyPictureSelectionQuestion, - ZSurveyQuestion, - ZSurveyRatingQuestion, - ZSurveyWelcomeCard, -} from "@formbricks/types/surveys/types"; - -// Helper function to create an i18nString from a regular string. -export const createI18nString = (text: string | TI18nString, languages: string[]): TI18nString => { - if (typeof text === "object") { - // It's already an i18n object, so clone it - const i18nString: TI18nString = structuredClone(text); - // Add new language keys with empty strings if they don't exist - languages?.forEach((language) => { - if (!(language in i18nString)) { - i18nString[language] = ""; - } - }); - - // Remove language keys that are not in the languages array - Object.keys(i18nString).forEach((key) => { - if (key !== "default" && languages && !languages.includes(key)) { - delete i18nString[key]; - } - }); - - return i18nString; - } else { - // It's a regular string, so create a new i18n object - const i18nString: TI18nString = { - ["default"]: text, // Type assertion to assure TypeScript `text` is a string - }; - - // Initialize all provided languages with empty strings - languages?.forEach((language) => { - if (language !== "default") { - i18nString[language] = ""; - } - }); - - return i18nString; - } -}; - -// Function to translate a choice label -const translateChoice = (choice: TSurveyQuestionChoice, languages: string[]): TSurveyQuestionChoice => { - if (typeof choice.label !== "undefined") { - return { - ...choice, - label: createI18nString(choice.label, languages), - }; - } else { - return { - ...choice, - label: choice.label, - }; - } -}; - -export const translateWelcomeCard = ( - welcomeCard: TSurveyWelcomeCard, - 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); -}; - -const translateThankYouCard = (thankYouCard: any, languages: string[]): any => { - const clonedThankYouCard = structuredClone(thankYouCard); - - if (typeof thankYouCard.headline !== "undefined") { - clonedThankYouCard.headline = createI18nString(thankYouCard.headline ?? "", languages); - } - - if (typeof thankYouCard.subheader !== "undefined") { - clonedThankYouCard.subheader = createI18nString(thankYouCard.subheader ?? "", languages); - } - - if (typeof clonedThankYouCard.buttonLabel !== "undefined") { - clonedThankYouCard.buttonLabel = createI18nString(thankYouCard.buttonLabel ?? "", languages); - } - return clonedThankYouCard; -}; - -// Function that will translate a single question -const translateQuestion = (question: 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 - ); - } - const range = question.range; - if (typeof range === "string") { - const parsedRange = parseInt(range); - // @ts-expect-error - clonedQuestion.range = parsedRange; - } - 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.id); -}; - -// 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); - const translatedThankYouCard = translateThankYouCard(survey.thankYouCard, languageCodes); - const translatedSurvey = structuredClone(survey); - return { - ...translatedSurvey, - questions: translatedQuestions, - welcomeCard: translatedWelcomeCard, - thankYouCard: translatedThankYouCard, - }; -}; - -export const hasStringSubheaders = (questions: TSurveyQuestions): boolean => { - for (const question of questions) { - if (typeof question.subheader !== "undefined") { - return true; - } - } - return false; -}; diff --git a/packages/database/data-migrations/20240320090315_add_form_styling/data-migration-fix.ts b/packages/database/data-migrations/20240320090315_add_form_styling/data-migration-fix.ts deleted file mode 100644 index 8a95795339..0000000000 --- a/packages/database/data-migrations/20240320090315_add_form_styling/data-migration-fix.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Prisma, PrismaClient } from "@prisma/client"; - -const prisma = new PrismaClient(); - -async function main() { - await prisma.$transaction( - async (tx) => { - const startTime = Date.now(); - console.log("Starting data migration for styling fixes..."); - - const surveysWithProductOverwrites = await tx.survey.findMany({ - where: { - productOverwrites: { - not: Prisma.JsonNull, - }, - }, - }); - - console.log(`Found ${surveysWithProductOverwrites.length} surveys with product overwrites to process.`); - - for (const survey of surveysWithProductOverwrites) { - if (survey.productOverwrites) { - const { brandColor, highlightBorderColor, ...rest } = survey.productOverwrites; - - if (!brandColor && !highlightBorderColor) { - continue; - } - - await tx.survey.update({ - where: { - id: survey.id, - }, - data: { - styling: { - ...(survey.styling ?? {}), - ...(brandColor && { brandColor: { light: brandColor } }), - ...(highlightBorderColor && { highlightBorderColor: { light: highlightBorderColor } }), - ...((brandColor || highlightBorderColor || Object.keys(survey.styling ?? {}).length > 0) && { - overwriteThemeStyling: true, - }), - }, - productOverwrites: { - ...rest, - }, - }, - }); - } - } - - const endTime = Date.now(); - console.log(`Data migration for styling fixes completed in ${(endTime - startTime) / 1000} seconds.`); - }, - { timeout: 50000 } - ); -} - -main() - .catch(async (e) => { - console.error(e); - process.exit(1); - }) - .finally(async () => await prisma.$disconnect()); diff --git a/packages/database/data-migrations/20240320090315_add_form_styling/data-migration.ts b/packages/database/data-migrations/20240320090315_add_form_styling/data-migration.ts deleted file mode 100644 index 9f47976dfb..0000000000 --- a/packages/database/data-migrations/20240320090315_add_form_styling/data-migration.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { PrismaClient } from "@prisma/client"; - -const DEFAULT_BRAND_COLOR = "#64748b"; -const DEFAULT_STYLING = { - allowStyleOverwrite: true, -}; - -const prisma = new PrismaClient(); - -const main = async () => { - await prisma.$transaction( - async (tx) => { - // product table with brand color and the highlight border color (if available) - // styling object needs to be created for each product - const products = await tx.product.findMany({ - include: { environments: { include: { surveys: true } } }, - }); - - if (!products) { - // something went wrong, could not find any products - return; - } - - if (products.length) { - for (const product of products) { - // no migration needed - // 1. product's brandColor is equal to the default one - // 2. product's styling object is equal the default one - // 3. product has no highlightBorderColor - - if ( - product.brandColor === DEFAULT_BRAND_COLOR && - JSON.stringify(product.styling) === JSON.stringify(DEFAULT_STYLING) && - !product.highlightBorderColor - ) { - continue; - } - - await tx.product.update({ - where: { - id: product.id, - }, - data: { - styling: { - ...product.styling, - // only if the brand color is not null and not equal to the default one, we need to update the styling object. Otherwise, we'll just use the default value - ...(product.brandColor && - product.brandColor !== DEFAULT_BRAND_COLOR && { - brandColor: { light: product.brandColor }, - }), - ...(product.highlightBorderColor && { - highlightBorderColor: { - light: product.highlightBorderColor, - }, - }), - }, - brandColor: null, - highlightBorderColor: null, - }, - }); - - // for each survey in the product, we need to update the stying object with the brand color and the highlight border color - for (const environment of product.environments) { - for (const survey of environment.surveys) { - const { styling } = product; - const { brandColor, highlightBorderColor } = styling; - - if (!survey.styling) { - continue; - } - - const { styling: surveyStyling } = survey; - const { hideProgressBar } = surveyStyling; - - await tx.survey.update({ - where: { - id: survey.id, - }, - data: { - styling: { - ...(survey.styling ?? {}), - ...(brandColor && - brandColor.light && { - brandColor: { light: brandColor.light }, - }), - ...(highlightBorderColor?.light && { - highlightBorderColor: { - light: highlightBorderColor.light, - }, - }), - - // if the previous survey had the hideProgressBar set to true, we need to update the styling object with overwriteThemeStyling set to true - ...(hideProgressBar && { - overwriteThemeStyling: true, - }), - }, - }, - }); - - // if the survey has product overwrites, we need to update the styling object with the brand color and the highlight border color - if (survey.productOverwrites) { - const { brandColor, highlightBorderColor, ...rest } = survey.productOverwrites; - - await tx.survey.update({ - where: { - id: survey.id, - }, - data: { - styling: { - ...(survey.styling ?? {}), - ...(brandColor && { brandColor: { light: brandColor } }), - ...(highlightBorderColor && { highlightBorderColor: { light: highlightBorderColor } }), - ...((brandColor || - highlightBorderColor || - Object.keys(survey.styling ?? {}).length > 0) && { - overwriteThemeStyling: true, - }), - }, - productOverwrites: { - ...rest, - }, - }, - }); - } - } - } - } - } - }, - { - timeout: 50000, - } - ); -}; -main() - .catch(async (e) => { - console.error(e); - process.exit(1); - }) - .finally(async () => await prisma.$disconnect()); diff --git a/packages/database/data-migrations/20240408123456_userid_migration/data-migration.ts b/packages/database/data-migrations/20240408123456_userid_migration/data-migration.ts deleted file mode 100644 index b8a566267e..0000000000 --- a/packages/database/data-migrations/20240408123456_userid_migration/data-migration.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { PrismaClient } from "@prisma/client"; - -const prisma = new PrismaClient(); - -const main = async () => { - await prisma.$transaction( - async (tx) => { - // get all the persons that have an attribute class with the name "userId" - const userIdAttributeClasses = await tx.attributeClass.findMany({ - where: { - name: "userId", - }, - include: { - attributes: { - include: { person: true }, - }, - }, - }); - - for (let attributeClass of userIdAttributeClasses) { - for (let attribute of attributeClass.attributes) { - if (attribute.person.userId) { - continue; - } - - await tx.person.update({ - where: { - id: attribute.personId, - }, - data: { - userId: attribute.value, - }, - }); - } - } - - console.log("Migrated userIds to the person table."); - - // Delete all attributeClasses with the name "userId" - await tx.attributeClass.deleteMany({ - where: { - name: "userId", - }, - }); - - console.log("Deleted attributeClasses with the name 'userId'."); - }, - { - timeout: 60000 * 3, // 3 minutes - } - ); -}; -main() - .catch(async (e) => { - console.error(e); - process.exit(1); - }) - .finally(async () => await prisma.$disconnect()); diff --git a/packages/database/data-migrations/20240410111624_adds_website_and_inapp_survey/data-migration.ts b/packages/database/data-migrations/20240410111624_adds_website_and_inapp_survey/data-migration.ts deleted file mode 100644 index 301a1a6ca4..0000000000 --- a/packages/database/data-migrations/20240410111624_adds_website_and_inapp_survey/data-migration.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { PrismaClient } from "@prisma/client"; - -const prisma = new PrismaClient(); - -const main = async () => { - await prisma.$transaction( - async (tx) => { - // Retrieve all surveys of type "web" with necessary fields for efficient processing - const webSurveys = await tx.survey.findMany({ - where: { type: "web" }, - select: { - id: true, - segment: { - select: { - id: true, - isPrivate: true, - }, - }, - }, - }); - - const linkSurveysWithSegment = await tx.survey.findMany({ - where: { - type: "link", - segmentId: { - not: null, - }, - }, - include: { - segment: true, - }, - }); - - const updateOperations = []; - const segmentDeletionIds = []; - const surveyTitlesForDeletion = []; - - if (webSurveys?.length > 0) { - for (const webSurvey of webSurveys) { - const latestResponse = await tx.response.findFirst({ - where: { surveyId: webSurvey.id }, - orderBy: { createdAt: "desc" }, - select: { personId: true }, - }); - - const newType = latestResponse?.personId ? "app" : "website"; - updateOperations.push( - tx.survey.update({ - where: { id: webSurvey.id }, - data: { type: newType }, - }) - ); - - if (newType === "website") { - if (webSurvey.segment) { - if (webSurvey.segment.isPrivate) { - segmentDeletionIds.push(webSurvey.segment.id); - } else { - updateOperations.push( - tx.survey.update({ - where: { id: webSurvey.id }, - data: { - segment: { disconnect: true }, - }, - }) - ); - } - } - - surveyTitlesForDeletion.push(webSurvey.id); - } - } - - await Promise.all(updateOperations); - - if (segmentDeletionIds.length > 0) { - await tx.segment.deleteMany({ - where: { - id: { in: segmentDeletionIds }, - }, - }); - } - - if (surveyTitlesForDeletion.length > 0) { - await tx.segment.deleteMany({ - where: { - title: { in: surveyTitlesForDeletion }, - isPrivate: true, - }, - }); - } - } - - if (linkSurveysWithSegment?.length > 0) { - const linkSurveySegmentDeletionIds = []; - const linkSurveySegmentUpdateOperations = []; - - for (const linkSurvey of linkSurveysWithSegment) { - const { segment } = linkSurvey; - if (segment) { - linkSurveySegmentUpdateOperations.push( - tx.survey.update({ - where: { - id: linkSurvey.id, - }, - data: { - segment: { - disconnect: true, - }, - }, - }) - ); - - if (segment.isPrivate) { - linkSurveySegmentDeletionIds.push(segment.id); - } - } - } - - await Promise.all(linkSurveySegmentUpdateOperations); - - if (linkSurveySegmentDeletionIds.length > 0) { - await tx.segment.deleteMany({ - where: { - id: { in: linkSurveySegmentDeletionIds }, - }, - }); - } - } - }, - { - timeout: 50000, - } - ); -}; - -main() - .catch((e: Error) => { - console.error("Error during migration: ", e.message); - process.exit(1); - }) - .finally(async () => { - await prisma.$disconnect(); - }); diff --git a/packages/database/data-migrations/20240501111944_refactors_actions_and_removes_inline_triggers/data-migration.ts b/packages/database/data-migrations/20240501111944_refactors_actions_and_removes_inline_triggers/data-migration.ts deleted file mode 100644 index 258eedda16..0000000000 --- a/packages/database/data-migrations/20240501111944_refactors_actions_and_removes_inline_triggers/data-migration.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { init } from "@paralleldrive/cuid2"; -import { Prisma, PrismaClient } from "@prisma/client"; - -const createId = init({ length: 5 }); -const prisma = new PrismaClient(); - -const main = async () => { - await prisma.$transaction( - async (tx) => { - const startTime = Date.now(); - console.log("Starting data migration..."); - - // 1. copy value of name to key for all action classes where type is code - const codeActionClasses = await tx.actionClass.findMany({ - where: { - type: "code", - }, - }); - console.log(`Found ${codeActionClasses.length} saved code action classes to update.`); - - await Promise.all( - codeActionClasses.map((codeActionClass) => { - return tx.actionClass.update({ - where: { - id: codeActionClass.id, - }, - data: { - key: codeActionClass.name, - }, - }); - }) - ); - console.log("Updated keys for saved code action classes."); - - // 2. find all surveys with inlineTriggers and create action classes for them - const surveysWithInlineTriggers = await tx.survey.findMany({ - where: { - inlineTriggers: { - not: Prisma.JsonNull, - }, - }, - }); - console.log(`Found ${surveysWithInlineTriggers.length} surveys with inline triggers to process.`); - - // 3. Create action classes for inlineTriggers and update survey to use the newly created action classes - const getActionClassIdByCode = async (code: string, environmentId: string): Promise => { - const existingActionClass = await tx.actionClass.findFirst({ - where: { - type: "code", - key: code, - environmentId: environmentId, - }, - }); - - let codeActionId = ""; - - if (existingActionClass) { - codeActionId = existingActionClass.id; - } else { - let codeActionClassName = code; - - // check if there is an existing noCode action class with this name - const existingNoCodeActionClass = await tx.actionClass.findFirst({ - where: { - name: code, - environmentId: environmentId, - NOT: { - type: "code", - }, - }, - }); - - if (existingNoCodeActionClass) { - codeActionClassName = `${code}-${createId()}`; - } - - // create a new private action for codeConfig - const codeActionClass = await tx.actionClass.create({ - data: { - name: codeActionClassName, - key: code, - type: "code", - environment: { - connect: { - id: environmentId, - }, - }, - }, - }); - codeActionId = codeActionClass.id; - } - - return codeActionId; - }; - - for (const survey of surveysWithInlineTriggers) { - const { codeConfig, noCodeConfig } = survey.inlineTriggers ?? {}; - - if ( - noCodeConfig && - Object.keys(noCodeConfig).length > 0 && - (!codeConfig || codeConfig.identifier === "") - ) { - // surveys with only noCodeConfig - - // create a new private action for noCodeConfig - const noCodeActionClass = await tx.actionClass.create({ - data: { - name: `Custom Action-${createId()}`, - noCodeConfig, - type: "noCode", - environment: { - connect: { - id: survey.environmentId, - }, - }, - }, - }); - - // update survey to use the newly created action class - await tx.survey.update({ - where: { - id: survey.id, - }, - data: { - triggers: { - create: { - actionClassId: noCodeActionClass.id, - }, - }, - }, - }); - } else if ((!noCodeConfig || Object.keys(noCodeConfig).length === 0) && codeConfig?.identifier) { - const codeActionId = await getActionClassIdByCode(codeConfig.identifier, survey.environmentId); - - await tx.survey.update({ - where: { - id: survey.id, - }, - data: { - triggers: { - create: { - actionClassId: codeActionId, - }, - }, - }, - }); - } else if (codeConfig?.identifier && noCodeConfig) { - // create a new private action for noCodeConfig - - const noCodeActionClass = await tx.actionClass.create({ - data: { - name: `Custom Action-${createId()}`, - noCodeConfig, - type: "noCode", - environment: { - connect: { - id: survey.environmentId, - }, - }, - }, - }); - - const codeActionId = await getActionClassIdByCode(codeConfig.identifier, survey.environmentId); - - // update survey to use the newly created action classes - await tx.survey.update({ - where: { - id: survey.id, - }, - data: { - triggers: { - createMany: { - data: [ - { - actionClassId: noCodeActionClass.id, - }, - { - actionClassId: codeActionId, - }, - ], - }, - }, - }, - }); - } - } - - 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/data-migrations/20240524053239_extends_no_code_action_schema/data-migration.ts b/packages/database/data-migrations/20240524053239_extends_no_code_action_schema/data-migration.ts deleted file mode 100644 index 1e9165eaaf..0000000000 --- a/packages/database/data-migrations/20240524053239_extends_no_code_action_schema/data-migration.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { PrismaClient } from "@prisma/client"; -import { TActionClassNoCodeConfig } from "@formbricks/types/action-classes"; - -const prisma = new PrismaClient(); - -const main = async () => { - await prisma.$transaction( - async (tx) => { - const startTime = Date.now(); - console.log("Starting data migration..."); - - const getConfig = (noCodeConfig: any): TActionClassNoCodeConfig => { - const cssSelector = noCodeConfig?.cssSelector?.value; - const innerHtml = noCodeConfig?.innerHtml?.value; - const pageUrl = noCodeConfig?.pageUrl; - - const urlFilters = pageUrl ? [pageUrl] : []; - - if (!cssSelector && !innerHtml && pageUrl) { - return { - type: "pageView", - urlFilters, - }; - } else { - return { - type: "click", - elementSelector: { - ...(cssSelector && { cssSelector }), - ...(innerHtml && { innerHtml }), - }, - urlFilters, - }; - } - }; - - // 1. Updation of all noCode actions to fit in the latest schema - const noCodeActionClasses = await tx.actionClass.findMany({ - where: { - type: "noCode", - }, - select: { - id: true, - noCodeConfig: true, - }, - }); - - console.log(`Found ${noCodeActionClasses.length} noCode action classes to update.`); - - await Promise.all( - noCodeActionClasses.map((noCodeActionClass) => { - return tx.actionClass.update({ - where: { - id: noCodeActionClass.id, - }, - data: { - noCodeConfig: getConfig(noCodeActionClass.noCodeConfig), - }, - }); - }) - ); - - const targetAutomaticActions = [ - { name: "Exit Intent (Desktop)", type: "exitIntent" }, - { name: "50% Scroll", type: "fiftyPercentScroll" }, - ]; - - // 2. Update "Exit Intent (Desktop)", "50% Scroll" automatic actions classes that have atleast one survey trigger to noCode actions, update them one by one - const automaticActionClassesToUpdatePromises = targetAutomaticActions.map((action) => { - return tx.actionClass.updateMany({ - where: { - name: action.name, - type: "automatic", - surveys: { - some: {}, - }, - }, - data: { - type: "noCode", - noCodeConfig: { - type: action.type as "exitIntent" | "fiftyPercentScroll", - urlFilters: [], - }, - }, - }); - }); - - const targetAutomaticActionNames = targetAutomaticActions.map((action) => action.name); - - console.log(`Updating ${targetAutomaticActionNames.join(" and ")} action classes...`); - - await Promise.all(automaticActionClassesToUpdatePromises); - - // 3. Delete all automatic action classes that are not associated with a survey - const automaticActionClassesToDelete = await tx.actionClass.deleteMany({ - where: { - name: { - in: targetAutomaticActionNames, - }, - type: "automatic", - }, - }); - - console.log( - `Deleted ${automaticActionClassesToDelete.count} unused automatic action classes of 50% scroll and exit intent.` - ); - - 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/data-migrations/20240610055828_adds_app_and_website_status_indicators/data-migration.ts b/packages/database/data-migrations/20240610055828_adds_app_and_website_status_indicators/data-migration.ts deleted file mode 100644 index 79e15d1ef0..0000000000 --- a/packages/database/data-migrations/20240610055828_adds_app_and_website_status_indicators/data-migration.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { PrismaClient } from "@prisma/client"; - -const prisma = new PrismaClient(); - -const main = async () => { - await prisma.$transaction( - async (tx) => { - const startTime = Date.now(); - console.log("Starting data migration..."); - - // Retrieve all environments with widget setup completed - const environmentsWithWidgetSetupCompleted = await tx.environment.findMany({ - where: { - widgetSetupCompleted: true, - }, - select: { - id: true, - }, - }); - - console.log( - `Found ${environmentsWithWidgetSetupCompleted.length} environments with widget setup completed.` - ); - - if (environmentsWithWidgetSetupCompleted.length > 0) { - const environmentIds = environmentsWithWidgetSetupCompleted.map((env) => env.id); - - // Fetch survey counts in a single query - const surveyCounts = await tx.survey.groupBy({ - by: ["environmentId", "type"], - where: { - environmentId: { - in: environmentIds, - }, - displays: { - some: {}, - }, - type: { - in: ["app", "website"], - }, - }, - _count: { - _all: true, - }, - }); - - // Create a map of environmentId to survey counts - const surveyCountMap = surveyCounts.reduce( - (acc, survey) => { - if (!acc[survey.environmentId]) { - acc[survey.environmentId] = { website: 0, app: 0, link: 0, web: 0 }; - } - acc[survey.environmentId][survey.type] = survey._count._all; - return acc; - }, - {} as Record - ); - - // Update the appSetupCompleted and websiteSetupCompleted flags for each environment - const updatePromises = environmentsWithWidgetSetupCompleted.map((environment) => { - const counts = surveyCountMap[environment.id] || { website: 0, app: 0 }; - - return tx.environment.update({ - where: { id: environment.id }, - data: { - appSetupCompleted: counts.app > 0, - websiteSetupCompleted: counts.website > 0, - }, - }); - }); - - await Promise.all(updatePromises); - } - - const endTime = Date.now(); - console.log(`Data migration completed. Total time: ${(endTime - startTime) / 1000}s`); - }, - { - timeout: 50000, - } - ); -}; - -main() - .catch((e: Error) => { - console.error("Error during migration: ", e.message); - process.exit(1); - }) - .finally(async () => { - await prisma.$disconnect(); - }); 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 deleted file mode 100644 index f2e989dad8..0000000000 --- a/packages/database/data-migrations/20240612115151_adds_product_config/data-migration.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { PrismaClient, SurveyType } from "@prisma/client"; -import { TProjectConfigChannel } from "@formbricks/types/project"; - -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: TProjectConfigChannel = 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/data-migrations/20240613070218_pricing_v2/data-migration.ts b/packages/database/data-migrations/20240613070218_pricing_v2/data-migration.ts deleted file mode 100644 index 36169b80f3..0000000000 --- a/packages/database/data-migrations/20240613070218_pricing_v2/data-migration.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { Prisma, PrismaClient } from "@prisma/client"; - -const prisma = new PrismaClient(); - -type TSubscriptionStatusLegacy = "active" | "cancelled" | "inactive"; - -interface TSubscriptionLegacy { - status: TSubscriptionStatusLegacy; - unlimited: boolean; -} - -interface TFeatures { - inAppSurvey: TSubscriptionLegacy; - linkSurvey: TSubscriptionLegacy; - userTargeting: TSubscriptionLegacy; - multiLanguage: TSubscriptionLegacy; -} - -interface TOrganizationBillingLegacy { - stripeCustomerId: string | null; - features: TFeatures; -} - -const now = new Date(); -const firstOfMonthUTC = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)); - -async function main() { - await prisma.$transaction( - async (tx) => { - // const startTime = Date.now(); - console.log("Starting data migration for pricing v2..."); - - // Free tier - const orgsWithoutBilling = await tx.organization.findMany({ - where: { - billing: { - path: ["stripeCustomerId"], - equals: Prisma.AnyNull, - }, - }, - }); - - console.log( - `Found ${orgsWithoutBilling.length} organizations without billing information. Moving them to free plan...` - ); - - const freePlanPromises = orgsWithoutBilling - // if the organization has a plan, it means it's already migrated - .filter((org) => !(org.billing.plan && typeof org.billing.plan === "string")) - .map((organization) => - tx.organization.update({ - where: { - id: organization.id, - }, - data: { - billing: { - stripeCustomerId: null, - plan: "free", - period: "monthly", - limits: { - monthly: { - responses: 500, - miu: 1000, - }, - }, - periodStart: new Date(), - }, - }, - }) - ); - - await Promise.all(freePlanPromises); - console.log("Moved all organizations without billing to free plan"); - - const orgsWithBilling = await tx.organization.findMany({ - where: { - billing: { - path: ["stripeCustomerId"], - not: Prisma.AnyNull, - }, - }, - }); - - console.log(`Found ${orgsWithBilling.length} organizations with billing information`); - - for (const org of orgsWithBilling) { - const billing = org.billing as TOrganizationBillingLegacy; - - console.log("Current organization: ", org.id); - - // @ts-expect-error - if (billing.plan && typeof billing.plan === "string") { - // no migration needed, already following the latest schema - continue; - } - - if ( - (billing.features.linkSurvey?.status === "active" && billing.features.linkSurvey?.unlimited) || - (billing.features.inAppSurvey?.status === "active" && billing.features.inAppSurvey?.unlimited) || - (billing.features.userTargeting?.status === "active" && billing.features.userTargeting?.unlimited) - ) { - await tx.organization.update({ - where: { - id: org.id, - }, - data: { - billing: { - plan: "enterprise", - period: "monthly", - limits: { - monthly: { - responses: null, - miu: null, - }, - }, - stripeCustomerId: billing.stripeCustomerId, - periodStart: firstOfMonthUTC, - }, - }, - }); - - console.log("Updated org with unlimited to enterprise plan: ", org.id); - continue; - } - - if (billing.features.linkSurvey.status === "active") { - await tx.organization.update({ - where: { - id: org.id, - }, - data: { - billing: { - plan: "startup", - period: "monthly", - limits: { - monthly: { - responses: 2000, - miu: 2500, - }, - }, - stripeCustomerId: billing.stripeCustomerId, - periodStart: firstOfMonthUTC, - }, - }, - }); - - console.log("Updated org with linkSurvey to pro plan: ", org.id); - continue; - } - - await tx.organization.update({ - where: { - id: org.id, - }, - data: { - billing: { - plan: "free", - period: "monthly", - limits: { - monthly: { - responses: 500, - miu: 1000, - }, - }, - stripeCustomerId: billing.stripeCustomerId, - periodStart: new Date(), - }, - }, - }); - - console.log("Updated org to free plan: ", org.id); - } - }, - { timeout: 50000 } - ); -} - -main() - .catch(async (e) => { - console.error(e); - process.exit(1); - }) - .finally(async () => await prisma.$disconnect()); diff --git a/packages/database/data-migrations/20240625101352_update_zh_to_zh-Hans/data-migration.ts b/packages/database/data-migrations/20240625101352_update_zh_to_zh-Hans/data-migration.ts deleted file mode 100644 index 1868c521fd..0000000000 --- a/packages/database/data-migrations/20240625101352_update_zh_to_zh-Hans/data-migration.ts +++ /dev/null @@ -1,119 +0,0 @@ -// migration script for converting zh code for chinese language to zh-Hans -import { PrismaClient } from "@prisma/client"; -import { - TSurveyLanguage, - TSurveyQuestion, - TSurveyThankYouCard, - TSurveyWelcomeCard, -} from "@formbricks/types/surveys"; -import { - updateLanguageCodeForQuestion, - updateLanguageCodeForThankYouCard, - updateLanguageCodeForWelcomeCard, -} from "./lib/utils"; - -const prisma = new PrismaClient(); - -const main = async () => { - await prisma.$transaction( - async (tx) => { - const startTime = Date.now(); - console.log("Starting data migration..."); - - // Fetch all surveys - const surveys: { - id: string; - questions: TSurveyQuestion[]; - welcomeCard: TSurveyWelcomeCard; - thankYouCard: TSurveyThankYouCard; - languages: TSurveyLanguage[]; - }[] = await tx.survey.findMany({ - select: { - id: true, - questions: true, - welcomeCard: true, - thankYouCard: true, - languages: { - select: { - default: true, - enabled: true, - language: { - select: { - id: true, - code: true, - alias: true, - createdAt: true, - updatedAt: true, - }, - }, - }, - }, - }, - }); - - if (surveys.length === 0) { - // stop the migration if there are no surveys - console.log("No Surveys found"); - return; - } - - console.log(`Total surveys found:${surveys.length}`); - let transformedSurveyCount = 0; - - const surveysWithChineseTranslations = surveys.filter((survey) => - survey.languages.some((surveyLanguage) => surveyLanguage.language.code === "zh") - ); - - const updatePromises = surveysWithChineseTranslations.map((survey) => { - transformedSurveyCount++; - const updatedSurvey = structuredClone(survey); - - // Update cards and questions - updatedSurvey.welcomeCard = updateLanguageCodeForWelcomeCard(survey.welcomeCard, "zh", "zh-Hans"); - updatedSurvey.thankYouCard = updateLanguageCodeForThankYouCard(survey.thankYouCard, "zh", "zh-Hans"); - updatedSurvey.questions = survey.questions.map((question) => - updateLanguageCodeForQuestion(question, "zh", "zh-Hans") - ); - - // Return the update promise - return tx.survey.update({ - where: { id: survey.id }, - data: { - welcomeCard: updatedSurvey.welcomeCard, - thankYouCard: updatedSurvey.thankYouCard, - questions: updatedSurvey.questions, - }, - }); - }); - - await Promise.all(updatePromises); - - console.log(transformedSurveyCount, " surveys transformed"); - - console.log("updating languages"); - await tx.language.updateMany({ - where: { - code: "zh", - }, - data: { - code: "zh-Hans", - }, - }); - console.log("survey language updated"); - 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/data-migrations/20240625101352_update_zh_to_zh-Hans/lib/utils.ts b/packages/database/data-migrations/20240625101352_update_zh_to_zh-Hans/lib/utils.ts deleted file mode 100644 index 8661a8a20d..0000000000 --- a/packages/database/data-migrations/20240625101352_update_zh_to_zh-Hans/lib/utils.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { - TI18nString, - TSurveyCTAQuestion, - TSurveyChoice, - TSurveyConsentQuestion, - TSurveyMatrixQuestion, - TSurveyMultipleChoiceQuestion, - TSurveyNPSQuestion, - TSurveyOpenTextQuestion, - TSurveyQuestion, - TSurveyRatingQuestion, - TSurveyThankYouCard, - TSurveyWelcomeCard, - ZSurveyAddressQuestion, - ZSurveyCTAQuestion, - ZSurveyCalQuestion, - ZSurveyConsentQuestion, - ZSurveyFileUploadQuestion, - ZSurveyMatrixQuestion, - ZSurveyMultipleChoiceQuestion, - ZSurveyNPSQuestion, - ZSurveyOpenTextQuestion, - ZSurveyPictureSelectionQuestion, - ZSurveyQuestion, - ZSurveyRatingQuestion, - ZSurveyThankYouCard, - ZSurveyWelcomeCard, -} from "@formbricks/types/surveys"; - -export const updateLanguageCode = (i18nString: TI18nString, oldCode: string, newCode: string) => { - const updatedI18nString = structuredClone(i18nString); - if (Object.keys(i18nString).includes(oldCode)) { - const text = i18nString[oldCode]; - delete updatedI18nString[oldCode]; - updatedI18nString[newCode] = text; - } - return updatedI18nString; -}; - -export const updateChoiceLanguage = ( - choice: TSurveyChoice, - oldCode: string, - newCode: string -): TSurveyChoice => { - if (typeof choice.label !== "undefined" && Object.keys(choice.label).includes(oldCode)) { - return { - ...choice, - label: updateLanguageCode(choice.label, oldCode, newCode), - }; - } else { - return { - ...choice, - label: choice.label, - }; - } -}; - -export const updateLanguageCodeForWelcomeCard = ( - welcomeCard: TSurveyWelcomeCard, - oldCode: string, - newCode: string -) => { - const clonedWelcomeCard = structuredClone(welcomeCard); - if (typeof welcomeCard.headline !== "undefined" && Object.keys(welcomeCard.headline).includes(oldCode)) { - clonedWelcomeCard.headline = updateLanguageCode(welcomeCard.headline, oldCode, newCode); - } - - if (typeof welcomeCard.html !== "undefined" && Object.keys(welcomeCard.html).includes(oldCode)) { - clonedWelcomeCard.html = updateLanguageCode(welcomeCard.html, oldCode, newCode); - } - - if ( - typeof welcomeCard.buttonLabel !== "undefined" && - Object.keys(welcomeCard.buttonLabel).includes(oldCode) - ) { - clonedWelcomeCard.buttonLabel = updateLanguageCode(welcomeCard.buttonLabel, oldCode, newCode); - } - - return ZSurveyWelcomeCard.parse(clonedWelcomeCard); -}; - -export const updateLanguageCodeForThankYouCard = ( - thankYouCard: TSurveyThankYouCard, - oldCode: string, - newCode: string -) => { - const clonedThankYouCard = structuredClone(thankYouCard); - if (typeof thankYouCard.headline !== "undefined" && Object.keys(thankYouCard.headline).includes(oldCode)) { - clonedThankYouCard.headline = updateLanguageCode(thankYouCard.headline, oldCode, newCode); - } - - if ( - typeof thankYouCard.subheader !== "undefined" && - Object.keys(thankYouCard.subheader).includes(oldCode) - ) { - clonedThankYouCard.subheader = updateLanguageCode(thankYouCard.subheader, oldCode, newCode); - } - - if ( - typeof thankYouCard.buttonLabel !== "undefined" && - Object.keys(thankYouCard.buttonLabel).includes(oldCode) - ) { - clonedThankYouCard.buttonLabel = updateLanguageCode(thankYouCard.buttonLabel, oldCode, newCode); - } - return ZSurveyThankYouCard.parse(clonedThankYouCard); -}; - -export const updateLanguageCodeForQuestion = ( - question: TSurveyQuestion, - oldCode: string, - newCode: string -) => { - const clonedQuestion = structuredClone(question); - //common question properties - if (typeof question.headline !== "undefined" && Object.keys(question.headline).includes(oldCode)) { - clonedQuestion.headline = updateLanguageCode(question.headline, oldCode, newCode); - } - - if (typeof question.subheader !== "undefined" && Object.keys(question.subheader).includes(oldCode)) { - clonedQuestion.subheader = updateLanguageCode(question.subheader, oldCode, newCode); - } - - if (typeof question.buttonLabel !== "undefined" && Object.keys(question.buttonLabel).includes(oldCode)) { - clonedQuestion.buttonLabel = updateLanguageCode(question.buttonLabel, oldCode, newCode); - } - - if ( - typeof question.backButtonLabel !== "undefined" && - Object.keys(question.backButtonLabel).includes(oldCode) - ) { - clonedQuestion.backButtonLabel = updateLanguageCode(question.backButtonLabel, oldCode, newCode); - } - - switch (question.type) { - case "openText": - if ( - typeof question.placeholder !== "undefined" && - Object.keys(question.placeholder).includes(oldCode) - ) { - (clonedQuestion as TSurveyOpenTextQuestion).placeholder = updateLanguageCode( - question.placeholder, - oldCode, - newCode - ); - } - return ZSurveyOpenTextQuestion.parse(clonedQuestion); - - case "multipleChoiceSingle": - case "multipleChoiceMulti": - (clonedQuestion as TSurveyMultipleChoiceQuestion).choices = question.choices.map((choice) => { - return updateChoiceLanguage(choice, oldCode, newCode); - }); - if ( - typeof question.otherOptionPlaceholder !== "undefined" && - Object.keys(question.otherOptionPlaceholder).includes(oldCode) - ) { - (clonedQuestion as TSurveyMultipleChoiceQuestion).otherOptionPlaceholder = updateLanguageCode( - question.otherOptionPlaceholder, - oldCode, - newCode - ); - } - return ZSurveyMultipleChoiceQuestion.parse(clonedQuestion); - - case "cta": - if ( - typeof question.dismissButtonLabel !== "undefined" && - Object.keys(question.dismissButtonLabel).includes(oldCode) - ) { - (clonedQuestion as TSurveyCTAQuestion).dismissButtonLabel = updateLanguageCode( - question.dismissButtonLabel, - oldCode, - newCode - ); - } - if (typeof question.html !== "undefined" && Object.keys(question.html).includes(oldCode)) { - (clonedQuestion as TSurveyCTAQuestion).html = updateLanguageCode(question.html, oldCode, newCode); - } - return ZSurveyCTAQuestion.parse(clonedQuestion); - - case "consent": - if (typeof question.html !== "undefined" && Object.keys(question.html).includes(oldCode)) { - (clonedQuestion as TSurveyConsentQuestion).html = updateLanguageCode(question.html, oldCode, newCode); - } - - if (typeof question.label !== "undefined" && Object.keys(question.label).includes(oldCode)) { - (clonedQuestion as TSurveyConsentQuestion).label = updateLanguageCode( - question.label, - oldCode, - newCode - ); - } - return ZSurveyConsentQuestion.parse(clonedQuestion); - - case "nps": - if (typeof question.lowerLabel !== "undefined" && Object.keys(question.lowerLabel).includes(oldCode)) { - (clonedQuestion as TSurveyNPSQuestion).lowerLabel = updateLanguageCode( - question.lowerLabel, - oldCode, - newCode - ); - } - if (typeof question.upperLabel !== "undefined" && Object.keys(question.upperLabel).includes(oldCode)) { - (clonedQuestion as TSurveyNPSQuestion).upperLabel = updateLanguageCode( - question.upperLabel, - oldCode, - newCode - ); - } - return ZSurveyNPSQuestion.parse(clonedQuestion); - - case "rating": - if (typeof question.lowerLabel !== "undefined" && Object.keys(question.lowerLabel).includes(oldCode)) { - (clonedQuestion as TSurveyRatingQuestion).lowerLabel = updateLanguageCode( - question.lowerLabel, - oldCode, - newCode - ); - } - - if (typeof question.upperLabel !== "undefined" && Object.keys(question.upperLabel).includes(oldCode)) { - (clonedQuestion as TSurveyRatingQuestion).upperLabel = updateLanguageCode( - question.upperLabel, - oldCode, - newCode - ); - } - return ZSurveyRatingQuestion.parse(clonedQuestion); - - case "matrix": - (clonedQuestion as TSurveyMatrixQuestion).rows = question.rows.map((row) => { - if (typeof row !== "undefined" && Object.keys(row).includes(oldCode)) { - return updateLanguageCode(row, oldCode, newCode); - } else return row; - }); - - (clonedQuestion as TSurveyMatrixQuestion).columns = question.columns.map((column) => { - if (typeof column !== "undefined" && Object.keys(column).includes(oldCode)) { - return updateLanguageCode(column, oldCode, newCode); - } else return column; - }); - return ZSurveyMatrixQuestion.parse(clonedQuestion); - - case "fileUpload": - return ZSurveyFileUploadQuestion.parse(clonedQuestion); - - case "pictureSelection": - return ZSurveyPictureSelectionQuestion.parse(clonedQuestion); - - case "cal": - return ZSurveyCalQuestion.parse(clonedQuestion); - - case "address": - return ZSurveyAddressQuestion.parse(clonedQuestion); - - default: - return ZSurveyQuestion.parse(clonedQuestion); - } -}; diff --git a/packages/database/data-migrations/20240712123456_segments_cleanup/data-migration.ts b/packages/database/data-migrations/20240712123456_segments_cleanup/data-migration.ts deleted file mode 100644 index 143c68d60d..0000000000 --- a/packages/database/data-migrations/20240712123456_segments_cleanup/data-migration.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* eslint-disable no-console -- logging is allowed in migration scripts */ -import { PrismaClient } from "@prisma/client"; - -const prisma = new PrismaClient(); - -async function runMigration(): Promise { - await prisma.$transaction( - async (tx) => { - console.log("starting migration"); - const segmentsWithNoSurveys = await tx.segment.findMany({ - where: { - surveys: { - none: {}, - }, - }, - }); - - const surveyIds = segmentsWithNoSurveys.map((segment) => segment.id); - - await tx.segment.deleteMany({ - where: { - id: { - in: surveyIds, - }, - }, - }); - - console.log(`Deleted ${segmentsWithNoSurveys.length.toString()} segments with no surveys`); - - const appSurveysWithoutSegment = await tx.survey.findMany({ - where: { - type: "app", - segmentId: null, - }, - }); - - console.log(`Found ${appSurveysWithoutSegment.length.toString()} app surveys without a segment`); - - const segmentPromises = appSurveysWithoutSegment.map((appSurvey) => - tx.segment.create({ - data: { - title: appSurvey.id, - isPrivate: true, - environment: { connect: { id: appSurvey.environmentId } }, - surveys: { connect: { id: appSurvey.id } }, - }, - }) - ); - - await Promise.all(segmentPromises); - console.log("Migration completed"); - }, - { timeout: 50000 } - ); -} - -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(); diff --git a/packages/database/data-migrations/20240726124100_replace_verifyEmail_with_isVerifyEmailEnabled/data-migration.ts b/packages/database/data-migrations/20240726124100_replace_verifyEmail_with_isVerifyEmailEnabled/data-migration.ts deleted file mode 100644 index 5dab896e5e..0000000000 --- a/packages/database/data-migrations/20240726124100_replace_verifyEmail_with_isVerifyEmailEnabled/data-migration.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* eslint-disable no-console -- logging is allowed in migration scripts */ -import { PrismaClient } from "@prisma/client"; - -const prisma = new PrismaClient(); - -async function runMigration(): Promise { - await prisma.$transaction( - async (tx) => { - const startTime = Date.now(); - console.log("Starting data migration..."); - - // Fetch all surveys - const surveys = await tx.survey.findMany({ - select: { - id: true, - verifyEmail: true, - }, - }); - - if (surveys.length === 0) { - // stop the migration if there are no surveys - console.log("No Surveys found"); - return; - } - - let transformedSurveyCount = 0; - - const surveysWithEmailVerificationEnabled = surveys.filter( - (survey) => survey.verifyEmail !== null && survey.verifyEmail !== undefined - ); - - const updatePromises = surveysWithEmailVerificationEnabled.map((survey) => { - transformedSurveyCount++; - // Return the update promise - return tx.survey.update({ - where: { id: survey.id }, - data: { - isVerifyEmailEnabled: true, - verifyEmail: null, - }, - }); - }); - - await Promise.all(updatePromises); - console.log(transformedSurveyCount, " 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(); diff --git a/packages/database/data-migrations/20240801120500_thankYouCard_to_endings/data-migration.ts b/packages/database/data-migrations/20240801120500_thankYouCard_to_endings/data-migration.ts deleted file mode 100644 index 5a74eba259..0000000000 --- a/packages/database/data-migrations/20240801120500_thankYouCard_to_endings/data-migration.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* eslint-disable no-console -- logging is allowed in migration scripts */ -import { createId } from "@paralleldrive/cuid2"; -import { PrismaClient } from "@prisma/client"; -import { type TSurveyEndings } from "@formbricks/types/surveys/types"; - -interface Survey { - id: string; - thankYouCard: { - enabled: boolean; - title: string; - description: string; - } | null; - redirectUrl: string | null; -} -interface UpdatedSurvey extends Survey { - endings?: TSurveyEndings; -} - -const prisma = new PrismaClient(); - -async function runMigration(): Promise { - await prisma.$transaction( - async (tx) => { - const startTime = Date.now(); - console.log("Starting data migration..."); - - // Fetch all surveys - const surveys: Survey[] = (await tx.survey.findMany({ - select: { - id: true, - thankYouCard: true, - redirectUrl: true, - }, - })) as Survey[]; - - if (surveys.length === 0) { - // Stop the migration if there are no surveys - console.log("No Surveys found"); - return; - } - - console.log(`Total surveys found: ${surveys.length.toString()}`); - let transformedSurveyCount = 0; - - const updatePromises = surveys - .filter((s) => s.thankYouCard !== null) - .map((survey) => { - transformedSurveyCount++; - const updatedSurvey: UpdatedSurvey = structuredClone(survey) as UpdatedSurvey; - - if (survey.redirectUrl) { - updatedSurvey.endings = [ - { - type: "redirectToUrl", - label: "Redirect Url", - id: createId(), - url: survey.redirectUrl, - }, - ]; - } else if (survey.thankYouCard?.enabled) { - updatedSurvey.endings = [ - { - ...survey.thankYouCard, - type: "endScreen", - id: createId(), - }, - ]; - } else { - updatedSurvey.endings = []; - } - - // Return the update promise - return tx.survey.update({ - where: { id: survey.id }, - data: { - endings: updatedSurvey.endings, - thankYouCard: null, - redirectUrl: null, - }, - }); - }); - - 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(); diff --git a/packages/database/data-migrations/20240806120500_fix-logic-end-destination/data-migration.ts b/packages/database/data-migrations/20240806120500_fix-logic-end-destination/data-migration.ts deleted file mode 100644 index 48ea52ff15..0000000000 --- a/packages/database/data-migrations/20240806120500_fix-logic-end-destination/data-migration.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* 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 { - 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(); diff --git a/packages/database/data-migrations/20240807120500_cta_consent_dismissed_inconsistency/data-migration.ts b/packages/database/data-migrations/20240807120500_cta_consent_dismissed_inconsistency/data-migration.ts deleted file mode 100644 index 9b7758f6af..0000000000 --- a/packages/database/data-migrations/20240807120500_cta_consent_dismissed_inconsistency/data-migration.ts +++ /dev/null @@ -1,107 +0,0 @@ -/* eslint-disable @typescript-eslint/restrict-template-expressions -- using template strings for logging */ - -/* eslint-disable no-console -- logging is allowed in migration scripts */ -import { PrismaClient } from "@prisma/client"; -import { - type TSurveyQuestion, - type TSurveyQuestionId, - TSurveyQuestionTypeEnum, -} from "@formbricks/types/surveys/types"; - -const prisma = new PrismaClient(); - -async function runMigration(): Promise { - await prisma.$transaction( - async (tx) => { - const startTime = Date.now(); - console.log("Starting data migration..."); - - // Get all surveys with status not in draft and questions containing cta or consent - const relevantSurveys = await tx.survey.findMany({ - where: { - status: { - notIn: ["draft"], - }, - OR: [ - { - questions: { - array_contains: [{ type: "cta" }], - }, - }, - { - questions: { - array_contains: [{ type: "consent" }], - }, - }, - ], - }, - select: { - id: true, - questions: true, - }, - }); - - // Process each survey - const migrationPromises = relevantSurveys.map(async (survey) => { - const ctaOrConsentQuestionIds = survey.questions - .filter( - (ques: TSurveyQuestion) => - ques.type === TSurveyQuestionTypeEnum.CTA || ques.type === TSurveyQuestionTypeEnum.Consent - ) - .map((ques: TSurveyQuestion) => ques.id); - - const responses = await tx.response.findMany({ - where: { surveyId: survey.id }, - select: { id: true, data: true }, - }); - - return Promise.all( - responses.map(async (response) => { - const updatedData = { ...response.data }; - - ctaOrConsentQuestionIds.forEach((questionId: TSurveyQuestionId) => { - if (updatedData[questionId] && updatedData[questionId] === "dismissed") { - updatedData[questionId] = ""; - } - }); - - return tx.response.update({ - where: { id: response.id }, - data: { data: updatedData }, - }); - }) - ); - }); - - await Promise.all(migrationPromises); - - console.log(`Updated ${migrationPromises.length} questions in ${relevantSurveys.length} surveys`); - - const endTime = Date.now(); - console.log(`Data migration completed. Total time: ${((endTime - startTime) / 1000).toString()}s`); - }, - { - timeout: 900000, // 15 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(); diff --git a/packages/database/data-migrations/20240828122408_advanced_logic_editor/data-migration.ts b/packages/database/data-migrations/20240828122408_advanced_logic_editor/data-migration.ts deleted file mode 100644 index fce96a56eb..0000000000 --- a/packages/database/data-migrations/20240828122408_advanced_logic_editor/data-migration.ts +++ /dev/null @@ -1,316 +0,0 @@ -/* eslint-disable @typescript-eslint/restrict-template-expressions -- string interpolation is allowed in migration scripts */ - -/* eslint-disable no-console -- logging is allowed in migration scripts */ -import { createId } from "@paralleldrive/cuid2"; -import { PrismaClient } from "@prisma/client"; -import { - type TRightOperand, - type TSingleCondition, - type TSurveyEndings, - type TSurveyLogic, - type TSurveyLogicAction, - type TSurveyLogicConditionsOperator, - type TSurveyMultipleChoiceQuestion, - type TSurveyQuestion, - TSurveyQuestionTypeEnum, -} from "@formbricks/types/surveys/types"; - -const prisma = new PrismaClient(); - -interface TOldLogic { - condition: string; - value?: string | string[]; - destination: string; -} - -const isOldLogic = (logic: TOldLogic | TSurveyLogic): logic is TOldLogic => { - return Object.keys(logic).some((key) => ["condition", "destination", "value"].includes(key)); -}; - -const doesRightOperandExist = (operator: TSurveyLogicConditionsOperator): boolean => { - return ![ - "isAccepted", - "isBooked", - "isClicked", - "isCompletelySubmitted", - "isPartiallySubmitted", - "isSkipped", - "isSubmitted", - ].includes(operator); -}; - -const getChoiceId = (question: TSurveyMultipleChoiceQuestion, choiceText: string): string | undefined => { - const choiceOption = question.choices.find((choice) => choice.label.default === choiceText); - if (choiceOption) { - return choiceOption.id; - } - if (question.choices.at(-1)?.id === "other") { - return "other"; - } -}; - -const getRightOperandValue = ( - oldCondition: string, - oldValue: string | string[] | undefined, - question: TSurveyQuestion -): TRightOperand | undefined => { - if (["lessThan", "lessEqual", "greaterThan", "greaterEqual"].includes(oldCondition)) { - return { - type: "static", - value: parseInt(oldValue as string), - }; - } - - if (["equals", "notEquals"].includes(oldCondition)) { - if (["string", "number"].includes(typeof oldValue)) { - if (question.type === TSurveyQuestionTypeEnum.Rating || question.type === TSurveyQuestionTypeEnum.NPS) { - return { - type: "static", - value: parseInt(oldValue as string), - }; - } else if ( - question.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle || - question.type === TSurveyQuestionTypeEnum.MultipleChoiceMulti - ) { - const choiceId = getChoiceId(question, oldValue as string); - if (choiceId) { - return { - type: "static", - value: choiceId, - }; - } - return undefined; - } else if (question.type === TSurveyQuestionTypeEnum.PictureSelection) { - return { - type: "static", - value: oldValue as string, - }; - } - } - - return undefined; - } - - if (["includesAll", "includesOne"].includes(oldCondition)) { - let choiceIds: string[] = []; - - if (oldValue && Array.isArray(oldValue)) { - if ( - question.type === TSurveyQuestionTypeEnum.MultipleChoiceMulti || - question.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle - ) { - oldValue.forEach((choiceText) => { - const choiceId = getChoiceId(question, choiceText); - if (choiceId) { - choiceIds.push(choiceId); - } - }); - - choiceIds = Array.from(new Set(choiceIds)); - - return { - type: "static", - value: choiceIds, - }; - } - - return { - type: "static", - value: oldValue, - }; - } - - return undefined; - } - - return undefined; -}; - -// Helper function to convert old logic condition to new format -function convertLogicCondition( - oldCondition: string, - oldValue: string | string[] | undefined, - question: TSurveyQuestion -): TSingleCondition | undefined { - const operator = mapOldOperatorToNew(oldCondition, question.type); - - let rightOperandValue: TRightOperand | undefined; - - const doesRightOperandExistResult = doesRightOperandExist(operator); - if (doesRightOperandExistResult) { - rightOperandValue = getRightOperandValue(oldCondition, oldValue, question); - - if (!rightOperandValue) { - return undefined; - } - } - - const newCondition: TSingleCondition = { - id: createId(), - leftOperand: { - type: "question", - value: question.id, - }, - operator, - ...(doesRightOperandExistResult ? { rightOperand: rightOperandValue } : {}), - }; - - return newCondition; -} - -// Helper function to map old conditions to new ones -function mapOldOperatorToNew( - oldCondition: string, - questionType: TSurveyQuestionTypeEnum -): TSurveyLogicConditionsOperator { - const conditionMap: Record = { - accepted: "isAccepted", - clicked: "isClicked", - submitted: "isSubmitted", - skipped: "isSkipped", - equals: "equals", - notEquals: "doesNotEqual", - lessThan: "isLessThan", - lessEqual: "isLessThanOrEqual", - greaterThan: "isGreaterThan", - greaterEqual: "isGreaterThanOrEqual", - includesAll: "includesAllOf", - includesOne: "includesOneOf", - uploaded: "isSubmitted", // Assuming 'uploaded' maps to 'isSubmitted' - notUploaded: "isSkipped", // Assuming 'notUploaded' maps to 'isSkipped' - booked: "isBooked", - isCompletelySubmitted: "isCompletelySubmitted", - isPartiallySubmitted: "isPartiallySubmitted", - }; - - const newOpeator = conditionMap[oldCondition]; - - if (questionType === TSurveyQuestionTypeEnum.MultipleChoiceSingle && newOpeator === "includesOneOf") { - return "equalsOneOf"; - } - - return newOpeator; -} - -// Helper function to convert old logic to new format -function convertLogic( - surveyEndings: TSurveyEndings, - oldLogic: TOldLogic, - question: TSurveyQuestion -): TSurveyLogic | undefined { - if (!oldLogic.condition || !oldLogic.destination) { - return undefined; - } - - const condition = convertLogicCondition(oldLogic.condition, oldLogic.value, question); - - if (!condition) { - return undefined; - } - - let actionTarget = oldLogic.destination; - - if (actionTarget === "end") { - if (surveyEndings.length > 0) { - actionTarget = surveyEndings[0].id; - } else { - return undefined; - } - } - - const action: TSurveyLogicAction = { - id: createId(), - objective: "jumpToQuestion", - target: actionTarget, - }; - - return { - id: createId(), - conditions: { - id: createId(), - connector: "and", - conditions: [condition], - }, - actions: [action], - }; -} - -async function runMigration(): Promise { - await prisma.$transaction( - async (tx) => { - const startTime = Date.now(); - console.log("Starting survey logic migration..."); - - // Get all surveys with questions containing old logic - const relevantSurveys = await tx.survey.findMany({ - select: { - id: true, - questions: true, - endings: true, - }, - }); - - // Process each survey - const migrationPromises = relevantSurveys - .map((survey) => { - let doesThisSurveyHasOldLogic = false; - const questions: TSurveyQuestion[] = []; - - for (const question of survey.questions) { - if (question.logic && Array.isArray(question.logic) && question.logic.some(isOldLogic)) { - doesThisSurveyHasOldLogic = true; - const newLogic = (question.logic as unknown as TOldLogic[]) - .map((oldLogic) => convertLogic(survey.endings, oldLogic, question)) - .filter((logic) => logic !== undefined); - - questions.push({ ...question, logic: newLogic }); - } else { - questions.push(question); - } - } - - if (!doesThisSurveyHasOldLogic) { - return null; - } - - return tx.survey.update({ - where: { id: survey.id }, - data: { questions }, - }); - }) - .filter((promise) => promise !== null); - - console.log(`Found ${migrationPromises.length} surveys with old logic`); - - await Promise.all(migrationPromises); - - const endTime = Date.now(); - console.log( - `Survey logic migration completed. Total time: ${((endTime - startTime) / 1000).toString()}s` - ); - }, - { - timeout: 300000, // 5 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(); diff --git a/packages/database/data-migrations/20240904091113_removed_actions_table/data-migration.ts b/packages/database/data-migrations/20240904091113_removed_actions_table/data-migration.ts deleted file mode 100644 index d042ae32e4..0000000000 --- a/packages/database/data-migrations/20240904091113_removed_actions_table/data-migration.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* eslint-disable @typescript-eslint/restrict-template-expressions -- using template strings for logging */ - -/* eslint-disable no-console -- logging is allowed in migration scripts */ -import { PrismaClient } from "@prisma/client"; -import type { TBaseFilter, TBaseFilters } from "@formbricks/types/segment"; - -const prisma = new PrismaClient(); - -function removeActionFilters(filters: TBaseFilters): TBaseFilters { - const cleanedFilters = filters.reduce((acc: TBaseFilters, filter: TBaseFilter) => { - if (Array.isArray(filter.resource)) { - // If it's a group, recursively clean it - const cleanedGroup = removeActionFilters(filter.resource); - if (cleanedGroup.length > 0) { - acc.push({ - ...filter, - resource: cleanedGroup, - }); - } - // @ts-expect-error -- we're checking for an older type of filter - } else if (filter.resource.root.type !== "action") { - // If it's not an action filter, keep it - acc.push(filter); - } - // Action filters are implicitly removed by not being added to acc - return acc; - }, []); - - // Ensure the first filter in the group has a null connector - return cleanedFilters.map((filter, index) => { - if (index === 0) { - return { ...filter, connector: null }; - } - return filter; - }); -} - -async function runMigration(): Promise { - await prisma.$transaction( - async (tx) => { - console.log("Starting the data migration..."); - - const segmentsToUpdate = await tx.segment.findMany({}); - - console.log(`Found ${segmentsToUpdate.length} total segments`); - - let changedFiltersCount = 0; - - const updatePromises = segmentsToUpdate.map((segment) => { - const updatedFilters = removeActionFilters(segment.filters); - if (JSON.stringify(segment.filters) !== JSON.stringify(updatedFilters)) { - changedFiltersCount++; - } - - return tx.segment.update({ - where: { id: segment.id }, - data: { filters: updatedFilters }, - }); - }); - - await Promise.all(updatePromises); - console.log(`Successfully updated ${changedFiltersCount} segments`); - }, - { - 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(); diff --git a/packages/database/data-migrations/20240905120500_refactor_display_response_relationship/data-migration.ts b/packages/database/data-migrations/20240905120500_refactor_display_response_relationship/data-migration.ts deleted file mode 100644 index 73737784e6..0000000000 --- a/packages/database/data-migrations/20240905120500_refactor_display_response_relationship/data-migration.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* eslint-disable no-console -- logging is allowed in migration scripts */ -import { PrismaClient } from "@prisma/client"; - -const prisma = new PrismaClient(); -const TRANSACTION_TIMEOUT = 30 * 60 * 1000; // 30 minutes in milliseconds - -async function runMigration(): Promise { - const startTime = Date.now(); - console.log("Starting data migration..."); - - await prisma.$transaction( - async (transactionPrisma) => { - // Step 1: Use raw SQL to bulk update responses where responseId is not null in displays - console.log("Running bulk update for responses with valid responseId..."); - - const rawQueryResult = await transactionPrisma.$executeRaw` - WITH updated_displays AS ( - UPDATE public."Response" r - SET "displayId" = d.id - FROM public."Display" d - WHERE r.id = d."responseId" - RETURNING d.id - ) - UPDATE public."Display" - SET "responseId" = NULL - WHERE id IN (SELECT id FROM updated_displays); - `; - - console.log("Bulk update completed!"); - - // Step 2: Handle the case where a display has a responseId but the corresponding response does not exist - console.log("Handling displays where the responseId exists but the response is missing..."); - - // Find displays where responseId is not null but the corresponding response does not exist - const displaysWithMissingResponses = await transactionPrisma.display.findMany({ - where: { - responseId: { - not: null, - }, - }, - select: { - id: true, - responseId: true, - }, - }); - - const responseIds = displaysWithMissingResponses - .map((display) => display.responseId) - .filter((id): id is string => id !== null); - - // Check which of the responseIds actually exist in the responses table - const existingResponses = await transactionPrisma.response.findMany({ - where: { - id: { - in: responseIds, - }, - }, - select: { - id: true, - }, - }); - - const existingResponseIds = new Set(existingResponses.map((response) => response.id)); - - // Find displays where the responseId does not exist in the responses table - const displayIdsToDelete = displaysWithMissingResponses - .filter((display) => !existingResponseIds.has(display.responseId as unknown as string)) - .map((display) => display.id); - - if (displayIdsToDelete.length > 0) { - console.log( - `Deleting ${displayIdsToDelete.length.toString()} displays where the response is missing...` - ); - - await transactionPrisma.display.deleteMany({ - where: { - id: { - in: displayIdsToDelete, - }, - }, - }); - } - - console.log("Displays where the response was missing have been deleted."); - console.log("Data migration completed."); - console.log(`Affected rows: ${rawQueryResult.toString() + displayIdsToDelete.length.toString()}`); - }, - { - timeout: TRANSACTION_TIMEOUT, - } - ); - - const endTime = Date.now(); - console.log(`Data migration completed. Total time: ${((endTime - startTime) / 1000).toFixed(2)}s`); -} - -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(); diff --git a/packages/database/data-migrations/20240924123456_migrate_address_question/data-migration.ts b/packages/database/data-migrations/20240924123456_migrate_address_question/data-migration.ts deleted file mode 100644 index 79d29bd6a3..0000000000 --- a/packages/database/data-migrations/20240924123456_migrate_address_question/data-migration.ts +++ /dev/null @@ -1,122 +0,0 @@ -/* eslint-disable no-console -- logging is allowed in migration scripts */ -import { PrismaClient } from "@prisma/client"; -import { - type TSurveyAddressQuestion, - type TSurveyQuestion, - TSurveyQuestionTypeEnum, -} from "@formbricks/types/surveys/types"; - -const prisma = new PrismaClient(); -const TRANSACTION_TIMEOUT = 30 * 60 * 1000; // 30 minutes in milliseconds - -async function runMigration(): Promise { - const startTime = Date.now(); - console.log("Starting data migration..."); - - await prisma.$transaction( - async (transactionPrisma) => { - const surveysWithAddressQuestion = await transactionPrisma.survey.findMany({ - where: { - questions: { - array_contains: [{ type: "address" }], - }, - }, - }); - - console.log(`Found ${surveysWithAddressQuestion.length.toString()} surveys with address questions`); - - const updationPromises = []; - for (const survey of surveysWithAddressQuestion) { - const updatedQuestions = survey.questions.map((question: TSurveyQuestion) => { - if (question.type === TSurveyQuestionTypeEnum.Address) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- addressLine1 is not defined for unmigrated surveys - if (question.addressLine1 !== undefined) { - return null; - } - - const { - isAddressLine1Required, - isAddressLine2Required, - isCityRequired, - isStateRequired, - isZipRequired, - isCountryRequired, - ...rest - } = question as TSurveyAddressQuestion & { - isAddressLine1Required: boolean; - isAddressLine2Required: boolean; - isCityRequired: boolean; - isStateRequired: boolean; - isZipRequired: boolean; - isCountryRequired: boolean; - }; - - return { - ...rest, - addressLine1: { show: true, required: isAddressLine1Required }, - addressLine2: { show: true, required: isAddressLine2Required }, - city: { show: true, required: isCityRequired }, - state: { show: true, required: isStateRequired }, - zip: { show: true, required: isZipRequired }, - country: { show: true, required: isCountryRequired }, - }; - } - - return question; - }); - - const isUpdationNotRequired = updatedQuestions.some( - (question: TSurveyQuestion | null) => question === null - ); - - if (!isUpdationNotRequired) { - updationPromises.push( - transactionPrisma.survey.update({ - where: { - id: survey.id, - }, - data: { - questions: updatedQuestions.filter((question: TSurveyQuestion | null) => question !== null), - }, - }) - ); - } - } - - if (updationPromises.length === 0) { - console.log("No surveys require migration... Exiting"); - return; - } - - await Promise.all(updationPromises); - - console.log("Total surveys updated: ", updationPromises.length.toString()); - }, - { - timeout: TRANSACTION_TIMEOUT, - } - ); - - const endTime = Date.now(); - console.log(`Data migration completed. Total time: ${((endTime - startTime) / 1000).toFixed(2)}s`); -} - -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(); diff --git a/packages/database/data-migrations/20241002123456_migrate_survey_types/data-migration.ts b/packages/database/data-migrations/20241002123456_migrate_survey_types/data-migration.ts deleted file mode 100644 index 8d6800e790..0000000000 --- a/packages/database/data-migrations/20241002123456_migrate_survey_types/data-migration.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* eslint-disable no-console -- logging is allowed in migration scripts */ -import { PrismaClient } from "@prisma/client"; - -const prisma = new PrismaClient(); -const TRANSACTION_TIMEOUT = 30 * 60 * 1000; // 30 minutes in milliseconds - -async function runMigration(): Promise { - const startTime = Date.now(); - console.log("Starting data migration..."); - - await prisma.$transaction( - async (transactionPrisma) => { - const websiteSurveys = await transactionPrisma.survey.findMany({ - where: { type: "website" }, - }); - - const updationPromises = []; - - for (const websiteSurvey of websiteSurveys) { - updationPromises.push( - transactionPrisma.survey.update({ - where: { id: websiteSurvey.id }, - data: { - type: "app", - segment: { - connectOrCreate: { - where: { - environmentId_title: { - environmentId: websiteSurvey.environmentId, - title: websiteSurvey.id, - }, - }, - create: { - title: websiteSurvey.id, - isPrivate: true, - environmentId: websiteSurvey.environmentId, - }, - }, - }, - }, - }) - ); - } - - await Promise.all(updationPromises); - console.log(`Updated ${websiteSurveys.length.toString()} website surveys to app surveys`); - }, - { - timeout: TRANSACTION_TIMEOUT, - } - ); - - const endTime = Date.now(); - console.log(`Data migration completed. Total time: ${((endTime - startTime) / 1000).toFixed(2)}s`); -} - -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(); diff --git a/packages/database/data-migrations/20241010133706_xm_user_identification/data-migration.ts b/packages/database/data-migrations/20241010133706_xm_user_identification/data-migration.ts deleted file mode 100644 index 0a3fac29da..0000000000 --- a/packages/database/data-migrations/20241010133706_xm_user_identification/data-migration.ts +++ /dev/null @@ -1,270 +0,0 @@ -/* eslint-disable no-constant-condition -- Required for the while loop */ - -/* eslint-disable @typescript-eslint/no-unnecessary-condition -- Required for a while loop here */ - -/* eslint-disable no-console -- logging is allowed in migration scripts */ -import { PrismaClient } from "@prisma/client"; - -const prisma = new PrismaClient(); -const TRANSACTION_TIMEOUT = 30 * 60 * 1000; // 30 minutes in milliseconds - -async function runMigration(): Promise { - const startTime = Date.now(); - console.log("Starting data migration..."); - - await prisma.$transaction( - async (tx) => { - const totalContacts = await tx.contact.count(); - - // Check if any contacts still have a userId - const contactsWithUserId = await tx.contact.count({ - where: { - userId: { not: null }, - }, - }); - - // If no contacts have a userId, migration is already complete - if (totalContacts > 0 && contactsWithUserId === 0) { - console.log("Migration already completed. No contacts with userId found."); - return; - } - - const BATCH_SIZE = 10000; // Adjust based on your system's capacity - let skip = 0; - - while (true) { - // Ensure email, firstName, lastName attributeKeys exist for all environments - const allEnvironmentsInBatch = await tx.environment.findMany({ - select: { id: true }, - skip, - take: BATCH_SIZE, - }); - - if (allEnvironmentsInBatch.length === 0) { - break; - } - - console.log("Processing attributeKeys for", allEnvironmentsInBatch.length, "environments"); - - for (const env of allEnvironmentsInBatch) { - await tx.environment.update({ - where: { id: env.id }, - data: { - attributeKeys: { - upsert: [ - { - where: { - key_environmentId: { - key: "email", - environmentId: env.id, - }, - }, - update: { - type: "default", - isUnique: true, - }, - create: { - key: "email", - name: "Email", - description: "The email of a contact", - type: "default", - isUnique: true, - }, - }, - { - where: { - key_environmentId: { - key: "firstName", - environmentId: env.id, - }, - }, - update: { - type: "default", - }, - create: { - key: "firstName", - name: "First Name", - description: "Your contact's first name", - type: "default", - }, - }, - { - where: { - key_environmentId: { - key: "lastName", - environmentId: env.id, - }, - }, - update: { - type: "default", - }, - create: { - key: "lastName", - name: "Last Name", - description: "Your contact's last name", - type: "default", - }, - }, - { - where: { - key_environmentId: { - key: "userId", - environmentId: env.id, - }, - }, - update: { - type: "default", - isUnique: true, - }, - create: { - key: "userId", - name: "User ID", - description: "The user ID of a contact", - type: "default", - isUnique: true, - }, - }, - ], - }, - }, - }); - } - - skip += allEnvironmentsInBatch.length; - } - - const CONTACTS_BATCH_SIZE = 20000; - let processedContacts = 0; - - // delete userIds for these environments: - const { count } = await tx.contactAttribute.deleteMany({ - where: { - attributeKey: { - key: "userId", - }, - }, - }); - - console.log("Deleted userId attributes for", count, "contacts"); - - while (true) { - const contacts = await tx.contact.findMany({ - take: CONTACTS_BATCH_SIZE, - select: { - id: true, - userId: true, - environmentId: true, - }, - where: { - userId: { not: null }, - }, - }); - - if (contacts.length === 0) { - break; - } - - const environmentIdsByContacts = contacts.map((c) => c.environmentId); - - const attributeMap = new Map(); - - const userIdAttributeKeys = await tx.contactAttributeKey.findMany({ - where: { - key: "userId", - environmentId: { - in: environmentIdsByContacts, - }, - }, - select: { id: true, environmentId: true }, - }); - - userIdAttributeKeys.forEach((ak) => { - attributeMap.set(ak.environmentId, ak.id); - }); - - // Insert contactAttributes in bulk - await tx.contactAttribute.createMany({ - data: contacts.map((contact) => { - if (!contact.userId) { - throw new Error(`Contact with id ${contact.id} has no userId`); - } - - const userIdAttributeKey = attributeMap.get(contact.environmentId); - - if (!userIdAttributeKey) { - throw new Error(`Attribute key for userId not found for environment ${contact.environmentId}`); - } - - return { - contactId: contact.id, - value: contact.userId, - attributeKeyId: userIdAttributeKey, - }; - }), - }); - - await tx.contact.updateMany({ - where: { - id: { in: contacts.map((c) => c.id) }, - }, - data: { - userId: null, - }, - }); - - processedContacts += contacts.length; - - if (processedContacts > 0) { - console.log(`Processed ${processedContacts.toString()} contacts`); - } - } - - const totalContactsAfterMigration = await tx.contact.count(); - - console.log("Total contacts after migration:", totalContactsAfterMigration); - - // total attributes with userId: - const totalAttributes = await tx.contactAttribute.count({ - where: { - attributeKey: { - key: "userId", - }, - }, - }); - - console.log("Total attributes with userId now:", totalAttributes); - - if (totalContactsAfterMigration !== totalAttributes) { - throw new Error( - "Data migration failed. Total contacts after migration does not match total attributes with userId" - ); - } - }, - { - timeout: TRANSACTION_TIMEOUT, - } - ); - - const endTime = Date.now(); - console.log(`Data migration completed. Total time: ${((endTime - startTime) / 1000).toFixed(2)}s`); -} - -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(); diff --git a/packages/database/data-migrations/20241021123456_xm_segment_migration/data-migration.ts b/packages/database/data-migrations/20241021123456_xm_segment_migration/data-migration.ts deleted file mode 100644 index 703b1b9ffa..0000000000 --- a/packages/database/data-migrations/20241021123456_xm_segment_migration/data-migration.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unnecessary-condition -- Required for a while loop here */ - -/* eslint-disable no-console -- logging is allowed in migration scripts */ -import { PrismaClient } from "@prisma/client"; -import type { TBaseFilters, TSegmentAttributeFilter, TSegmentFilter } from "../../../types/segment"; - -const prisma = new PrismaClient(); -const TRANSACTION_TIMEOUT = 30 * 60 * 1000; // 30 minutes in milliseconds - -export const isResourceFilter = (resource: TSegmentFilter | TBaseFilters): resource is TSegmentFilter => { - return (resource as TSegmentFilter).root !== undefined; -}; - -const findAndReplace = (filters: TBaseFilters): TBaseFilters => { - const newFilters: TBaseFilters = []; - for (const filter of filters) { - if (isResourceFilter(filter.resource)) { - let { root } = filter.resource; - if (root.type === "attribute") { - // @ts-expect-error -- Legacy type - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- Legacy type - if (root.attributeClassName as string) { - root = { - type: "attribute", - // @ts-expect-error -- Legacy type - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Legacy type - contactAttributeKey: root.attributeClassName, - }; - - const newFilter = { - ...filter.resource, - root, - } as TSegmentAttributeFilter; - - newFilters.push({ - ...filter, - resource: newFilter, - }); - } - } else { - newFilters.push(filter); - } - } else { - const updatedResource = findAndReplace(filter.resource); - newFilters.push({ - ...filter, - resource: updatedResource, - }); - } - } - - return newFilters; -}; - -async function runMigration(): Promise { - const startTime = Date.now(); - console.log("Starting data migration..."); - - await prisma.$transaction( - async (tx) => { - const allSegments = await tx.segment.findMany(); - const updationPromises = []; - for (const segment of allSegments) { - updationPromises.push( - tx.segment.update({ - where: { id: segment.id }, - data: { - filters: findAndReplace(segment.filters), - }, - }) - ); - } - - await Promise.all(updationPromises); - }, - { - timeout: TRANSACTION_TIMEOUT, - } - ); - - const endTime = Date.now(); - console.log(`Data migration completed. Total time: ${((endTime - startTime) / 1000).toFixed(2)}s`); -} - -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(); diff --git a/packages/database/data-migrations/20241024123456_xm_attribute_removal/data-migration.ts b/packages/database/data-migrations/20241024123456_xm_attribute_removal/data-migration.ts deleted file mode 100644 index 96ecbc8244..0000000000 --- a/packages/database/data-migrations/20241024123456_xm_attribute_removal/data-migration.ts +++ /dev/null @@ -1,143 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unnecessary-condition -- Required for a while loop here */ - -/* eslint-disable no-console -- logging is allowed in migration scripts */ -import { PrismaClient } from "@prisma/client"; - -const prisma = new PrismaClient(); -const TRANSACTION_TIMEOUT = 30 * 60 * 1000; // 30 minutes in milliseconds - -async function runMigration(): Promise { - const startTime = Date.now(); - console.log("Starting data migration..."); - - await prisma.$transaction( - async (tx) => { - const emailAttributes = await tx.contactAttribute.findMany({ - where: { - attributeKey: { - key: "email", - }, - }, - select: { - id: true, - value: true, - contact: { - select: { - id: true, - environmentId: true, - createdAt: true, - }, - }, - createdAt: true, - }, - orderBy: { - createdAt: "asc", // Keep oldest attribute - }, - }); - - // 2. Group by environment and email - const emailsByEnvironment: Record< - // environmentId key - string, - // email record - Record - > = {}; - - // Group attributes by environment and email - for (const attr of emailAttributes) { - const { environmentId } = attr.contact; - const email = attr.value; - - if (!emailsByEnvironment[environmentId]) { - emailsByEnvironment[environmentId] = {}; - } - - if (!emailsByEnvironment[environmentId][email]) { - emailsByEnvironment[environmentId][email] = []; - } - - emailsByEnvironment[environmentId][email].push({ - id: attr.id, - contactId: attr.contact.id, - createdAt: attr.createdAt, - }); - } - - // 3. Identify and delete duplicates - const deletionSummary: Record< - string, - { - email: string; - deletedAttributeIds: string[]; - keptAttributeId: string; - }[] - > = {}; - - for (const [environmentId, emailGroups] of Object.entries(emailsByEnvironment)) { - deletionSummary[environmentId] = []; - - for (const [email, attributes] of Object.entries(emailGroups)) { - if (attributes.length > 1) { - // Sort by createdAt to ensure we keep the oldest - attributes.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); - - // Keep the first (oldest) attribute and delete the rest - const [kept, ...duplicates] = attributes; - const duplicateIds = duplicates.map((d) => d.id); - - // Delete duplicate attributes - await tx.contactAttribute.deleteMany({ - where: { - id: { - in: duplicateIds, - }, - }, - }); - - deletionSummary[environmentId].push({ - email, - deletedAttributeIds: duplicateIds, - keptAttributeId: kept.id, - }); - } - } - } - - // 4. Return summary of what was cleaned up - const summary = { - totalDuplicateAttributesRemoved: Object.values(deletionSummary).reduce( - (acc, env) => acc + env.reduce((sum, item) => sum + item.deletedAttributeIds.length, 0), - 0 - ), - }; - - console.log("Data migration completed. Summary: ", summary); - }, - { - timeout: TRANSACTION_TIMEOUT, - } - ); - - const endTime = Date.now(); - console.log(`Data migration completed. Total time: ${((endTime - startTime) / 1000).toFixed(2)}s`); -} - -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(); diff --git a/packages/database/data-migrations/20241107161932_add_teams/data-migration.ts b/packages/database/data-migrations/20241107161932_add_teams/data-migration.ts deleted file mode 100644 index b007abdbcd..0000000000 --- a/packages/database/data-migrations/20241107161932_add_teams/data-migration.ts +++ /dev/null @@ -1,326 +0,0 @@ -/* eslint-disable no-console -- logging is allowed in migration scripts */ -import { PrismaClient } from "@prisma/client"; - -const prisma = new PrismaClient(); -const TRANSACTION_TIMEOUT = 30 * 60 * 1000; // 30 minutes in milliseconds - -interface TInvite { - organizationId: string; - deprecatedRole: "owner" | "admin" | "editor" | "developer" | "viewer"; - email: string; - id: string; - creatorId: string; - createdAt: Date; - expiresAt: Date; - name?: string | null | undefined; - acceptorId?: string; -} - -async function runMigration(): Promise { - const startTime = Date.now(); - console.log("Starting data migration..."); - - await prisma.$transaction( - async (transactionPrisma) => { - // Fetch all invites and group them by organizationId - const invites = (await transactionPrisma.invite.findMany({ - select: { - id: true, - organizationId: true, - deprecatedRole: true, - }, - })) as Pick[]; - - // Group invites by organizationId - const groupInvitesMap = new Map< - string, - { id: string; organizationId: string; deprecatedRole: TInvite["deprecatedRole"] }[] - >(); - invites.forEach((invite) => { - if (!groupInvitesMap.has(invite.organizationId)) { - groupInvitesMap.set(invite.organizationId, []); - } - - groupInvitesMap.get(invite.organizationId)?.push(invite); - }); - - const groupInvites = groupInvitesMap.entries(); - - // Process each organization's invites to update roles accordingly - await Promise.all( - Array.from(groupInvites).map(async ([organizationId, organizationInvites]) => { - const adminInvites = organizationInvites.filter((invite) => invite.deprecatedRole === "admin"); - const otherRoles = organizationInvites.filter((invite) => invite.deprecatedRole !== "admin"); - - // If no admin invites exist, skip this organization - if (adminInvites.length === 0) { - return; - } - - // Update admin invites to "manager" if there are non-admin roles - if (otherRoles.length > 0) { - return transactionPrisma.invite.updateMany({ - where: { - id: { - in: adminInvites.map((invite) => invite.id), - }, - }, - data: { - role: "manager", - }, - }); - } - - // Check if there are other memberships (editor, developer, viewer) - const otherMembershipsCount = await transactionPrisma.membership.count({ - where: { - organizationId, - deprecatedRole: { - in: ["editor", "developer", "viewer"], - }, - }, - }); - - // If there are other memberships, update admin invites to "manager" - if (otherMembershipsCount > 0) { - return transactionPrisma.invite.updateMany({ - where: { - id: { - in: adminInvites.map((invite) => invite.id), - }, - }, - data: { - role: "manager", - }, - }); - } - - // If no other memberships exist, promote admins to "owner", case where the organization has only owner and admin memberships as well as invite - return transactionPrisma.invite.updateMany({ - where: { - id: { - in: adminInvites.map((invite) => invite.id), - }, - }, - data: { - role: "owner", - }, - }); - }) - ); - - // Set all invites with roles of editor, developer, or viewer to "member" - await transactionPrisma.invite.updateMany({ - where: { - deprecatedRole: { - in: ["editor", "developer", "viewer"], - }, - }, - data: { - role: "member", - }, - }); - - // Fetch non-owner memberships and group them by organizationId - const nonOwnerMemberships = await transactionPrisma.membership.findMany({ - where: { - role: { - notIn: ["owner"], - }, - }, - select: { - userId: true, - organizationId: true, - deprecatedRole: true, - organization: { - select: { - invites: { - where: { - deprecatedRole: { - not: "admin", - }, - }, - select: { - deprecatedRole: true, - }, - }, - }, - }, - }, - }); - - const groupedMemberships = new Map(); - const otherInvitesCount = new Map(); - - nonOwnerMemberships.forEach((membership) => { - if (!groupedMemberships.has(membership.organizationId)) { - groupedMemberships.set(membership.organizationId, []); - } - - if (!otherInvitesCount.has(membership.organizationId)) { - otherInvitesCount.set(membership.organizationId, membership.organization.invites.length); - } - - groupedMemberships.get(membership.organizationId)?.push(membership); - }); - - const groupedMembershipsEntries = groupedMemberships.entries(); - - // Process each organization's memberships to update or create teams - await Promise.all( - Array.from(groupedMembershipsEntries).map(async ([organizationId, memberships]) => { - const adminMembership = memberships.filter((membership) => membership.deprecatedRole === "admin"); - const developerMembership = memberships.filter( - (membership) => membership.deprecatedRole === "developer" - ); - const editorMembership = memberships.filter((membership) => membership.deprecatedRole === "editor"); - const viewerMembership = memberships.filter((membership) => membership.deprecatedRole === "viewer"); - - const otherMemberships = - developerMembership.length + editorMembership.length + viewerMembership.length; - - // If admin members exist alongside others, set their role to "manager" - if (adminMembership.length) { - const otherInvites = otherInvitesCount.get(organizationId) ?? 0; - - await transactionPrisma.membership.updateMany({ - where: { - organizationId, - deprecatedRole: "admin", - }, - data: { - role: otherMemberships || otherInvites > 0 ? "manager" : "owner", - }, - }); - } - - // Create team and update roles for developer or editor memberships - if (developerMembership.length || editorMembership.length || viewerMembership.length) { - const productIdsInOrganization = await transactionPrisma.product.findMany({ - where: { - organizationId, - }, - select: { - id: true, - }, - }); - - // Create an "all access" team for developer and editor roles - if (developerMembership.length || editorMembership.length) { - await transactionPrisma.team.create({ - data: { - organizationId, - name: "all access", - teamUsers: { - create: [...developerMembership, ...editorMembership].map((membership) => ({ - userId: membership.userId, - role: "contributor", - })), - }, - productTeams: { - create: productIdsInOrganization.map((product) => ({ - productId: product.id, - permission: "manage", - })), - }, - }, - }); - - await transactionPrisma.membership.updateMany({ - where: { - organizationId, - deprecatedRole: { - in: ["developer", "editor"], - }, - }, - data: { - role: "member", - }, - }); - } - - // Create a "read only" team for viewer roles - if (viewerMembership.length) { - await transactionPrisma.team.create({ - data: { - organizationId, - name: "read only", - teamUsers: { - create: viewerMembership.map((membership) => ({ - userId: membership.userId, - role: "contributor", - })), - }, - productTeams: { - create: productIdsInOrganization.map((product) => ({ - productId: product.id, - permission: "read", - })), - }, - }, - }); - - await transactionPrisma.membership.updateMany({ - where: { - organizationId, - deprecatedRole: "viewer", - }, - data: { - role: "member", - }, - }); - } - } - }) - ); - - await transactionPrisma.membership.updateMany({ - where: { - deprecatedRole: "owner", - }, - data: { - role: "owner", - }, - }); - - // Clear out the old "role" field in invites after migration - await transactionPrisma.invite.updateMany({ - data: { - deprecatedRole: null, - }, - }); - - await transactionPrisma.membership.updateMany({ - data: { - deprecatedRole: null, - }, - }); - }, - { - timeout: TRANSACTION_TIMEOUT, - } - ); - - const endTime = Date.now(); - console.log(`Data migration completed. Total time: ${((endTime - startTime) / 1000).toFixed(2)}s`); -} - -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(); diff --git a/packages/database/data-migrations/20241118123456_update_org_limits/data-migration.ts b/packages/database/data-migrations/20241118123456_update_org_limits/data-migration.ts deleted file mode 100644 index 29ee7dcaf3..0000000000 --- a/packages/database/data-migrations/20241118123456_update_org_limits/data-migration.ts +++ /dev/null @@ -1,183 +0,0 @@ -/* eslint-disable no-console -- logging is allowed in migration scripts */ -import { PrismaClient } from "@prisma/client"; - -const prisma = new PrismaClient(); -const TRANSACTION_TIMEOUT = 3 * 60 * 1000; // 3 minutes in milliseconds - -type Plan = "free" | "startup" | "scale"; - -interface TOrganization { - id: string; - billing: { - plan: Plan; - limits: { - monthly: { - responses: number; - miu: number; - }; - }; - }; -} - -export const BILLING_LIMITS = { - free: { - RESPONSES: 1500, - MIU: 2000, - }, - startup: { - RESPONSES: 5000, - MIU: 7500, - }, - scale: { - RESPONSES: 10000, - MIU: 30000, - }, -} as const; - -async function runMigration(): Promise { - const startTime = Date.now(); - console.log("Starting data migration..."); - - await prisma.$transaction( - async (transactionPrisma) => { - const organizations = (await transactionPrisma.organization.findMany({ - where: { - OR: [ - { - AND: [ - { - billing: { - path: ["plan"], - equals: "free", - }, - }, - { - billing: { - path: ["limits", "monthly", "miu"], - not: 2000, - }, - }, - { - billing: { - path: ["limits", "monthly", "responses"], - not: 1500, - }, - }, - ], - }, - { - AND: [ - { - billing: { - path: ["plan"], - equals: "startup", - }, - }, - { - billing: { - path: ["limits", "monthly", "miu"], - not: 7500, - }, - }, - { - billing: { - path: ["limits", "monthly", "responses"], - not: 5000, - }, - }, - ], - }, - { - AND: [ - { - billing: { - path: ["plan"], - equals: "scale", - }, - }, - { - billing: { - path: ["limits", "monthly", "miu"], - not: 30000, - }, - }, - { - billing: { - path: ["limits", "monthly", "responses"], - not: 10000, - }, - }, - ], - }, - ], - }, - select: { - id: true, - billing: true, - }, - })) as TOrganization[]; - - const updationPromises = []; - - for (const organization of organizations) { - const plan = organization.billing.plan; - const limits = BILLING_LIMITS[plan]; - - let billing = organization.billing; - - billing = { - ...billing, - limits: { - ...billing.limits, - monthly: { - ...billing.limits.monthly, - responses: limits.RESPONSES, - miu: limits.MIU, - }, - }, - }; - - const updatePromise = transactionPrisma.organization.update({ - where: { - id: organization.id, - }, - data: { - billing, - }, - }); - - updationPromises.push(updatePromise); - } - - await Promise.all(updationPromises); - - console.log(`Updated ${organizations.length.toString()} organizations`); - }, - { - timeout: TRANSACTION_TIMEOUT, - } - ); - - const endTime = Date.now(); - console.log(`Data migration completed. Total time: ${((endTime - startTime) / 1000).toFixed(2)}s`); -} - -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(); diff --git a/packages/database/data-migrations/20241120150728_product_revamp/data-migration.ts b/packages/database/data-migrations/20241120150728_product_revamp/data-migration.ts deleted file mode 100644 index ea20dcd97f..0000000000 --- a/packages/database/data-migrations/20241120150728_product_revamp/data-migration.ts +++ /dev/null @@ -1,99 +0,0 @@ -/* eslint-disable no-console -- logging is allowed in migration scripts */ -import { Prisma, PrismaClient } from "@prisma/client"; - -const prisma = new PrismaClient(); -const TRANSACTION_TIMEOUT = 3 * 60 * 1000; // 3 minutes in milliseconds - -type Plan = "free" | "startup" | "scale" | "enterprise"; - -const projectsLimitByPlan: Record = { - free: 3, - startup: null, - scale: null, - enterprise: null, -}; - -async function runMigration(): Promise { - const startTime = Date.now(); - console.log("Starting data migration..."); - - await prisma.$transaction( - async (transactionPrisma) => { - const organizations = await transactionPrisma.organization.findMany({ - where: { - billing: { - path: ["limits", "projects"], - equals: Prisma.DbNull, - }, - }, - select: { - id: true, - billing: true, - }, - }); - - const updateOrganizationPromises = organizations.map((org) => - transactionPrisma.organization.update({ - where: { - id: org.id, - }, - data: { - billing: { - ...org.billing, - limits: { - ...org.billing.limits, - projects: projectsLimitByPlan[org.billing.plan as Plan], - }, - }, - }, - }) - ); - - await Promise.all(updateOrganizationPromises); - - console.log(`Updated ${updateOrganizationPromises.length.toString()} organizations`); - - const updatedemptyConfigProjects = await transactionPrisma.project.updateMany({ - where: { - config: { - equals: {}, - }, - }, - data: { - config: { - channel: null, - industry: null, - }, - }, - }); - - console.log(`Updated ${updatedemptyConfigProjects.count.toString()} projects with empty config`); - }, - { - timeout: TRANSACTION_TIMEOUT, - } - ); - - const endTime = Date.now(); - console.log(`Data migration completed. Total time: ${((endTime - startTime) / 1000).toFixed(2)}s`); -} - -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(); diff --git a/packages/database/migrations/20230329205933_init/migration.sql b/packages/database/migration/20230329205933_init/migration.sql similarity index 100% rename from packages/database/migrations/20230329205933_init/migration.sql rename to packages/database/migration/20230329205933_init/migration.sql diff --git a/packages/database/migrations/20230405105937_add_api_keys_to_environments/migration.sql b/packages/database/migration/20230405105937_add_api_keys_to_environments/migration.sql similarity index 100% rename from packages/database/migrations/20230405105937_add_api_keys_to_environments/migration.sql rename to packages/database/migration/20230405105937_add_api_keys_to_environments/migration.sql diff --git a/packages/database/migrations/20230406124657_person_id_optional_in_response/migration.sql b/packages/database/migration/20230406124657_person_id_optional_in_response/migration.sql similarity index 100% rename from packages/database/migrations/20230406124657_person_id_optional_in_response/migration.sql rename to packages/database/migration/20230406124657_person_id_optional_in_response/migration.sql diff --git a/packages/database/migrations/20230412100209_add_thankyoucard/migration.sql b/packages/database/migration/20230412100209_add_thankyoucard/migration.sql similarity index 100% rename from packages/database/migrations/20230412100209_add_thankyoucard/migration.sql rename to packages/database/migration/20230412100209_add_thankyoucard/migration.sql diff --git a/packages/database/migrations/20230418074110_add_survey_types/migration.sql b/packages/database/migration/20230418074110_add_survey_types/migration.sql similarity index 100% rename from packages/database/migrations/20230418074110_add_survey_types/migration.sql rename to packages/database/migration/20230418074110_add_survey_types/migration.sql diff --git a/packages/database/migrations/20230418084158_display_person_optional/migration.sql b/packages/database/migration/20230418084158_display_person_optional/migration.sql similarity index 100% rename from packages/database/migrations/20230418084158_display_person_optional/migration.sql rename to packages/database/migration/20230418084158_display_person_optional/migration.sql diff --git a/packages/database/migrations/20230419181441_add_onboarding_displayed_property_with_default_of_true/migration.sql b/packages/database/migration/20230419181441_add_onboarding_displayed_property_with_default_of_true/migration.sql similarity index 100% rename from packages/database/migrations/20230419181441_add_onboarding_displayed_property_with_default_of_true/migration.sql rename to packages/database/migration/20230419181441_add_onboarding_displayed_property_with_default_of_true/migration.sql diff --git a/packages/database/migrations/20230429141942_add_role_objective_and_intention_to_user/migration.sql b/packages/database/migration/20230429141942_add_role_objective_and_intention_to_user/migration.sql similarity index 100% rename from packages/database/migrations/20230429141942_add_role_objective_and_intention_to_user/migration.sql rename to packages/database/migration/20230429141942_add_role_objective_and_intention_to_user/migration.sql diff --git a/packages/database/migrations/20230503105347_add_onboarding/migration.sql b/packages/database/migration/20230503105347_add_onboarding/migration.sql similarity index 100% rename from packages/database/migrations/20230503105347_add_onboarding/migration.sql rename to packages/database/migration/20230503105347_add_onboarding/migration.sql diff --git a/packages/database/migrations/20230503132723_rename_onboarding_flag/migration.sql b/packages/database/migration/20230503132723_rename_onboarding_flag/migration.sql similarity index 100% rename from packages/database/migrations/20230503132723_rename_onboarding_flag/migration.sql rename to packages/database/migration/20230503132723_rename_onboarding_flag/migration.sql diff --git a/packages/database/migrations/20230505085230_add_webhooks/migration.sql b/packages/database/migration/20230505085230_add_webhooks/migration.sql similarity index 100% rename from packages/database/migrations/20230505085230_add_webhooks/migration.sql rename to packages/database/migration/20230505085230_add_webhooks/migration.sql diff --git a/packages/database/migrations/20230515101242_add_attribute_filter/migration.sql b/packages/database/migration/20230515101242_add_attribute_filter/migration.sql similarity index 100% rename from packages/database/migrations/20230515101242_add_attribute_filter/migration.sql rename to packages/database/migration/20230515101242_add_attribute_filter/migration.sql diff --git a/packages/database/migrations/20230517145313_change_brand_color_default/migration.sql b/packages/database/migration/20230517145313_change_brand_color_default/migration.sql similarity index 100% rename from packages/database/migrations/20230517145313_change_brand_color_default/migration.sql rename to packages/database/migration/20230517145313_change_brand_color_default/migration.sql diff --git a/packages/database/migrations/20230519120218_add_google_oauth/migration.sql b/packages/database/migration/20230519120218_add_google_oauth/migration.sql similarity index 100% rename from packages/database/migrations/20230519120218_add_google_oauth/migration.sql rename to packages/database/migration/20230519120218_add_google_oauth/migration.sql diff --git a/packages/database/migrations/20230523125921_add_notification_settings_to_user/migration.sql b/packages/database/migration/20230523125921_add_notification_settings_to_user/migration.sql similarity index 100% rename from packages/database/migrations/20230523125921_add_notification_settings_to_user/migration.sql rename to packages/database/migration/20230523125921_add_notification_settings_to_user/migration.sql diff --git a/packages/database/migrations/20230529092700_add_formbricks_signature_to_product/migration.sql b/packages/database/migration/20230529092700_add_formbricks_signature_to_product/migration.sql similarity index 100% rename from packages/database/migrations/20230529092700_add_formbricks_signature_to_product/migration.sql rename to packages/database/migration/20230529092700_add_formbricks_signature_to_product/migration.sql diff --git a/packages/database/migrations/20230529092737_make_formbricks_signature_default/migration.sql b/packages/database/migration/20230529092737_make_formbricks_signature_default/migration.sql similarity index 100% rename from packages/database/migrations/20230529092737_make_formbricks_signature_default/migration.sql rename to packages/database/migration/20230529092737_make_formbricks_signature_default/migration.sql diff --git a/packages/database/migrations/20230529101210_add_autoclose/migration.sql b/packages/database/migration/20230529101210_add_autoclose/migration.sql similarity index 100% rename from packages/database/migrations/20230529101210_add_autoclose/migration.sql rename to packages/database/migration/20230529101210_add_autoclose/migration.sql diff --git a/packages/database/migrations/20230531143258_remove_user_attributes_from_response/migration.sql b/packages/database/migration/20230531143258_remove_user_attributes_from_response/migration.sql similarity index 100% rename from packages/database/migrations/20230531143258_remove_user_attributes_from_response/migration.sql rename to packages/database/migration/20230531143258_remove_user_attributes_from_response/migration.sql diff --git a/packages/database/migrations/20230608112129_add_survey_delay/migration.sql b/packages/database/migration/20230608112129_add_survey_delay/migration.sql similarity index 100% rename from packages/database/migrations/20230608112129_add_survey_delay/migration.sql rename to packages/database/migration/20230608112129_add_survey_delay/migration.sql diff --git a/packages/database/migrations/20230613035154_add_archived_to_attribute_class/migration.sql b/packages/database/migration/20230613035154_add_archived_to_attribute_class/migration.sql similarity index 100% rename from packages/database/migrations/20230613035154_add_archived_to_attribute_class/migration.sql rename to packages/database/migration/20230613035154_add_archived_to_attribute_class/migration.sql diff --git a/packages/database/migrations/20230613074826_add_response_notes/migration.sql b/packages/database/migration/20230613074826_add_response_notes/migration.sql similarity index 100% rename from packages/database/migrations/20230613074826_add_response_notes/migration.sql rename to packages/database/migration/20230613074826_add_response_notes/migration.sql diff --git a/packages/database/migrations/20230616174129_add_auto_complete_in_survey_schema/migration.sql b/packages/database/migration/20230616174129_add_auto_complete_in_survey_schema/migration.sql similarity index 100% rename from packages/database/migrations/20230616174129_add_auto_complete_in_survey_schema/migration.sql rename to packages/database/migration/20230616174129_add_auto_complete_in_survey_schema/migration.sql diff --git a/packages/database/migrations/20230618112915_person_attributes/migration.sql b/packages/database/migration/20230618112915_person_attributes/migration.sql similarity index 100% rename from packages/database/migrations/20230618112915_person_attributes/migration.sql rename to packages/database/migration/20230618112915_person_attributes/migration.sql diff --git a/packages/database/migrations/20230624161355_add_tags/migration.sql b/packages/database/migration/20230624161355_add_tags/migration.sql similarity index 100% rename from packages/database/migrations/20230624161355_add_tags/migration.sql rename to packages/database/migration/20230624161355_add_tags/migration.sql diff --git a/packages/database/migrations/20230626105714_add_redirect_url/migration.sql b/packages/database/migration/20230626105714_add_redirect_url/migration.sql similarity index 100% rename from packages/database/migrations/20230626105714_add_redirect_url/migration.sql rename to packages/database/migration/20230626105714_add_redirect_url/migration.sql diff --git a/packages/database/migrations/20230628113957_add_placement/migration.sql b/packages/database/migration/20230628113957_add_placement/migration.sql similarity index 100% rename from packages/database/migrations/20230628113957_add_placement/migration.sql rename to packages/database/migration/20230628113957_add_placement/migration.sql diff --git a/packages/database/migrations/20230710112555_add_survey_ids_to_webhooks/migration.sql b/packages/database/migration/20230710112555_add_survey_ids_to_webhooks/migration.sql similarity index 100% rename from packages/database/migrations/20230710112555_add_survey_ids_to_webhooks/migration.sql rename to packages/database/migration/20230710112555_add_survey_ids_to_webhooks/migration.sql diff --git a/packages/database/migrations/20230711103042_add_close_on_date/migration.sql b/packages/database/migration/20230711103042_add_close_on_date/migration.sql similarity index 100% rename from packages/database/migrations/20230711103042_add_close_on_date/migration.sql rename to packages/database/migration/20230711103042_add_close_on_date/migration.sql diff --git a/packages/database/migrations/20230711110136_add_survey_closed_message/migration.sql b/packages/database/migration/20230711110136_add_survey_closed_message/migration.sql similarity index 100% rename from packages/database/migrations/20230711110136_add_survey_closed_message/migration.sql rename to packages/database/migration/20230711110136_add_survey_closed_message/migration.sql diff --git a/packages/database/migrations/20230712183558_add_expires_at_to_session/migration.sql b/packages/database/migration/20230712183558_add_expires_at_to_session/migration.sql similarity index 100% rename from packages/database/migrations/20230712183558_add_expires_at_to_session/migration.sql rename to packages/database/migration/20230712183558_add_expires_at_to_session/migration.sql diff --git a/packages/database/migrations/20230807133058_add_highlight_border_color/migration.sql b/packages/database/migration/20230807133058_add_highlight_border_color/migration.sql similarity index 100% rename from packages/database/migrations/20230807133058_add_highlight_border_color/migration.sql rename to packages/database/migration/20230807133058_add_highlight_border_color/migration.sql diff --git a/packages/database/migrations/20230809132511_add_name_to_webhook/migration.sql b/packages/database/migration/20230809132511_add_name_to_webhook/migration.sql similarity index 100% rename from packages/database/migrations/20230809132511_add_name_to_webhook/migration.sql rename to packages/database/migration/20230809132511_add_name_to_webhook/migration.sql diff --git a/packages/database/migrations/20230901053500_adds_verify_email/migration.sql b/packages/database/migration/20230901053500_adds_verify_email/migration.sql similarity index 100% rename from packages/database/migrations/20230901053500_adds_verify_email/migration.sql rename to packages/database/migration/20230901053500_adds_verify_email/migration.sql diff --git a/packages/database/migrations/20230913115355_add_source_to_webhooks/migration.sql b/packages/database/migration/20230913115355_add_source_to_webhooks/migration.sql similarity index 100% rename from packages/database/migrations/20230913115355_add_source_to_webhooks/migration.sql rename to packages/database/migration/20230913115355_add_source_to_webhooks/migration.sql diff --git a/packages/database/migrations/20230915143725_remove_survey_status_archived/migration.sql b/packages/database/migration/20230915143725_remove_survey_status_archived/migration.sql similarity index 100% rename from packages/database/migrations/20230915143725_remove_survey_status_archived/migration.sql rename to packages/database/migration/20230915143725_remove_survey_status_archived/migration.sql diff --git a/packages/database/migrations/20230915154251_add_integration/migration.sql b/packages/database/migration/20230915154251_add_integration/migration.sql similarity index 100% rename from packages/database/migrations/20230915154251_add_integration/migration.sql rename to packages/database/migration/20230915154251_add_integration/migration.sql diff --git a/packages/database/migrations/20230918025340_add_is_resolved_and_is_edited_to_response_note/migration.sql b/packages/database/migration/20230918025340_add_is_resolved_and_is_edited_to_response_note/migration.sql similarity index 100% rename from packages/database/migrations/20230918025340_add_is_resolved_and_is_edited_to_response_note/migration.sql rename to packages/database/migration/20230918025340_add_is_resolved_and_is_edited_to_response_note/migration.sql diff --git a/packages/database/migrations/20231003113835_add_single_use_id_to_survey/migration.sql b/packages/database/migration/20231003113835_add_single_use_id_to_survey/migration.sql similarity index 100% rename from packages/database/migrations/20231003113835_add_single_use_id_to_survey/migration.sql rename to packages/database/migration/20231003113835_add_single_use_id_to_survey/migration.sql diff --git a/packages/database/migrations/20231003122730_survey_styling_overwrites/migration.sql b/packages/database/migration/20231003122730_survey_styling_overwrites/migration.sql similarity index 100% rename from packages/database/migrations/20231003122730_survey_styling_overwrites/migration.sql rename to packages/database/migration/20231003122730_survey_styling_overwrites/migration.sql diff --git a/packages/database/migrations/20231004115913_add_short_url/migration.sql b/packages/database/migration/20231004115913_add_short_url/migration.sql similarity index 100% rename from packages/database/migrations/20231004115913_add_short_url/migration.sql rename to packages/database/migration/20231004115913_add_short_url/migration.sql diff --git a/packages/database/migrations/20231008155535_add_response_id_to_display/migration.sql b/packages/database/migration/20231008155535_add_response_id_to_display/migration.sql similarity index 100% rename from packages/database/migrations/20231008155535_add_response_id_to_display/migration.sql rename to packages/database/migration/20231008155535_add_response_id_to_display/migration.sql diff --git a/packages/database/migrations/20231009074251_add_2fa/migration.sql b/packages/database/migration/20231009074251_add_2fa/migration.sql similarity index 100% rename from packages/database/migrations/20231009074251_add_2fa/migration.sql rename to packages/database/migration/20231009074251_add_2fa/migration.sql diff --git a/packages/database/migrations/20231011070343_unique_product_name/migration.sql b/packages/database/migration/20231011070343_unique_product_name/migration.sql similarity index 100% rename from packages/database/migrations/20231011070343_unique_product_name/migration.sql rename to packages/database/migration/20231011070343_unique_product_name/migration.sql diff --git a/packages/database/migrations/20231015165646_add_hidden_fields/migration.sql b/packages/database/migration/20231015165646_add_hidden_fields/migration.sql similarity index 100% rename from packages/database/migrations/20231015165646_add_hidden_fields/migration.sql rename to packages/database/migration/20231015165646_add_hidden_fields/migration.sql diff --git a/packages/database/migrations/20231016093655_add_pin_to_survey/migration.sql b/packages/database/migration/20231016093655_add_pin_to_survey/migration.sql similarity index 100% rename from packages/database/migrations/20231016093655_add_pin_to_survey/migration.sql rename to packages/database/migration/20231016093655_add_pin_to_survey/migration.sql diff --git a/packages/database/migrations/20231016190223_add_welcomecard/migration.sql b/packages/database/migration/20231016190223_add_welcomecard/migration.sql similarity index 100% rename from packages/database/migrations/20231016190223_add_welcomecard/migration.sql rename to packages/database/migration/20231016190223_add_welcomecard/migration.sql diff --git a/packages/database/migrations/20231019160204_add_airtable_integration/migration.sql b/packages/database/migration/20231019160204_add_airtable_integration/migration.sql similarity index 100% rename from packages/database/migrations/20231019160204_add_airtable_integration/migration.sql rename to packages/database/migration/20231019160204_add_airtable_integration/migration.sql diff --git a/packages/database/migrations/20231020073124_pin_as_string/migration.sql b/packages/database/migration/20231020073124_pin_as_string/migration.sql similarity index 100% rename from packages/database/migrations/20231020073124_pin_as_string/migration.sql rename to packages/database/migration/20231020073124_pin_as_string/migration.sql diff --git a/packages/database/migrations/20231020082319_add_azuread/migration.sql b/packages/database/migration/20231020082319_add_azuread/migration.sql similarity index 100% rename from packages/database/migrations/20231020082319_add_azuread/migration.sql rename to packages/database/migration/20231020082319_add_azuread/migration.sql diff --git a/packages/database/migrations/20231030105533_add_cascade_delete_to_integrations/migration.sql b/packages/database/migration/20231030105533_add_cascade_delete_to_integrations/migration.sql similarity index 100% rename from packages/database/migrations/20231030105533_add_cascade_delete_to_integrations/migration.sql rename to packages/database/migration/20231030105533_add_cascade_delete_to_integrations/migration.sql diff --git a/packages/database/migrations/20231030174314_add_billing_to_team/migration.sql b/packages/database/migration/20231030174314_add_billing_to_team/migration.sql similarity index 100% rename from packages/database/migrations/20231030174314_add_billing_to_team/migration.sql rename to packages/database/migration/20231030174314_add_billing_to_team/migration.sql diff --git a/packages/database/migrations/20231102045537_adds_image_url_column_in_the_user_table/migration.sql b/packages/database/migration/20231102045537_adds_image_url_column_in_the_user_table/migration.sql similarity index 100% rename from packages/database/migrations/20231102045537_adds_image_url_column_in_the_user_table/migration.sql rename to packages/database/migration/20231102045537_adds_image_url_column_in_the_user_table/migration.sql diff --git a/packages/database/migrations/20231107145619_add_indexes/migration.sql b/packages/database/migration/20231107145619_add_indexes/migration.sql similarity index 100% rename from packages/database/migrations/20231107145619_add_indexes/migration.sql rename to packages/database/migration/20231107145619_add_indexes/migration.sql diff --git a/packages/database/migrations/20231109052945_restructure_session_action_person/migration.sql b/packages/database/migration/20231109052945_restructure_session_action_person/migration.sql similarity index 100% rename from packages/database/migrations/20231109052945_restructure_session_action_person/migration.sql rename to packages/database/migration/20231109052945_restructure_session_action_person/migration.sql diff --git a/packages/database/migrations/20231114143459_rename_branding/migration.sql b/packages/database/migration/20231114143459_rename_branding/migration.sql similarity index 100% rename from packages/database/migrations/20231114143459_rename_branding/migration.sql rename to packages/database/migration/20231114143459_rename_branding/migration.sql diff --git a/packages/database/migrations/20231116131301_add_types_to_wehbhook_source/migration.sql b/packages/database/migration/20231116131301_add_types_to_wehbhook_source/migration.sql similarity index 100% rename from packages/database/migrations/20231116131301_add_types_to_wehbhook_source/migration.sql rename to packages/database/migration/20231116131301_add_types_to_wehbhook_source/migration.sql diff --git a/packages/database/migrations/20231129143833_add_ttc_to_response/migration.sql b/packages/database/migration/20231129143833_add_ttc_to_response/migration.sql similarity index 100% rename from packages/database/migrations/20231129143833_add_ttc_to_response/migration.sql rename to packages/database/migration/20231129143833_add_ttc_to_response/migration.sql diff --git a/packages/database/migrations/20231204180155_add_styling/migration.sql b/packages/database/migration/20231204180155_add_styling/migration.sql similarity index 100% rename from packages/database/migrations/20231204180155_add_styling/migration.sql rename to packages/database/migration/20231204180155_add_styling/migration.sql diff --git a/packages/database/migrations/20231207064643_add_notion_integration/migration.sql b/packages/database/migration/20231207064643_add_notion_integration/migration.sql similarity index 100% rename from packages/database/migrations/20231207064643_add_notion_integration/migration.sql rename to packages/database/migration/20231207064643_add_notion_integration/migration.sql diff --git a/packages/database/migrations/20240102132851_add_result_share_key_to_survey/migration.sql b/packages/database/migration/20240102132851_add_result_share_key_to_survey/migration.sql similarity index 100% rename from packages/database/migrations/20240102132851_add_result_share_key_to_survey/migration.sql rename to packages/database/migration/20240102132851_add_result_share_key_to_survey/migration.sql diff --git a/packages/database/migrations/20240129054228_add_display_percentage_to_survey/migration.sql b/packages/database/migration/20240129054228_add_display_percentage_to_survey/migration.sql similarity index 100% rename from packages/database/migrations/20240129054228_add_display_percentage_to_survey/migration.sql rename to packages/database/migration/20240129054228_add_display_percentage_to_survey/migration.sql diff --git a/packages/database/migrations/20240130103957_add_created_by_to_survey/migration.sql b/packages/database/migration/20240130103957_add_created_by_to_survey/migration.sql similarity index 100% rename from packages/database/migrations/20240130103957_add_created_by_to_survey/migration.sql rename to packages/database/migration/20240130103957_add_created_by_to_survey/migration.sql diff --git a/packages/database/migrations/20240204084813_remove_creator_cascade_delete_in_survey/migration.sql b/packages/database/migration/20240204084813_remove_creator_cascade_delete_in_survey/migration.sql similarity index 100% rename from packages/database/migrations/20240204084813_remove_creator_cascade_delete_in_survey/migration.sql rename to packages/database/migration/20240204084813_remove_creator_cascade_delete_in_survey/migration.sql diff --git a/packages/database/migrations/20240207041922_advanced_targeting/migration.sql b/packages/database/migration/20240207041922_advanced_targeting/migration.sql similarity index 100% rename from packages/database/migrations/20240207041922_advanced_targeting/migration.sql rename to packages/database/migration/20240207041922_advanced_targeting/migration.sql diff --git a/packages/database/migrations/20240221093753_add_support_for_openid/migration.sql b/packages/database/migration/20240221093753_add_support_for_openid/migration.sql similarity index 100% rename from packages/database/migrations/20240221093753_add_support_for_openid/migration.sql rename to packages/database/migration/20240221093753_add_support_for_openid/migration.sql diff --git a/packages/database/migrations/20240223042852_adds_inline_trigger_to_survey_model/migration.sql b/packages/database/migration/20240223042852_adds_inline_trigger_to_survey_model/migration.sql similarity index 100% rename from packages/database/migrations/20240223042852_adds_inline_trigger_to_survey_model/migration.sql rename to packages/database/migration/20240223042852_adds_inline_trigger_to_survey_model/migration.sql diff --git a/packages/database/migrations/20240228180201_update_action_indexes/migration.sql b/packages/database/migration/20240228180201_update_action_indexes/migration.sql similarity index 100% rename from packages/database/migrations/20240228180201_update_action_indexes/migration.sql rename to packages/database/migration/20240228180201_update_action_indexes/migration.sql diff --git a/packages/database/migrations/20240229141200_add_attribute_class_indexes/migration.sql b/packages/database/migration/20240229141200_add_attribute_class_indexes/migration.sql similarity index 100% rename from packages/database/migrations/20240229141200_add_attribute_class_indexes/migration.sql rename to packages/database/migration/20240229141200_add_attribute_class_indexes/migration.sql diff --git a/packages/database/migrations/20240305135128_add_segment_id_index_to_surveys/migration.sql b/packages/database/migration/20240305135128_add_segment_id_index_to_surveys/migration.sql similarity index 100% rename from packages/database/migrations/20240305135128_add_segment_id_index_to_surveys/migration.sql rename to packages/database/migration/20240305135128_add_segment_id_index_to_surveys/migration.sql diff --git a/packages/database/migrations/20240318050527_add_languages_and_survey_languages/migration.sql b/packages/database/migration/20240318050527_add_languages_and_survey_languages/migration.sql similarity index 100% rename from packages/database/migrations/20240318050527_add_languages_and_survey_languages/migration.sql rename to packages/database/migration/20240318050527_add_languages_and_survey_languages/migration.sql diff --git a/packages/database/migrations/20240320090315_add_form_styling/migration.sql b/packages/database/migration/20240320090315_add_form_styling/migration.sql similarity index 100% rename from packages/database/migrations/20240320090315_add_form_styling/migration.sql rename to packages/database/migration/20240320090315_add_form_styling/migration.sql diff --git a/packages/database/migrations/20240327140901_add_more_indexes/migration.sql b/packages/database/migration/20240327140901_add_more_indexes/migration.sql similarity index 100% rename from packages/database/migrations/20240327140901_add_more_indexes/migration.sql rename to packages/database/migration/20240327140901_add_more_indexes/migration.sql diff --git a/packages/database/migrations/20240403141559_add_run_on_date_to_survey/migration.sql b/packages/database/migration/20240403141559_add_run_on_date_to_survey/migration.sql similarity index 100% rename from packages/database/migrations/20240403141559_add_run_on_date_to_survey/migration.sql rename to packages/database/migration/20240403141559_add_run_on_date_to_survey/migration.sql diff --git a/packages/database/migrations/20240404213707_add_logo/migration.sql b/packages/database/migration/20240404213707_add_logo/migration.sql similarity index 100% rename from packages/database/migrations/20240404213707_add_logo/migration.sql rename to packages/database/migration/20240404213707_add_logo/migration.sql diff --git a/packages/database/migrations/20240409071907_add_slack_integration/migration.sql b/packages/database/migration/20240409071907_add_slack_integration/migration.sql similarity index 100% rename from packages/database/migrations/20240409071907_add_slack_integration/migration.sql rename to packages/database/migration/20240409071907_add_slack_integration/migration.sql diff --git a/packages/database/migrations/20240413092848_simplify_action_indexes/migration.sql b/packages/database/migration/20240413092848_simplify_action_indexes/migration.sql similarity index 100% rename from packages/database/migrations/20240413092848_simplify_action_indexes/migration.sql rename to packages/database/migration/20240413092848_simplify_action_indexes/migration.sql diff --git a/packages/database/migrations/20240421093432_adds_website_survey_type/migration.sql b/packages/database/migration/20240421093432_adds_website_survey_type/migration.sql similarity index 100% rename from packages/database/migrations/20240421093432_adds_website_survey_type/migration.sql rename to packages/database/migration/20240421093432_adds_website_survey_type/migration.sql diff --git a/packages/database/migrations/20240501111944_refactors_actions_and_removes_inline_triggers/migration.sql b/packages/database/migration/20240501111944_refactors_actions_and_removes_inline_triggers/migration.sql similarity index 100% rename from packages/database/migrations/20240501111944_refactors_actions_and_removes_inline_triggers/migration.sql rename to packages/database/migration/20240501111944_refactors_actions_and_removes_inline_triggers/migration.sql diff --git a/packages/database/migrations/20240516122752_rename_teams_to_organizations/migration.sql b/packages/database/migration/20240516122752_rename_teams_to_organizations/migration.sql similarity index 100% rename from packages/database/migrations/20240516122752_rename_teams_to_organizations/migration.sql rename to packages/database/migration/20240516122752_rename_teams_to_organizations/migration.sql diff --git a/packages/database/migrations/20240530073540_made_name_property_on_user_model_optional/migration.sql b/packages/database/migration/20240530073540_made_name_property_on_user_model_optional/migration.sql similarity index 100% rename from packages/database/migrations/20240530073540_made_name_property_on_user_model_optional/migration.sql rename to packages/database/migration/20240530073540_made_name_property_on_user_model_optional/migration.sql diff --git a/packages/database/migrations/20240603130103_display_percentage_to_decimal/migration.sql b/packages/database/migration/20240603130103_display_percentage_to_decimal/migration.sql similarity index 100% rename from packages/database/migrations/20240603130103_display_percentage_to_decimal/migration.sql rename to packages/database/migration/20240603130103_display_percentage_to_decimal/migration.sql diff --git a/packages/database/migrations/20240605104525_added_display_some_and_recontact_sessions/migration.sql b/packages/database/migration/20240605104525_added_display_some_and_recontact_sessions/migration.sql similarity index 100% rename from packages/database/migrations/20240605104525_added_display_some_and_recontact_sessions/migration.sql rename to packages/database/migration/20240605104525_added_display_some_and_recontact_sessions/migration.sql diff --git a/packages/database/migrations/20240610055828_adds_app_and_website_status_indicators/migration.sql b/packages/database/migration/20240610055828_adds_app_and_website_status_indicators/migration.sql similarity index 100% rename from packages/database/migrations/20240610055828_adds_app_and_website_status_indicators/migration.sql rename to packages/database/migration/20240610055828_adds_app_and_website_status_indicators/migration.sql diff --git a/packages/database/migrations/20240612074254_add_show_language_switch_to_survey/migration.sql b/packages/database/migration/20240612074254_add_show_language_switch_to_survey/migration.sql similarity index 100% rename from packages/database/migrations/20240612074254_add_show_language_switch_to_survey/migration.sql rename to packages/database/migration/20240612074254_add_show_language_switch_to_survey/migration.sql diff --git a/packages/database/migrations/20240612115151_adds_product_config/migration.sql b/packages/database/migration/20240612115151_adds_product_config/migration.sql similarity index 100% rename from packages/database/migrations/20240612115151_adds_product_config/migration.sql rename to packages/database/migration/20240612115151_adds_product_config/migration.sql diff --git a/packages/database/migrations/20240613070218_pricing_v2/migration.sql b/packages/database/migration/20240613070218_pricing_v2/migration.sql similarity index 100% rename from packages/database/migrations/20240613070218_pricing_v2/migration.sql rename to packages/database/migration/20240613070218_pricing_v2/migration.sql diff --git a/packages/database/migrations/20240618115052_remove_onboarding_completed_from_user/migration.sql b/packages/database/migration/20240618115052_remove_onboarding_completed_from_user/migration.sql similarity index 100% rename from packages/database/migrations/20240618115052_remove_onboarding_completed_from_user/migration.sql rename to packages/database/migration/20240618115052_remove_onboarding_completed_from_user/migration.sql diff --git a/packages/database/migrations/20240726102232_add_is_verify_email_enabled_to_survey/migration.sql b/packages/database/migration/20240726102232_add_is_verify_email_enabled_to_survey/migration.sql similarity index 100% rename from packages/database/migrations/20240726102232_add_is_verify_email_enabled_to_survey/migration.sql rename to packages/database/migration/20240726102232_add_is_verify_email_enabled_to_survey/migration.sql diff --git a/packages/database/migrations/20240801071532_add_endings_to_survey/migration.sql b/packages/database/migration/20240801071532_add_endings_to_survey/migration.sql similarity index 100% rename from packages/database/migrations/20240801071532_add_endings_to_survey/migration.sql rename to packages/database/migration/20240801071532_add_endings_to_survey/migration.sql diff --git a/packages/database/migrations/20240813094711_added_variables_to_survey_model/migration.sql b/packages/database/migration/20240813094711_added_variables_to_survey_model/migration.sql similarity index 100% rename from packages/database/migrations/20240813094711_added_variables_to_survey_model/migration.sql rename to packages/database/migration/20240813094711_added_variables_to_survey_model/migration.sql diff --git a/packages/database/migrations/20240827113336_adds_variables_to_repsonse/migration.sql b/packages/database/migration/20240827113336_adds_variables_to_repsonse/migration.sql similarity index 100% rename from packages/database/migrations/20240827113336_adds_variables_to_repsonse/migration.sql rename to packages/database/migration/20240827113336_adds_variables_to_repsonse/migration.sql diff --git a/packages/database/migrations/20240904043737_add_is_single_response_per_email_enabled_to_survey/migration.sql b/packages/database/migration/20240904043737_add_is_single_response_per_email_enabled_to_survey/migration.sql similarity index 100% rename from packages/database/migrations/20240904043737_add_is_single_response_per_email_enabled_to_survey/migration.sql rename to packages/database/migration/20240904043737_add_is_single_response_per_email_enabled_to_survey/migration.sql diff --git a/packages/database/migrations/20240904091113_removed_actions_table/migration.sql b/packages/database/migration/20240904091113_removed_actions_table/migration.sql similarity index 100% rename from packages/database/migrations/20240904091113_removed_actions_table/migration.sql rename to packages/database/migration/20240904091113_removed_actions_table/migration.sql diff --git a/packages/database/migrations/20240917112456_add_display_id_to_response/migration.sql b/packages/database/migration/20240917112456_add_display_id_to_response/migration.sql similarity index 100% rename from packages/database/migrations/20240917112456_add_display_id_to_response/migration.sql rename to packages/database/migration/20240917112456_add_display_id_to_response/migration.sql diff --git a/packages/database/migrations/20241004070040_removed_website_setup_completed/migration.sql b/packages/database/migration/20241004070040_removed_website_setup_completed/migration.sql similarity index 100% rename from packages/database/migrations/20241004070040_removed_website_setup_completed/migration.sql rename to packages/database/migration/20241004070040_removed_website_setup_completed/migration.sql diff --git a/packages/database/migrations/20241010133706_xm_user_identification/migration.sql b/packages/database/migration/20241010133706_xm_user_identification/migration.sql similarity index 100% rename from packages/database/migrations/20241010133706_xm_user_identification/migration.sql rename to packages/database/migration/20241010133706_xm_user_identification/migration.sql diff --git a/packages/database/migrations/20241017124431_add_documents_and_insights/migration.sql b/packages/database/migration/20241017124431_add_documents_and_insights/migration.sql similarity index 100% rename from packages/database/migrations/20241017124431_add_documents_and_insights/migration.sql rename to packages/database/migration/20241017124431_add_documents_and_insights/migration.sql diff --git a/packages/database/migrations/20241025105622_add_locale_to_user/migration.sql b/packages/database/migration/20241025105622_add_locale_to_user/migration.sql similarity index 100% rename from packages/database/migrations/20241025105622_add_locale_to_user/migration.sql rename to packages/database/migration/20241025105622_add_locale_to_user/migration.sql diff --git a/packages/database/migrations/20241105074829_added_survey_follow_up/migration.sql b/packages/database/migration/20241105074829_added_survey_follow_up/migration.sql similarity index 100% rename from packages/database/migrations/20241105074829_added_survey_follow_up/migration.sql rename to packages/database/migration/20241105074829_added_survey_follow_up/migration.sql diff --git a/packages/database/migrations/20241107161932_add_teams/migration.sql b/packages/database/migration/20241107161932_add_teams/migration.sql similarity index 100% rename from packages/database/migrations/20241107161932_add_teams/migration.sql rename to packages/database/migration/20241107161932_add_teams/migration.sql diff --git a/packages/database/migrations/20241120150728_product_revamp/migration.sql b/packages/database/migration/20241120150728_product_revamp/migration.sql similarity index 100% rename from packages/database/migrations/20241120150728_product_revamp/migration.sql rename to packages/database/migration/20241120150728_product_revamp/migration.sql diff --git a/packages/database/migration/20241209051259_added_data_migration_table/migration.sql b/packages/database/migration/20241209051259_added_data_migration_table/migration.sql new file mode 100644 index 0000000000..c53aa358cc --- /dev/null +++ b/packages/database/migration/20241209051259_added_data_migration_table/migration.sql @@ -0,0 +1,16 @@ +-- CreateEnum +CREATE TYPE "DataMigrationStatus" AS ENUM ('pending', 'applied', 'failed'); + +-- CreateTable +CREATE TABLE "DataMigration" ( + "id" TEXT NOT NULL, + "started_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "finished_at" TIMESTAMP(3), + "status" "DataMigrationStatus" NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "DataMigration_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "DataMigration_name_key" ON "DataMigration"("name"); diff --git a/packages/database/migration/20241209104738_xm_user_identification/migration.ts b/packages/database/migration/20241209104738_xm_user_identification/migration.ts new file mode 100644 index 0000000000..1d82ac7748 --- /dev/null +++ b/packages/database/migration/20241209104738_xm_user_identification/migration.ts @@ -0,0 +1,200 @@ +/* eslint-disable no-constant-condition -- Required for the while loop */ + +/* eslint-disable @typescript-eslint/no-unnecessary-condition -- Required for a while loop here */ +import { createId } from "@paralleldrive/cuid2"; +import { Prisma } from "@prisma/client"; +import type { DataMigrationScript } from "../../src/scripts/migration-runner"; + +export const xmUserIdentification: DataMigrationScript = { + type: "data", + id: "n2u5d3wmcw1t2h8a4vgfu2y9", + name: "20241209104738_xm_user_identification", + run: async ({ tx }) => { + // Check total contacts + const [{ total_contacts: totalContacts }] = await tx.$queryRaw<[{ total_contacts: number }]>` + SELECT COUNT(*) AS total_contacts FROM "Contact" + `; + + // Check contacts with userId + const [{ contacts_with_user_id: contactsWithUserId }] = await tx.$queryRaw< + [{ contacts_with_user_id: number }] + >` + SELECT COUNT(*) AS contacts_with_user_id FROM "Contact" WHERE "userId" IS NOT NULL + `; + + // If no contacts have a userId, migration is already complete + if (totalContacts > 0 && contactsWithUserId === 0) { + console.log("Migration already completed. No contacts with userId found."); + return; + } + + const BATCH_SIZE = 10000; + let skip = 0; + + while (true) { + // Fetch environments in batches + const environments = await tx.$queryRaw<{ id: string }[]>` + SELECT id FROM "Environment" LIMIT ${BATCH_SIZE} OFFSET ${skip} + `; + + if (environments.length === 0) { + break; + } + + console.log("Processing attributeKeys for", environments.length, "environments"); + + // Process each environment + for (const env of environments) { + // Upsert attribute keys for each environment + await tx.$executeRaw` + INSERT INTO "ContactAttributeKey" ( + "id", "created_at", "updated_at", "key", "name", "description", "type", "isUnique", "environmentId" + ) VALUES + (${createId()}, NOW(), NOW(), 'email', 'Email', 'The email of a contact', 'default', true, ${env.id}), + (${createId()}, NOW(), NOW(),'firstName', 'First Name', 'Your contact''s first name', 'default', false, ${env.id}), + (${createId()}, NOW(), NOW(), 'lastName', 'Last Name', 'Your contact''s last name', 'default', false, ${env.id}), + (${createId()}, NOW(), NOW(), 'userId', 'User ID', 'The user ID of a contact', 'default', true, ${env.id}) + ON CONFLICT ("key", "environmentId") DO UPDATE + SET + "type" = EXCLUDED."type", + "isUnique" = EXCLUDED."isUnique", + "updated_at" = NOW() + `; + } + + skip += environments.length; + } + + const CONTACTS_BATCH_SIZE = 20000; + let processedContacts = 0; + + // Delete existing userId attributes + const [{ deleted_count: deletedCount }] = await tx.$queryRaw<[{ deleted_count: number }]>` + WITH deleted AS ( + DELETE FROM "ContactAttribute" + WHERE "attributeKeyId" IN ( + SELECT id FROM "ContactAttributeKey" + WHERE "key" = 'userId' + ) + RETURNING 1 + ) + SELECT COUNT(*)::integer AS deleted_count FROM deleted + `; + + console.log("Deleted userId attributes for", deletedCount, "contacts"); + + while (true) { + // Fetch contacts with userId in batches + const contacts = await tx.$queryRaw< + { + id: string; + userId: string; + environmentId: string; + }[] + >` + SELECT id, "userId", "environmentId" + FROM "Contact" + WHERE "userId" IS NOT NULL + LIMIT ${CONTACTS_BATCH_SIZE} + `; + + if (contacts.length === 0) { + break; + } + + // Get userId attribute keys for environments + const userIdAttributeKeys = await tx.$queryRaw< + { + id: string; + environmentId: string; + }[] + >` + SELECT id, "environmentId" + FROM "ContactAttributeKey" + WHERE "key" = 'userId' + AND "environmentId" IN (${Prisma.join(contacts.map((c) => c.environmentId))}) + `; + + // Create a map for quick lookup + const attributeMap = new Map(userIdAttributeKeys.map((ak) => [ak.environmentId, ak.id])); + + const attributeData = contacts.map((contact) => { + const userIdAttributeKey = attributeMap.get(contact.environmentId); + if (!userIdAttributeKey) { + throw new Error(`Attribute key for userId not found for environment ${contact.environmentId}`); + } + return { + id: createId(), + created_at: new Date(), + updated_at: new Date(), + contactId: contact.id, + value: contact.userId, + attributeKeyId: userIdAttributeKey, + }; + }); + + await tx.$executeRaw` + INSERT INTO "ContactAttribute" ( + "id", + "created_at", + "updated_at", + "contactId", + "value", + "attributeKeyId" + ) + SELECT + unnest(${Prisma.sql`ARRAY[${attributeData.map((d) => d.id)}]`}), + unnest(${Prisma.sql`ARRAY[${attributeData.map((d) => d.created_at)}]`}), + unnest(${Prisma.sql`ARRAY[${attributeData.map((d) => d.updated_at)}]`}), + unnest(${Prisma.sql`ARRAY[${attributeData.map((d) => d.contactId)}]`}), + unnest(${Prisma.sql`ARRAY[${attributeData.map((d) => d.value)}]`}), + unnest(${Prisma.sql`ARRAY[${attributeData.map((d) => d.attributeKeyId)}]`}) + ON CONFLICT ("contactId", "attributeKeyId") DO UPDATE + SET + "value" = EXCLUDED."value", + "updated_at" = EXCLUDED."updated_at" + `; + + // Clear userId from contacts + await tx.$queryRaw(Prisma.sql` + UPDATE "Contact" + SET "userId" = NULL + WHERE id IN (${Prisma.join(contacts.map((c) => c.id))}) + `); + + processedContacts += contacts.length; + + if (processedContacts > 0) { + console.log(`Processed ${processedContacts.toString()} contacts`); + } + } + + // Verify migration + const [{ total_contacts_after_migration: totalContactsAfterMigration }] = await tx.$queryRaw< + [{ total_contacts_after_migration: number }] + >` + SELECT COUNT(*)::integer AS total_contacts_after_migration FROM "Contact" + `; + + const [{ total_user_id_attributes: totalUserIdAttributes }] = await tx.$queryRaw< + [{ total_user_id_attributes: number }] + >` + SELECT COUNT(*)::integer AS total_user_id_attributes + FROM "ContactAttribute" + WHERE "attributeKeyId" IN ( + SELECT id FROM "ContactAttributeKey" + WHERE "key" = 'userId' + ) + `; + + console.log("Total contacts after migration:", totalContactsAfterMigration); + console.log("Total attributes with userId now:", totalUserIdAttributes); + + if (totalContactsAfterMigration !== totalUserIdAttributes) { + console.log( + "Difference between total contacts and total attributes with userId: ", + totalContactsAfterMigration - totalUserIdAttributes + ); + } + }, +}; diff --git a/packages/database/migration/20241209110456_xm_segment_migration/migration.ts b/packages/database/migration/20241209110456_xm_segment_migration/migration.ts new file mode 100644 index 0000000000..07023b8790 --- /dev/null +++ b/packages/database/migration/20241209110456_xm_segment_migration/migration.ts @@ -0,0 +1,68 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call -- required for any type */ + +/* eslint-disable @typescript-eslint/no-unsafe-assignment -- required for any type */ + +/* eslint-disable @typescript-eslint/no-unsafe-member-access -- required for any type */ + +/* eslint-disable @typescript-eslint/no-explicit-any -- required for any type */ +import type { DataMigrationScript } from "../../src/scripts/migration-runner"; + +export const isResourceFilter = (resource: any): boolean => { + return resource.root !== undefined; +}; + +const findAndReplace = (filters: any): any => { + const newFilters: any = []; + for (const filter of filters) { + if (isResourceFilter(filter.resource)) { + let { root } = filter.resource; + if (root.type === "attribute" && root.attributeClassName) { + root = { + type: "attribute", + contactAttributeKey: root.attributeClassName, + }; + const newFilter = { + ...filter.resource, + root, + }; + + newFilters.push({ + ...filter, + resource: newFilter, + }); + } else { + newFilters.push(filter); + } + } else { + const updatedResource = findAndReplace(filter.resource); + newFilters.push({ + ...filter, + resource: updatedResource, + }); + } + } + + return newFilters; +}; + +export const xmSegmentMigration: DataMigrationScript = { + type: "data", + id: "s644oyyqccstfdeejc4fluye", + name: "20241209110456_xm_segment_migration", + run: async ({ tx }) => { + const allSegments = await tx.segment.findMany(); + const updationPromises = []; + for (const segment of allSegments) { + updationPromises.push( + tx.segment.update({ + where: { id: segment.id }, + data: { + filters: findAndReplace(segment.filters), + }, + }) + ); + } + + await Promise.all(updationPromises); + }, +}; diff --git a/packages/database/migration/20241209111404_xm_attribute_removal/migration.ts b/packages/database/migration/20241209111404_xm_attribute_removal/migration.ts new file mode 100644 index 0000000000..41a86903a9 --- /dev/null +++ b/packages/database/migration/20241209111404_xm_attribute_removal/migration.ts @@ -0,0 +1,88 @@ +/* eslint-disable @typescript-eslint/no-unnecessary-condition -- Required for a while loop here */ +import { Prisma } from "@prisma/client"; +import type { DataMigrationScript } from "../../src/scripts/migration-runner"; + +export const xmAttributeRemoval: DataMigrationScript = { + type: "data", + id: "mq9x7rjdnq0saxoli9pl9b3o", + name: "20241209111404_xm_attribute_removal", + run: async ({ tx }) => { + // Your migration script goes here + const emailAttributes: { + id: string; + value: string; + contactId: string; + environmentId: string; + contactCreatedAt: Date; + attributeCreatedAt: Date; + }[] = + await tx.$queryRaw`SELECT ca.id, ca.value, c.id AS "contactId", c."environmentId", c.created_at AS "contactCreatedAt", ca.created_at AS "attributeCreatedAt" + FROM "ContactAttribute" ca + JOIN "Contact" c ON ca."contactId" = c.id + JOIN "ContactAttributeKey" ak ON ca."attributeKeyId" = ak.id + WHERE ak.key = 'email' + ORDER BY ca.created_at ASC`; + + const emailsByEnvironment: Record< + string, + Record + > = {}; + + for (const attr of emailAttributes) { + const { environmentId, value: email, id, contactId, attributeCreatedAt } = attr; + + if (!emailsByEnvironment[environmentId]) { + emailsByEnvironment[environmentId] = {}; + } + + if (!emailsByEnvironment[environmentId][email]) { + emailsByEnvironment[environmentId][email] = []; + } + + emailsByEnvironment[environmentId][email].push({ + id, + contactId, + createdAt: new Date(attributeCreatedAt), + }); + } + + const deletionSummary: Record< + string, + { + email: string; + deletedAttributeIds: string[]; + keptAttributeId: string; + }[] + > = {}; + + for (const [environmentId, emailGroups] of Object.entries(emailsByEnvironment)) { + deletionSummary[environmentId] = []; + + for (const [email, attributes] of Object.entries(emailGroups)) { + if (attributes.length > 1) { + attributes.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); + + const [kept, ...duplicates] = attributes; + const duplicateIds = duplicates.map((d) => d.id); + + await tx.$executeRaw`DELETE FROM "ContactAttribute" WHERE id IN (${Prisma.join(duplicateIds)})`; + + deletionSummary[environmentId].push({ + email, + deletedAttributeIds: duplicateIds, + keptAttributeId: kept.id, + }); + } + } + } + + const summary = { + totalDuplicateAttributesRemoved: Object.values(deletionSummary).reduce( + (acc, env) => acc + env.reduce((sum, item) => sum + item.deletedAttributeIds.length, 0), + 0 + ), + }; + + console.log("Data migration completed. Summary: ", summary); + }, +}; diff --git a/packages/database/migration/20241209111525_update_org_limits/migration.ts b/packages/database/migration/20241209111525_update_org_limits/migration.ts new file mode 100644 index 0000000000..ee2aecb584 --- /dev/null +++ b/packages/database/migration/20241209111525_update_org_limits/migration.ts @@ -0,0 +1,99 @@ +import type { DataMigrationScript } from "../../src/scripts/migration-runner"; + +type Plan = "free" | "startup" | "scale"; + +interface TOrganization { + id: string; + billing: { + plan: Plan; + limits: { + monthly: { + responses: number; + miu: number; + }; + }; + }; +} + +export const BILLING_LIMITS = { + free: { + RESPONSES: 1500, + MIU: 2000, + }, + startup: { + RESPONSES: 5000, + MIU: 7500, + }, + scale: { + RESPONSES: 10000, + MIU: 30000, + }, +} as const; + +export const updateOrgLimits: DataMigrationScript = { + type: "data", + id: "ax4otbz2f295rit6kn1jeu8l", + name: "20241209111525_update_org_limits", + run: async ({ tx }) => { + // Your migration script goes here + // Find organizations that need updates + const organizations = await tx.$queryRaw` + SELECT id, billing + FROM "Organization" + WHERE + ( + (billing->>'plan' = 'free' AND + ( + (billing->'limits'->'monthly'->>'miu')::numeric != 2000 OR + (billing->'limits'->'monthly'->>'responses')::numeric != 1500 + ) + ) + OR + ( + (billing->>'plan' = 'startup' AND + ( + (billing->'limits'->'monthly'->>'miu')::numeric != 7500 OR + (billing->'limits'->'monthly'->>'responses')::numeric != 5000 + ) + ) + ) + OR + ( + (billing->>'plan' = 'scale' AND + ( + (billing->'limits'->'monthly'->>'miu')::numeric != 30000 OR + (billing->'limits'->'monthly'->>'responses')::numeric != 10000 + ) + ) + ) + ) + `; + + const updationPromises = []; + + // Batch update organizations + for (const organization of organizations) { + const plan = organization.billing.plan; + const limits = BILLING_LIMITS[plan]; + + const updatedBilling = { + ...organization.billing, + limits: { + ...organization.billing.limits, + monthly: { + ...organization.billing.limits.monthly, + responses: limits.RESPONSES, + miu: limits.MIU, + }, + }, + }; + + const updatePromise = tx.$executeRaw`UPDATE "Organization" SET billing = ${updatedBilling}::jsonb WHERE id = ${organization.id}`; + updationPromises.push(updatePromise); + } + + await Promise.all(updationPromises); + + console.log(`Updated ${organizations.length.toString()} organizations`); + }, +}; diff --git a/packages/database/migration/20241209111725_product_revamp/migration.ts b/packages/database/migration/20241209111725_product_revamp/migration.ts new file mode 100644 index 0000000000..d391b45963 --- /dev/null +++ b/packages/database/migration/20241209111725_product_revamp/migration.ts @@ -0,0 +1,64 @@ +import type { DataMigrationScript } from "../../src/scripts/migration-runner"; + +type Plan = "free" | "startup" | "scale" | "enterprise"; + +const projectsLimitByPlan: Record = { + free: 3, + startup: null, + scale: null, + enterprise: null, +}; + +export const productRevamp: DataMigrationScript = { + type: "data", + id: "wq3b8pvrvm70nzmsg2647olq", + name: "20241209111725_product_revamp", + run: async ({ tx }) => { + // Your migration script goes here + + const organizations = await tx.$queryRaw< + { + id: string; + billing: { + plan: Plan; + limits: { + monthly: { + responses: number; + miu: number; + }; + projects: number | null; + }; + }; + }[] + >`SELECT id, billing FROM "Organization" WHERE (billing->'limits'->>'projects') IS NULL`; + + const updateOrganizationPromises = organizations.map((org) => { + const updatedBilling = { + ...org.billing, + limits: { + ...org.billing.limits, + projects: projectsLimitByPlan[org.billing.plan], + }, + }; + + return tx.$executeRaw`UPDATE "Organization" SET billing = ${updatedBilling}::jsonb WHERE id = ${org.id}`; + }); + + await Promise.all(updateOrganizationPromises); + + console.log(`Updated ${updateOrganizationPromises.length.toString()} organizations`); + + const updatedEmptyConfigProjects: number | undefined = await tx.$executeRaw` + UPDATE "Project" + SET config = jsonb_set( + jsonb_set(config, '{channel}', 'null'::jsonb, true), + '{industry}', 'null'::jsonb, true + ) + WHERE config = '{}'; + `; + + console.log( + `Updated ${updatedEmptyConfigProjects ? updatedEmptyConfigProjects.toString() : "0"} projects with empty config` + ); + }, +}; diff --git a/packages/database/migrations/migration_lock.toml b/packages/database/migration/migration_lock.toml similarity index 100% rename from packages/database/migrations/migration_lock.toml rename to packages/database/migration/migration_lock.toml diff --git a/packages/database/package.json b/packages/database/package.json index 911c64c799..426e6acfcd 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -10,9 +10,8 @@ ], "scripts": { "clean": "rimraf .turbo node_modules", - "db:migrate:deploy": "env DATABASE_URL=\"${MIGRATE_DATABASE_URL:-$DATABASE_URL}\" prisma migrate deploy", - "db:migrate:dev": "dotenv -e ../../.env -- pnpm prisma migrate dev", - "db:migrate:vercel": "if test \"$NEXT_PUBLIC_VERCEL_ENV\" = \"preview\" ; then env DATABASE_URL=\"$MIGRATE_DATABASE_URL\" prisma db push --accept-data-loss ; else env DATABASE_URL=\"$MIGRATE_DATABASE_URL\" prisma migrate deploy ; fi", + "db:migrate:deploy": "env DATABASE_URL=\"${MIGRATE_DATABASE_URL:-$DATABASE_URL}\" tsx ./src/scripts/apply-migrations.ts", + "db:migrate:dev": "dotenv -e ../../.env -- sh -c \"pnpm prisma generate && tsx ./src/scripts/apply-migrations.ts\"", "db:push": "prisma db push --accept-data-loss", "db:up": "docker compose up -d", "db:setup": "pnpm db:up && pnpm db:migrate:dev", @@ -22,46 +21,8 @@ "generate": "prisma generate", "lint": "eslint ./src --fix", "build": "pnpm generate", - "data-migration:v1.6": "ts-node ./data-migrations/20240207041922_advanced_targeting/data-migration.ts", - "data-migration:styling": "ts-node ./data-migrations/20240320090315_add_form_styling/data-migration.ts", - "data-migration:styling-fix": "ts-node ./data-migrations/20240320090315_add_form_styling/data-migration-fix.ts", - "data-migration:website-surveys": "ts-node ./data-migrations/20240410111624_adds_website_and_inapp_survey/data-migration.ts", - "data-migration:mls": "ts-node ./data-migrations/20240318050527_add_languages_and_survey_languages/data-migration.ts", - "data-migration:mls-fix": "ts-node ./data-migrations/20240318050527_add_languages_and_survey_languages/data-migration-fix.ts", - "data-migration:mls-range-fix": "ts-node ./data-migrations/20240318050527_add_languages_and_survey_languages/data-migration-range-fix.ts", - "data-migration:userId": "ts-node ./data-migrations/20240408123456_userid_migration/data-migration.ts", - "data-migration:refactor-actions": "ts-node ./data-migrations/20240501111944_refactors_actions_and_removes_inline_triggers/data-migration.ts", - "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:pricing-v2": "dotenv -e ../../.env -- ts-node ./data-migrations/20240613070218_pricing_v2/data-migration.ts", - "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:adds_app_and_website_status_indicator": "ts-node ./data-migrations/20240610055828_adds_app_and_website_status_indicators/data-migration.ts", - "data-migration:product-config": "ts-node ./data-migrations/20240612115151_adds_product_config/data-migration.ts", - "data-migration:v2.2": "pnpm data-migration:adds_app_and_website_status_indicator && pnpm data-migration:product-config && pnpm data-migration:pricing-v2", - "data-migration:zh-to-zh-Hans": "ts-node ./data-migrations/20240625101352_update_zh_to_zh-Hans/data-migration.ts", - "data-migration:v2.3": "pnpm data-migration:zh-to-zh-Hans", - "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: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", - "data-migration:remove-dismissed-value-inconsistency": "ts-node ./data-migrations/20240807120500_cta_consent_dismissed_inconsistency/data-migration.ts", - "data-migration:v2.5": "pnpm data-migration:remove-dismissed-value-inconsistency", - "data-migration:add-display-id-to-response": "ts-node ./data-migrations/20240905120500_refactor_display_response_relationship/data-migration.ts", - "data-migration:address-question": "ts-node ./data-migrations/20240924123456_migrate_address_question/data-migration.ts", - "data-migration:advanced-logic": "ts-node ./data-migrations/20240828122408_advanced_logic_editor/data-migration.ts", - "data-migration:segments-actions-cleanup": "ts-node ./data-migrations/20240904091113_removed_actions_table/data-migration.ts", - "data-migration:migrate-survey-types": "ts-node ./data-migrations/20241002123456_migrate_survey_types/data-migration.ts", - "data-migration:v2.6": "pnpm data-migration:add-display-id-to-response && pnpm data-migration:address-question && pnpm data-migration:advanced-logic && pnpm data-migration:segments-actions-cleanup && pnpm data-migration:migrate-survey-types", - "data-migration:xm": "ts-node ./data-migrations/20241010133706_xm_user_identification/data-migration.ts", - "data-migration:xm-segments": "ts-node ./data-migrations/20241021123456_xm_segment_migration/data-migration.ts", - "data-migration:xm-attribute-removal": "ts-node ./data-migrations/20241024123456_xm_attribute_removal/data-migration.ts", - "data-migration:xm-user-identification": "pnpm data-migration:xm && pnpm data-migration:xm-segments && pnpm data-migration:xm-attribute-removal", - "data-migration:add-teams": "ts-node ./data-migrations/20241107161932_add_teams/data-migration.ts", - "data-migration:v2.7": "pnpm data-migration:add-teams", - "data-migration:update-org-limits": "ts-node ./data-migrations/20241118123456_update_org_limits/data-migration.ts", - "data-migration:product-revamp": "ts-node ./data-migrations/20241120150728_product_revamp/data-migration.ts" + "generate-data-migration": "tsx ./src/scripts/generate-data-migration.ts", + "create-migration": "dotenv -e ../../.env -- tsx ./src/scripts/create-migration.ts" }, "dependencies": { "@prisma/client": "5.20.0", diff --git a/packages/database/schema.prisma b/packages/database/schema.prisma index 2d316aa5ab..cded5928ab 100644 --- a/packages/database/schema.prisma +++ b/packages/database/schema.prisma @@ -405,6 +405,20 @@ model Integration { @@index([environmentId]) } +enum DataMigrationStatus { + pending + applied + failed +} + +model DataMigration { + id String @id @default(cuid()) + startedAt DateTime @default(now()) @map(name: "started_at") + finishedAt DateTime? @map(name: "finished_at") + name String @unique + status DataMigrationStatus +} + model Environment { id String @id @default(cuid()) createdAt DateTime @default(now()) @map(name: "created_at") diff --git a/packages/database/src/scripts/apply-migrations.ts b/packages/database/src/scripts/apply-migrations.ts new file mode 100644 index 0000000000..3573d5310e --- /dev/null +++ b/packages/database/src/scripts/apply-migrations.ts @@ -0,0 +1,6 @@ +import { applyMigrations } from "./migration-runner"; + +applyMigrations().catch((error: unknown) => { + console.error("Migration failed:", error); + process.exit(1); +}); diff --git a/packages/database/src/scripts/create-migration.ts b/packages/database/src/scripts/create-migration.ts new file mode 100644 index 0000000000..44384b3fab --- /dev/null +++ b/packages/database/src/scripts/create-migration.ts @@ -0,0 +1,102 @@ +import { exec } from "node:child_process"; +import fs from "node:fs/promises"; +import path from "node:path"; +import readline from "node:readline"; +import { promisify } from "node:util"; +import { applyMigrations } from "./migration-runner"; + +const execAsync = promisify(exec); +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +function promptForMigrationName(): Promise { + return new Promise((resolve) => { + rl.question("Enter the name of the migration (please use spaces): ", (name) => { + if (!name.trim()) { + console.error("Migration name cannot be empty."); + process.exit(1); + } + if (/[^a-zA-Z0-9\s]/.test(name)) { + console.error( + "Migration name contains invalid characters. Only letters, numbers, and spaces are allowed." + ); + process.exit(1); + } + rl.close(); + resolve(name); + }); + }); +} + +async function main(): Promise { + const migrationName = await promptForMigrationName(); + const migrationNameUnderscored = migrationName.replace(/\s+/g, "_"); + + const migrationsDir = path.resolve(__dirname, "../../migrations"); + const customMigrationsDir = path.resolve(__dirname, "../../migration"); + + // Check if migration already exists in custom migrations + const existingMigrations = await fs.readdir(customMigrationsDir); + const duplicateMigration = existingMigrations.find((dir) => dir.includes(migrationNameUnderscored)); + + if (duplicateMigration) { + throw new Error(`Migration with name "${migrationName}" already exists in ${customMigrationsDir}`); + } + + // Create migration + await execAsync(`pnpm prisma migrate dev --name "${migrationName}" --create-only`); + console.log(`Migration created: ${migrationName}`); + + // Find the newly created migration + const migrationToCopy = await fs + .readdir(migrationsDir) + .then((files) => files.find((dir) => dir.includes(migrationNameUnderscored))); + + if (!migrationToCopy) { + throw new Error(`migration not found: ${migrationName}`); + } + + // Check if the migration is empty + const migrationToCopyPath = path.join(migrationsDir, migrationToCopy); + const files = await fs.readdir(migrationToCopyPath); + + if (!files.includes("migration.sql")) { + await fs.rm(migrationToCopyPath, { recursive: true, force: true }); + throw new Error( + `generated migration directory is empty: ${migrationName}. Please run the migration again.` + ); + } else { + const migrationSQL = await fs.readFile(path.join(migrationToCopyPath, "migration.sql"), "utf-8"); + if (migrationSQL === "-- This is an empty migration.") { + await fs.rm(migrationToCopyPath, { recursive: true, force: true }); + throw new Error( + "Database schema has not changed. Please make changes to the schema and run the migration again." + ); + } + } + + const sourcePath = path.join(migrationsDir, migrationToCopy); + const destPath = path.join(customMigrationsDir, migrationToCopy); + + // Copy migration folder + await fs.cp(sourcePath, destPath, { recursive: true }); + + // Delete the migration from the original migrations folder + await fs.rm(sourcePath, { recursive: true, force: true }); + + try { + await applyMigrations(); + } catch (err) { + console.error("Error applying migrations: ", err); + // delete the created migration directories: + await fs.rm(destPath, { recursive: true, force: true }); + process.exit(1); + } +} + +main().catch((error: unknown) => { + console.error("Migration creation failed:", error); + process.exit(1); +}); diff --git a/packages/database/src/scripts/generate-data-migration.ts b/packages/database/src/scripts/generate-data-migration.ts new file mode 100644 index 0000000000..0c7175709c --- /dev/null +++ b/packages/database/src/scripts/generate-data-migration.ts @@ -0,0 +1,110 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import readline from "node:readline"; +import { createId } from "@paralleldrive/cuid2"; + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +const migrationsDir = path.resolve(__dirname, "../../migration"); + +async function createMigration(): Promise { + // Log the full path to verify directory location + console.log("Migrations Directory Full Path:", migrationsDir); + + // Check if migrations directory exists, create if not + const hasAccess = await fs + .access(migrationsDir) + .then(() => true) + .catch(() => false); + + if (!hasAccess) { + await fs.mkdir(migrationsDir, { recursive: true }); + console.log(`Created migrations directory: ${migrationsDir}`); + } + + const migrationNameSpaced = await promptForMigrationName(); + const migrationName = migrationNameSpaced.replace(/\s+/g, "_"); + const migrationFunctionName = migrationNameSpaced + .replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => (index === 0 ? word.toLowerCase() : word.toUpperCase())) + .replace(/\s+/g, ""); + + const existingMigrations = await fs.readdir(migrationsDir); + const duplicateMigration = existingMigrations.find((dir) => dir.includes(migrationName)); + + if (duplicateMigration) { + throw new Error(`Migration with name "${migrationName}" already exists in ${migrationsDir}`); + } + + const timestamp = generateTimestamp(); + const migrationNameTimestamped = `${timestamp}_${migrationName}`; + const fullMigrationPath = path.join(migrationsDir, migrationNameTimestamped); + const filePath = path.join(fullMigrationPath, "migration.ts"); + + const hasAccessToMigration = await fs + .access(fullMigrationPath) + .then(() => true) + .catch(() => false); + + // Check if the migration already exists + if (hasAccessToMigration) { + throw new Error(`Migration "${migrationName}" already exists.`); + } + + // Create the migration directory + await fs.mkdir(fullMigrationPath, { recursive: true }); + console.log("Created migration directory:", fullMigrationPath); + + // Create the migration file + await fs.writeFile(filePath, getTemplateContent(migrationFunctionName, migrationNameTimestamped)); + console.log(`New migration created: ${filePath}`); +} + +function promptForMigrationName(): Promise { + return new Promise((resolve) => { + rl.question("Enter the name of the migration (please use spaces): ", (name) => { + if (!name.trim()) { + console.error("Migration name cannot be empty."); + process.exit(1); + } + if (/[^a-zA-Z0-9\s]/.test(name)) { + console.error("Migration name contains invalid characters. Only letters, numbers, and spaces are allowed."); + process.exit(1); + } + rl.close(); + resolve(name); + }); + }); +} + +function generateTimestamp(): string { + const now = new Date(); + // use UTC time to avoid timezone issues + + const utcTime = now.toISOString().replace(/[-:]/g, "").replace("T", "").replace("Z", "").slice(0, 14); + return utcTime; +} + +function getTemplateContent(migrationName: string, fullMigrationName: string): string { + const migrationId = createId(); + + return ` +import type { DataMigrationScript } from "../../src/scripts/migration-runner"; + +export const ${migrationName}: DataMigrationScript = { + type: "data", + id: "${migrationId}", + name: "${fullMigrationName}", + run: async ({ tx }) => { + // Your migration script goes here + } +}; +`; +} + +createMigration().catch((error: unknown) => { + console.error("An error occurred while creating the migration:", error); + process.exit(1); +}); diff --git a/packages/database/src/scripts/migration-runner.ts b/packages/database/src/scripts/migration-runner.ts new file mode 100644 index 0000000000..a1198e72f7 --- /dev/null +++ b/packages/database/src/scripts/migration-runner.ts @@ -0,0 +1,294 @@ +import { exec } from "node:child_process"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { promisify } from "node:util"; +import { type Prisma, PrismaClient } from "@prisma/client"; + +const execAsync = promisify(exec); + +export interface DataMigrationContext { + prisma: PrismaClient; + tx: Omit< + PrismaClient, + "$connect" | "$disconnect" | "$on" | "$transaction" | "$use" | "$extends" + >; +} + +export interface DataMigrationScript { + id?: string; + name: string; + run?: (context: DataMigrationContext) => Promise; + type: "data" | "schema"; +} + +const prisma = new PrismaClient(); +const TRANSACTION_TIMEOUT = 30 * 60 * 1000; // 30 minutes +const MIGRATIONS_DIR = path.resolve(__dirname, "../../migration"); + +const runMigrations = async (dataMigrations: DataMigrationScript[]): Promise => { + console.log(`Starting data migrations: ${dataMigrations.length.toString()} to run`); + const startTime = Date.now(); + + for (const dataMigration of dataMigrations) { + await runSingleMigration(dataMigration); + } + + const endTime = Date.now(); + console.log(`All data migrations completed in ${((endTime - startTime) / 1000).toFixed(2)}s`); +}; + +const runSingleMigration = async (migration: DataMigrationScript): Promise => { + if (migration.type === "data") { + console.log(`Running data migration: ${migration.name}`); + + try { + await prisma.$transaction( + async (tx) => { + // Check if migration has already been run + const existingMigration: { status: "pending" | "applied" | "failed" }[] | undefined = + await prisma.$queryRaw` + SELECT status FROM "DataMigration" + WHERE id = ${migration.id} + `; + + if (existingMigration?.[0]?.status === "pending") { + console.log(`Data migration ${migration.name} is pending.`); + console.log("Either there is another migration which is currently running or this is an error."); + console.log( + "If you are sure that there is no migration running, you need to manually resolve the issue." + ); + + throw new Error("Migration is pending. Please resolve the issue manually."); + } + + if (existingMigration?.[0]?.status === "applied") { + console.log(`Data migration ${migration.name} already completed. Skipping...`); + return; + } + + if (existingMigration?.[0]?.status === "failed") { + console.log(`Data migration ${migration.name} failed previously. Retrying...`); + } else { + // create a new data migration entry with pending status + await prisma.$executeRaw`INSERT INTO "DataMigration" (id, name, status) VALUES (${migration.id}, ${migration.name}, 'pending')`; + } + + if (migration.run) { + // Run the actual migration + await migration.run({ + prisma, + tx, + }); + + // Mark migration as applied + await prisma.$executeRaw` + UPDATE "DataMigration" + SET status = 'applied', finished_at = ${new Date()} + WHERE id = ${migration.id}; + `; + } + + console.log(`Data migration ${migration.name} completed successfully`); + }, + { timeout: TRANSACTION_TIMEOUT } + ); + } catch (error) { + // Record migration failure + console.error(`Data migration ${migration.name} failed:`, error); + // Mark migration as failed + await prisma.$queryRaw` + INSERT INTO "DataMigration" (id, name, status) + VALUES (${migration.id}, ${migration.name}, 'failed') + ON CONFLICT (id) DO UPDATE SET status = 'failed'; + `; + + throw error; + } + } else { + console.log(`Running schema migration: ${migration.name}`); + + // Original Prisma migrations directory + const originalMigrationsDir = path.resolve(__dirname, "../../migrations"); + // Temporary migrations directory for controlled migration + const customMigrationsDir = path.resolve(__dirname, "../../migration"); + + // TODO: Check if this can be implemented + // // if the migration directory exists, we will check if the migration has already been applied + + // const migrationDir = path.join(originalMigrationsDir, migration.name); + // const hasAccess = await fs + // .access(migrationDir) + // .then(() => true) + // .catch(() => false); + + // if (hasAccess) { + // // Check if there is a migration.sql file in the directory + // const hasSchemaMigration = await fs + // .readdir(migrationDir) + // .then((files) => files.includes("migration.sql")); + // if (hasSchemaMigration) { + // // Check if the migration has already been applied in the database + // const isApplied = await isSchemaMigrationApplied(migration.name, prisma); + // if (isApplied) { + // console.log(`Schema migration ${migration.name} already applied. Skipping...`); + // return; + // } + // } + // } + + const originalMigrationsDirExists = await fs + .access(originalMigrationsDir) + .then(() => true) + .catch(() => false); + + if (!originalMigrationsDirExists) { + await fs.mkdir(originalMigrationsDir, { recursive: true }); + } + + // Copy specific schema migration from temp migrations directory to original migrations directory + const migrationToCopy = await fs + .readdir(customMigrationsDir) + .then((files) => files.find((dir) => dir.includes(migration.name))); + + if (!migrationToCopy) { + console.error(`Schema migration not found: ${migration.name}`); + return; + } + + const sourcePath = path.join(customMigrationsDir, migrationToCopy); + const destPath = path.join(originalMigrationsDir, migrationToCopy); + + // Copy migration folder + await fs.cp(sourcePath, destPath, { recursive: true }); + + // Run Prisma migrate + // throws when migrate deploy fails + await execAsync("pnpm prisma migrate deploy"); + console.log(`Successfully applied schema migration: ${migration.name}`); + } +}; + +const loadMigrations = async (): Promise => { + const migrations: DataMigrationScript[] = []; + + const entries = await fs.readdir(MIGRATIONS_DIR, { withFileTypes: true }); + + // Filter only directories (each migration is in a directory) + const migrationDirs = entries + .filter((dirent) => dirent.isDirectory()) + .map((d) => d.name) + .sort(); // Assuming timestamped names, sorting ensures the correct order + + // Separate sets for schema and data migrations + const schemaMigrationNames = new Set(); + const dataMigrationNames = new Set(); + + // To keep track of duplicates for error reporting + const duplicateSchemaMigrations: string[] = []; + const duplicateDataMigrations: string[] = []; + + for (const dirName of migrationDirs) { + const migrationPath = path.join(MIGRATIONS_DIR, dirName); + const files = await fs.readdir(migrationPath); + + const hasSchemaMigration = files.includes("migration.sql"); + const hasDataMigration = files.includes("migration.ts"); + + if (hasSchemaMigration && hasDataMigration) { + throw new Error( + `Migration directory ${dirName} has both migration.sql and migration.ts. This should not happen.` + ); + } + + // Extract the migration name (underscored part after timestamp) + const migrationNameMatch = /^\d+_(?.+)$/.exec(dirName); + if (!migrationNameMatch) { + throw new Error(`Invalid migration directory name format: ${dirName}`); + } + + const migrationName = migrationNameMatch[1]; + + if (hasSchemaMigration) { + // It's a schema migration + if (schemaMigrationNames.has(migrationName)) { + duplicateSchemaMigrations.push(migrationName); + } else { + schemaMigrationNames.add(migrationName); + } + + // It's a schema migration + // We just create an object with type: "schema" and name: dirName + migrations.push({ + type: "schema", + name: dirName, + } as DataMigrationScript); + } else if (hasDataMigration) { + // Check for duplicates among data migrations + if (dataMigrationNames.has(migrationName)) { + duplicateDataMigrations.push(migrationName); + } else { + dataMigrationNames.add(migrationName); + } + + // It's a data migration, dynamically import and extract the scripts + const modulePath = path.join(migrationPath, "migration.ts"); + const mod = (await import(modulePath)) as Record; + + // Check each export in the module for a DataMigrationScript (type: "data") + for (const key of Object.keys(mod)) { + const exportedValue = mod[key]; + if (exportedValue && typeof exportedValue === "object" && exportedValue.type === "data") { + migrations.push(exportedValue); + } + } + } else { + console.warn( + `Migration directory ${dirName} doesn't have migration.sql or data-migration.ts. Skipping...` + ); + } + } + + // If any duplicate migration names are found for the same type, throw an error + if (duplicateSchemaMigrations.length > 0 || duplicateDataMigrations.length > 0) { + const errorParts: string[] = []; + if (duplicateSchemaMigrations.length > 0) { + errorParts.push(`Schema migrations: ${duplicateSchemaMigrations.join(", ")}`); + } + if (duplicateDataMigrations.length > 0) { + errorParts.push(`Data migrations: ${duplicateDataMigrations.join(", ")}`); + } + + throw new Error( + `Duplicate migration names found for the same type: ${errorParts.join(" | ")}. Please make sure each migration has a unique name within its type.` + ); + } + + return migrations; +}; + +export async function applyMigrations(): Promise { + try { + const allMigrations = await loadMigrations(); + console.log(`Loaded ${allMigrations.length.toString()} migrations from ${MIGRATIONS_DIR}`); + await runMigrations(allMigrations); + } catch (error) { + await prisma.$disconnect(); + throw error; + } +} + +// async function isSchemaMigrationApplied(migrationName: string, prismaClient: PrismaClient): Promise { +// try { +// const applied: unknown[] = await prismaClient.$queryRaw` +// SELECT 1 +// FROM _prisma_migrations +// WHERE migration_name = ${migrationName} +// AND finished_at IS NOT NULL +// LIMIT 1; +// `; +// return applied.length > 0; +// } catch (err) { +// console.log("Error: ", err); +// return false; +// } +// } diff --git a/packages/database/zod-utils.ts b/packages/database/zod-utils.ts index 131e7e21e9..cf12e1a769 100644 --- a/packages/database/zod-utils.ts +++ b/packages/database/zod-utils.ts @@ -5,7 +5,7 @@ export const ZActionProperties = z.record(z.string()); export { ZActionClassNoCodeConfig } from "../types/action-classes"; export { ZIntegrationConfig } from "../types/integration"; -export { ZResponseData, ZResponsePersonAttributes, ZResponseMeta, ZResponseTtc } from "../types/responses"; +export { ZResponseData, ZResponseMeta, ZResponseTtc } from "../types/responses"; export { ZSurveyWelcomeCard, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d8e926e86..05774be0a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -336,7 +336,7 @@ importers: version: 8.3.5 tsup: specifier: 8.3.0 - version: 8.3.0(@microsoft/api-extractor@7.43.0(@types/node@22.3.0))(jiti@2.3.3)(postcss@8.4.49)(tsx@4.16.5)(typescript@5.7.2)(yaml@2.6.1) + version: 8.3.0(@microsoft/api-extractor@7.48.0(@types/node@22.3.0))(jiti@2.3.3)(postcss@8.4.49)(tsx@4.16.5)(typescript@5.7.2)(yaml@2.6.1) vite: specifier: 5.4.8 version: 5.4.8(@types/node@22.3.0)(terser@5.31.6) @@ -3268,16 +3268,29 @@ packages: '@microsoft/api-extractor-model@7.28.13': resolution: {integrity: sha512-39v/JyldX4MS9uzHcdfmjjfS6cYGAoXV+io8B5a338pkHiSt+gy2eXQ0Q7cGFJ7quSa1VqqlMdlPrB6sLR/cAw==} + '@microsoft/api-extractor-model@7.30.0': + resolution: {integrity: sha512-26/LJZBrsWDKAkOWRiQbdVgcfd1F3nyJnAiJzsAgpouPk7LtOIj7PK9aJtBaw/pUXrkotEg27RrT+Jm/q0bbug==} + '@microsoft/api-extractor@7.43.0': resolution: {integrity: sha512-GFhTcJpB+MI6FhvXEI9b2K0snulNLWHqC/BbcJtyNYcKUiw7l3Lgis5ApsYncJ0leALX7/of4XfmXk+maT111w==} hasBin: true + '@microsoft/api-extractor@7.48.0': + resolution: {integrity: sha512-FMFgPjoilMUWeZXqYRlJ3gCVRhB7WU/HN88n8OLqEsmsG4zBdX/KQdtJfhq95LQTQ++zfu0Em1LLb73NqRCLYQ==} + hasBin: true + '@microsoft/tsdoc-config@0.16.2': resolution: {integrity: sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==} + '@microsoft/tsdoc-config@0.17.1': + resolution: {integrity: sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw==} + '@microsoft/tsdoc@0.14.2': resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==} + '@microsoft/tsdoc@0.15.1': + resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@neshca/cache-handler@1.7.4': resolution: {integrity: sha512-LcswRRzamauTwNZX+0pBGdGslHsvVrmZvOMLzeSxJnCXZyOHTH8CwJIV4bZzrPcC0e0/OLqZVrBwlry7UesMRg==} peerDependencies: @@ -4854,9 +4867,20 @@ packages: '@types/node': optional: true + '@rushstack/node-core-library@5.10.0': + resolution: {integrity: sha512-2pPLCuS/3x7DCd7liZkqOewGM0OzLyCacdvOe8j6Yrx9LkETGnxul1t7603bIaB8nUAooORcct9fFDOQMbWAgw==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + '@rushstack/rig-package@0.5.2': resolution: {integrity: sha512-mUDecIJeH3yYGZs2a48k+pbhM6JYwWlgjs2Ca5f2n1G2/kgdgP9D/07oglEGf6mRyXEnazhEENeYTSNDRCwdqA==} + '@rushstack/rig-package@0.5.3': + resolution: {integrity: sha512-olzSSjYrvCNxUFZowevC3uz8gvKr3WTpHQ7BkpjtRpA3wK+T0ybep/SRUMfr195gBzJm5gaXw0ZMgjIyHqJUow==} + '@rushstack/terminal@0.10.0': resolution: {integrity: sha512-UbELbXnUdc7EKwfH2sb8ChqNgapUOdqcCIdQP4NGxBpTZV2sQyeekuK3zmfQSa/MN+/7b4kBogl2wq0vpkpYGw==} peerDependencies: @@ -4865,9 +4889,20 @@ packages: '@types/node': optional: true + '@rushstack/terminal@0.14.3': + resolution: {integrity: sha512-csXbZsAdab/v8DbU1sz7WC2aNaKArcdS/FPmXMOXEj/JBBZMvDK0+1b4Qao0kkG0ciB1Qe86/Mb68GjH6/TnMw==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + '@rushstack/ts-command-line@4.19.1': resolution: {integrity: sha512-J7H768dgcpG60d7skZ5uSSwyCZs/S2HrWP1Ds8d1qYAyaaeJmpmmLr9BVw97RjFzmQPOYnoXcKA4GkqDCkduQg==} + '@rushstack/ts-command-line@4.23.1': + resolution: {integrity: sha512-40jTmYoiu/xlIpkkRsVfENtBq4CW3R4azbL0Vmda+fMwHWqss6wwf/Cy/UJmMqIzpfYc2OTnjYP1ZLD3CmyeCA==} + '@segment/loosely-validate-event@2.0.0': resolution: {integrity: sha512-ZMCSfztDBqwotkl848ODgVcAmN4OItEWDCkshcKz0/W6gGSQayuuCtWV/MlodFivAZD793d6UgANd6wCXUfrIw==} @@ -6334,6 +6369,22 @@ packages: zod: optional: true + ajv-draft-04@1.0.0: + resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv-keywords@3.5.2: resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} peerDependencies: @@ -6345,6 +6396,9 @@ packages: ajv@8.12.0: resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + ajv@8.13.0: + resolution: {integrity: sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==} + algoliasearch@4.24.0: resolution: {integrity: sha512-bf0QV/9jVejssFBmz2HQLxUadxk574t4iwjCKp5E7NBzwKkrDEhKPISIIjAU/p6K5qDx3qoeh4+26zWN1jmw3g==} @@ -12480,12 +12534,15 @@ packages: sudo-prompt@8.2.5: resolution: {integrity: sha512-rlBo3HU/1zAJUrkY6jNxDOC9eVYliG6nS4JA8u8KAshITd07tafMc/Br7xQwCSseXwJ2iCcHCE8SNWX3q8Z+kw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. sudo-prompt@9.1.1: resolution: {integrity: sha512-es33J1g2HjMpyAhz8lOR+ICmXXAqTuKbuXuUWLhOLew20oN9oUCgCJx615U/v7aioZg7IX5lIh9x34vwneu4pA==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. sudo-prompt@9.2.1: resolution: {integrity: sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. superjson@2.2.1: resolution: {integrity: sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==} @@ -16721,6 +16778,15 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@microsoft/api-extractor-model@7.30.0(@types/node@22.3.0)': + dependencies: + '@microsoft/tsdoc': 0.15.1 + '@microsoft/tsdoc-config': 0.17.1 + '@rushstack/node-core-library': 5.10.0(@types/node@22.3.0) + transitivePeerDependencies: + - '@types/node' + optional: true + '@microsoft/api-extractor@7.43.0(@types/node@22.3.0)': dependencies: '@microsoft/api-extractor-model': 7.28.13(@types/node@22.3.0) @@ -16739,6 +16805,25 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@microsoft/api-extractor@7.48.0(@types/node@22.3.0)': + dependencies: + '@microsoft/api-extractor-model': 7.30.0(@types/node@22.3.0) + '@microsoft/tsdoc': 0.15.1 + '@microsoft/tsdoc-config': 0.17.1 + '@rushstack/node-core-library': 5.10.0(@types/node@22.3.0) + '@rushstack/rig-package': 0.5.3 + '@rushstack/terminal': 0.14.3(@types/node@22.3.0) + '@rushstack/ts-command-line': 4.23.1(@types/node@22.3.0) + lodash: 4.17.21 + minimatch: 3.0.8 + resolve: 1.22.8 + semver: 7.5.4 + source-map: 0.6.1 + typescript: 5.4.2 + transitivePeerDependencies: + - '@types/node' + optional: true + '@microsoft/tsdoc-config@0.16.2': dependencies: '@microsoft/tsdoc': 0.14.2 @@ -16746,8 +16831,19 @@ snapshots: jju: 1.4.0 resolve: 1.19.0 + '@microsoft/tsdoc-config@0.17.1': + dependencies: + '@microsoft/tsdoc': 0.15.1 + ajv: 8.12.0 + jju: 1.4.0 + resolve: 1.22.8 + optional: true + '@microsoft/tsdoc@0.14.2': {} + '@microsoft/tsdoc@0.15.1': + optional: true + '@neshca/cache-handler@1.7.4(next@15.1.0(@opentelemetry/api@1.9.0)(@playwright/test@1.45.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(redis@4.7.0)': dependencies: cluster-key-slot: 1.1.2 @@ -18675,11 +18771,31 @@ snapshots: optionalDependencies: '@types/node': 22.3.0 + '@rushstack/node-core-library@5.10.0(@types/node@22.3.0)': + dependencies: + ajv: 8.13.0 + ajv-draft-04: 1.0.0(ajv@8.13.0) + ajv-formats: 3.0.1(ajv@8.13.0) + fs-extra: 7.0.1 + import-lazy: 4.0.0 + jju: 1.4.0 + resolve: 1.22.8 + semver: 7.5.4 + optionalDependencies: + '@types/node': 22.3.0 + optional: true + '@rushstack/rig-package@0.5.2': dependencies: resolve: 1.22.8 strip-json-comments: 3.1.1 + '@rushstack/rig-package@0.5.3': + dependencies: + resolve: 1.22.8 + strip-json-comments: 3.1.1 + optional: true + '@rushstack/terminal@0.10.0(@types/node@22.3.0)': dependencies: '@rushstack/node-core-library': 4.0.2(@types/node@22.3.0) @@ -18687,6 +18803,14 @@ snapshots: optionalDependencies: '@types/node': 22.3.0 + '@rushstack/terminal@0.14.3(@types/node@22.3.0)': + dependencies: + '@rushstack/node-core-library': 5.10.0(@types/node@22.3.0) + supports-color: 8.1.1 + optionalDependencies: + '@types/node': 22.3.0 + optional: true + '@rushstack/ts-command-line@4.19.1(@types/node@22.3.0)': dependencies: '@rushstack/terminal': 0.10.0(@types/node@22.3.0) @@ -18696,6 +18820,16 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@rushstack/ts-command-line@4.23.1(@types/node@22.3.0)': + dependencies: + '@rushstack/terminal': 0.14.3(@types/node@22.3.0) + '@types/argparse': 1.0.38 + argparse: 1.0.10 + string-argv: 0.3.2 + transitivePeerDependencies: + - '@types/node' + optional: true + '@segment/loosely-validate-event@2.0.0': dependencies: component-type: 1.2.2 @@ -20731,6 +20865,16 @@ snapshots: - solid-js - vue + ajv-draft-04@1.0.0(ajv@8.13.0): + optionalDependencies: + ajv: 8.13.0 + optional: true + + ajv-formats@3.0.1(ajv@8.13.0): + optionalDependencies: + ajv: 8.13.0 + optional: true + ajv-keywords@3.5.2(ajv@6.12.6): dependencies: ajv: 6.12.6 @@ -20749,6 +20893,14 @@ snapshots: require-from-string: 2.0.2 uri-js: 4.4.1 + ajv@8.13.0: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + optional: true + algoliasearch@4.24.0: dependencies: '@algolia/cache-browser-local-storage': 4.24.0 @@ -28616,7 +28768,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.3.0(@microsoft/api-extractor@7.43.0(@types/node@22.3.0))(jiti@2.3.3)(postcss@8.4.49)(tsx@4.16.5)(typescript@5.7.2)(yaml@2.6.1): + tsup@8.3.0(@microsoft/api-extractor@7.48.0(@types/node@22.3.0))(jiti@2.3.3)(postcss@8.4.49)(tsx@4.16.5)(typescript@5.7.2)(yaml@2.6.1): dependencies: bundle-require: 5.0.0(esbuild@0.23.1) cac: 6.7.14 @@ -28635,7 +28787,7 @@ snapshots: tinyglobby: 0.2.10 tree-kill: 1.2.2 optionalDependencies: - '@microsoft/api-extractor': 7.43.0(@types/node@22.3.0) + '@microsoft/api-extractor': 7.48.0(@types/node@22.3.0) postcss: 8.4.49 typescript: 5.7.2 transitivePeerDependencies: diff --git a/turbo.json b/turbo.json index b73f0293c9..e53a608acd 100644 --- a/turbo.json +++ b/turbo.json @@ -187,13 +187,11 @@ "outputs": [] }, "db:migrate:deploy": { + "cache": false, "outputs": [] }, "db:migrate:dev": { - "outputs": [] - }, - "db:migrate:vercel": { - "env": ["MIGRATE_DATABASE_URL", "DATABASE_URL"], + "cache": false, "outputs": [] }, "db:push": {