mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-02 20:00:16 -06:00
Compare commits
7 Commits
fix/attrib
...
feat/datab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e39cdd7a2 | ||
|
|
1859091efd | ||
|
|
b9b052c102 | ||
|
|
9dd604524f | ||
|
|
5cc2a61089 | ||
|
|
520dc1f7b6 | ||
|
|
6ca21bca5e |
@@ -18,6 +18,8 @@
|
|||||||
"db:migrate:deploy": "turbo run db:migrate:deploy",
|
"db:migrate:deploy": "turbo run db:migrate:deploy",
|
||||||
"db:start": "turbo run db:start",
|
"db:start": "turbo run db:start",
|
||||||
"db:push": "turbo run db:push",
|
"db:push": "turbo run db:push",
|
||||||
|
"db:seed": "turbo run db:seed",
|
||||||
|
"db:seed:clear": "turbo run db:seed -- -- --clear",
|
||||||
"db:up": "docker compose -f docker-compose.dev.yml up -d",
|
"db:up": "docker compose -f docker-compose.dev.yml up -d",
|
||||||
"db:down": "docker compose -f docker-compose.dev.yml down",
|
"db:down": "docker compose -f docker-compose.dev.yml down",
|
||||||
"go": "pnpm db:up && turbo run go --concurrency 20",
|
"go": "pnpm db:up && turbo run go --concurrency 20",
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ Run these commands from the root directory of the Formbricks monorepo:
|
|||||||
- Generates new `migration.sql` in the custom directory
|
- Generates new `migration.sql` in the custom directory
|
||||||
- Copies migration to Prisma's internal directory
|
- Copies migration to Prisma's internal directory
|
||||||
- Applies all pending migrations to the database
|
- Applies all pending migrations to the database
|
||||||
|
- **`pnpm db:seed`**: Seed the database with sample data
|
||||||
|
- Upserts base infrastructure (Organization, Project, Environments)
|
||||||
|
- Creates multi-role users (Admin, Manager)
|
||||||
|
- Generates complex surveys and sample responses
|
||||||
|
- **`pnpm db:seed:clear`**: Clear all seeded data and re-seed
|
||||||
|
- **WARNING**: This will delete existing data in the database.
|
||||||
|
|
||||||
### Package Level Commands
|
### Package Level Commands
|
||||||
|
|
||||||
@@ -92,6 +98,8 @@ Run these commands from the `packages/database` directory:
|
|||||||
- Creates new subdirectory with appropriate timestamp
|
- Creates new subdirectory with appropriate timestamp
|
||||||
- Generates `migration.ts` file with pre-configured ID and name
|
- Generates `migration.ts` file with pre-configured ID and name
|
||||||
- **Note**: Only use Prisma raw queries in data migrations for better performance and to avoid type errors
|
- **Note**: Only use Prisma raw queries in data migrations for better performance and to avoid type errors
|
||||||
|
- **`pnpm db:seed`**: Run the seeding script
|
||||||
|
- **`pnpm db:seed:clear`**: Clear data and run the seeding script
|
||||||
|
|
||||||
### Available Scripts
|
### Available Scripts
|
||||||
|
|
||||||
@@ -102,13 +110,41 @@ Run these commands from the `packages/database` directory:
|
|||||||
"db:migrate:deploy": "Apply migrations in production",
|
"db:migrate:deploy": "Apply migrations in production",
|
||||||
"db:migrate:dev": "Apply migrations in development",
|
"db:migrate:dev": "Apply migrations in development",
|
||||||
"db:push": "prisma db push --accept-data-loss",
|
"db:push": "prisma db push --accept-data-loss",
|
||||||
"db:setup": "pnpm db:migrate:dev && pnpm db:create-saml-database:dev",
|
"db:seed": "Seed the database with sample data",
|
||||||
|
"db:seed:clear": "Clear all data and re-seed",
|
||||||
|
"db:setup": "pnpm db:migrate:dev && pnpm db:create-saml-database:dev && pnpm db:seed",
|
||||||
"dev": "vite build --watch",
|
"dev": "vite build --watch",
|
||||||
"generate": "prisma generate",
|
"generate": "prisma generate",
|
||||||
"generate-data-migration": "Create new data migration"
|
"generate-data-migration": "Create new data migration"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Database Seeding
|
||||||
|
|
||||||
|
The seeding system provides a quick way to set up a functional environment for development, QA, and testing.
|
||||||
|
|
||||||
|
### Safety Guard
|
||||||
|
|
||||||
|
To prevent accidental data loss in production, seeding is blocked if `NODE_ENV=production`. If you explicitly need to seed a production-like environment (e.g., staging), you must set:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ALLOW_SEED=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Seeding Logic
|
||||||
|
|
||||||
|
The `pnpm db:seed` script:
|
||||||
|
1. **Infrastructure**: Upserts a default organization, project, and environments.
|
||||||
|
2. **Users**: Creates default users with the following credentials (passwords are hashed):
|
||||||
|
- **Admin**: `admin@formbricks.com` / `password123`
|
||||||
|
- **Manager**: `manager@formbricks.com` / `password123`
|
||||||
|
3. **Surveys**: Creates complex sample surveys (Kitchen Sink, CSAT, Draft, etc.) in the **Production** environment.
|
||||||
|
4. **Responses**: Generates ~50 realistic responses and displays for each survey.
|
||||||
|
|
||||||
|
### Idempotency
|
||||||
|
|
||||||
|
By default, the seed script uses `upsert` to ensure it can be run multiple times without creating duplicate infrastructure. To perform a clean reset, use `pnpm db:seed:clear`.
|
||||||
|
|
||||||
## Migration Workflow
|
## Migration Workflow
|
||||||
|
|
||||||
### Adding a Schema Migration
|
### Adding a Schema Migration
|
||||||
|
|||||||
@@ -22,6 +22,9 @@
|
|||||||
},
|
},
|
||||||
"./zod/*": {
|
"./zod/*": {
|
||||||
"import": "./zod/*.ts"
|
"import": "./zod/*.ts"
|
||||||
|
},
|
||||||
|
"./seed/constants": {
|
||||||
|
"import": "./src/seed/constants.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -33,7 +36,9 @@
|
|||||||
"db:create-saml-database:deploy": "env SAML_DATABASE_URL=\"${SAML_DATABASE_URL}\" node ./dist/scripts/create-saml-database.js",
|
"db:create-saml-database:deploy": "env SAML_DATABASE_URL=\"${SAML_DATABASE_URL}\" node ./dist/scripts/create-saml-database.js",
|
||||||
"db:create-saml-database:dev": "dotenv -e ../../.env -- node ./dist/scripts/create-saml-database.js",
|
"db:create-saml-database:dev": "dotenv -e ../../.env -- node ./dist/scripts/create-saml-database.js",
|
||||||
"db:push": "prisma db push --accept-data-loss",
|
"db:push": "prisma db push --accept-data-loss",
|
||||||
"db:setup": "pnpm db:migrate:dev && pnpm db:create-saml-database:dev",
|
"db:seed": "dotenv -e ../../.env -- tsx src/seed.ts",
|
||||||
|
"db:seed:clear": "dotenv -e ../../.env -- tsx src/seed.ts --clear",
|
||||||
|
"db:setup": "pnpm db:migrate:dev && pnpm db:create-saml-database:dev && pnpm db:seed",
|
||||||
"db:start": "pnpm db:setup",
|
"db:start": "pnpm db:setup",
|
||||||
"format": "prisma format",
|
"format": "prisma format",
|
||||||
"generate": "prisma generate",
|
"generate": "prisma generate",
|
||||||
@@ -45,17 +50,20 @@
|
|||||||
"@formbricks/logger": "workspace:*",
|
"@formbricks/logger": "workspace:*",
|
||||||
"@paralleldrive/cuid2": "2.2.2",
|
"@paralleldrive/cuid2": "2.2.2",
|
||||||
"@prisma/client": "6.14.0",
|
"@prisma/client": "6.14.0",
|
||||||
|
"bcryptjs": "2.4.3",
|
||||||
"zod": "3.24.4",
|
"zod": "3.24.4",
|
||||||
"zod-openapi": "4.2.4"
|
"zod-openapi": "4.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@formbricks/config-typescript": "workspace:*",
|
"@formbricks/config-typescript": "workspace:*",
|
||||||
"@formbricks/eslint-config": "workspace:*",
|
"@formbricks/eslint-config": "workspace:*",
|
||||||
|
"@types/bcryptjs": "2.4.6",
|
||||||
"dotenv-cli": "8.0.0",
|
"dotenv-cli": "8.0.0",
|
||||||
"glob": "11.1.0",
|
"glob": "11.1.0",
|
||||||
"prisma": "6.14.0",
|
"prisma": "6.14.0",
|
||||||
"prisma-json-types-generator": "3.5.4",
|
"prisma-json-types-generator": "3.5.4",
|
||||||
"ts-node": "10.9.2",
|
"ts-node": "10.9.2",
|
||||||
|
"tsx": "4.19.2",
|
||||||
"vite": "6.4.1",
|
"vite": "6.4.1",
|
||||||
"vite-plugin-dts": "4.5.3"
|
"vite-plugin-dts": "4.5.3"
|
||||||
}
|
}
|
||||||
|
|||||||
596
packages/database/src/seed.ts
Normal file
596
packages/database/src/seed.ts
Normal file
@@ -0,0 +1,596 @@
|
|||||||
|
import { createId } from "@paralleldrive/cuid2";
|
||||||
|
import { type Prisma, PrismaClient } from "@prisma/client";
|
||||||
|
import bcrypt from "bcryptjs";
|
||||||
|
import { logger } from "@formbricks/logger";
|
||||||
|
import { SEED_CREDENTIALS, SEED_IDS } from "./seed/constants";
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
const isProduction = process.env.NODE_ENV === "production";
|
||||||
|
const allowSeed = process.env.ALLOW_SEED === "true";
|
||||||
|
|
||||||
|
if (isProduction && !allowSeed) {
|
||||||
|
logger.error("ERROR: Seeding blocked in production. Set ALLOW_SEED=true to override.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearData = process.argv.includes("--clear");
|
||||||
|
|
||||||
|
// Define local types to avoid resolution issues in seed script
|
||||||
|
type SurveyElementType =
|
||||||
|
| "openText"
|
||||||
|
| "multipleChoiceSingle"
|
||||||
|
| "multipleChoiceMulti"
|
||||||
|
| "nps"
|
||||||
|
| "cta"
|
||||||
|
| "rating"
|
||||||
|
| "consent"
|
||||||
|
| "date"
|
||||||
|
| "matrix"
|
||||||
|
| "address"
|
||||||
|
| "ranking"
|
||||||
|
| "contactInfo";
|
||||||
|
|
||||||
|
interface SurveyQuestion {
|
||||||
|
id: string;
|
||||||
|
type: SurveyElementType;
|
||||||
|
headline: { default: string; [key: string]: string };
|
||||||
|
subheader?: { default: string; [key: string]: string };
|
||||||
|
required?: boolean;
|
||||||
|
placeholder?: { default: string; [key: string]: string };
|
||||||
|
longAnswer?: boolean;
|
||||||
|
choices?: { id: string; label: { default: string }; imageUrl?: string }[];
|
||||||
|
lowerLabel?: { default: string };
|
||||||
|
upperLabel?: { default: string };
|
||||||
|
buttonLabel?: { default: string };
|
||||||
|
buttonUrl?: string;
|
||||||
|
buttonExternal?: boolean;
|
||||||
|
dismissButtonLabel?: { default: string };
|
||||||
|
ctaButtonLabel?: { default: string };
|
||||||
|
scale?: string;
|
||||||
|
range?: number;
|
||||||
|
label?: { default: string };
|
||||||
|
allowMulti?: boolean;
|
||||||
|
format?: string;
|
||||||
|
rows?: { id: string; label: { default: string } }[];
|
||||||
|
columns?: { id: string; label: { default: string } }[];
|
||||||
|
addressLine1?: { show: boolean; required: boolean; placeholder: { default: string } };
|
||||||
|
addressLine2?: { show: boolean; required: boolean; placeholder: { default: string } };
|
||||||
|
city?: { show: boolean; required: boolean; placeholder: { default: string } };
|
||||||
|
state?: { show: boolean; required: boolean; placeholder: { default: string } };
|
||||||
|
zip?: { show: boolean; required: boolean; placeholder: { default: string } };
|
||||||
|
country?: { show: boolean; required: boolean; placeholder: { default: string } };
|
||||||
|
firstName?: { show: boolean; required: boolean; placeholder: { default: string } };
|
||||||
|
lastName?: { show: boolean; required: boolean; placeholder: { default: string } };
|
||||||
|
email?: { show: boolean; required: boolean; placeholder: { default: string } };
|
||||||
|
phone?: { show: boolean; required: boolean; placeholder: { default: string } };
|
||||||
|
company?: { show: boolean; required: boolean; placeholder: { default: string } };
|
||||||
|
allowMultipleFiles?: boolean;
|
||||||
|
maxSizeInMB?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteData(): Promise<void> {
|
||||||
|
logger.info("Clearing existing data...");
|
||||||
|
|
||||||
|
const deleteOrder: Prisma.TypeMap["meta"]["modelProps"][] = [
|
||||||
|
"responseQuotaLink",
|
||||||
|
"surveyQuota",
|
||||||
|
"tagsOnResponses",
|
||||||
|
"tag",
|
||||||
|
"surveyFollowUp",
|
||||||
|
"response",
|
||||||
|
"display",
|
||||||
|
"surveyTrigger",
|
||||||
|
"surveyAttributeFilter",
|
||||||
|
"surveyLanguage",
|
||||||
|
"survey",
|
||||||
|
"actionClass",
|
||||||
|
"contactAttribute",
|
||||||
|
"contactAttributeKey",
|
||||||
|
"contact",
|
||||||
|
"apiKeyEnvironment",
|
||||||
|
"apiKey",
|
||||||
|
"segment",
|
||||||
|
"webhook",
|
||||||
|
"integration",
|
||||||
|
"projectTeam",
|
||||||
|
"teamUser",
|
||||||
|
"team",
|
||||||
|
"project",
|
||||||
|
"invite",
|
||||||
|
"membership",
|
||||||
|
"account",
|
||||||
|
"user",
|
||||||
|
"organization",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const model of deleteOrder) {
|
||||||
|
try {
|
||||||
|
// @ts-expect-error - prisma[model] is not typed correctly
|
||||||
|
await prisma[model].deleteMany();
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
||||||
|
logger.error(`Could not delete data from ${model}: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Data cleared.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const KITCHEN_SINK_QUESTIONS: SurveyQuestion[] = [
|
||||||
|
{
|
||||||
|
id: createId(),
|
||||||
|
type: "openText",
|
||||||
|
headline: { default: "What do you think of Formbricks?" },
|
||||||
|
subheader: { default: "Please be honest!" },
|
||||||
|
required: true,
|
||||||
|
placeholder: { default: "Your feedback here..." },
|
||||||
|
longAnswer: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: createId(),
|
||||||
|
type: "multipleChoiceSingle",
|
||||||
|
headline: { default: "How often do you use Formbricks?" },
|
||||||
|
required: true,
|
||||||
|
choices: [
|
||||||
|
{ id: createId(), label: { default: "Daily" } },
|
||||||
|
{ id: createId(), label: { default: "Weekly" } },
|
||||||
|
{ id: createId(), label: { default: "Monthly" } },
|
||||||
|
{ id: createId(), label: { default: "Rarely" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: createId(),
|
||||||
|
type: "multipleChoiceMulti",
|
||||||
|
headline: { default: "Which features do you use?" },
|
||||||
|
required: false,
|
||||||
|
choices: [
|
||||||
|
{ id: createId(), label: { default: "Surveys" } },
|
||||||
|
{ id: createId(), label: { default: "Analytics" } },
|
||||||
|
{ id: createId(), label: { default: "Integrations" } },
|
||||||
|
{ id: createId(), label: { default: "Action Tracking" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: createId(),
|
||||||
|
type: "nps",
|
||||||
|
headline: { default: "How likely are you to recommend Formbricks?" },
|
||||||
|
required: true,
|
||||||
|
lowerLabel: { default: "Not likely" },
|
||||||
|
upperLabel: { default: "Very likely" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: createId(),
|
||||||
|
type: "cta",
|
||||||
|
headline: { default: "Check out our documentation!" },
|
||||||
|
required: true,
|
||||||
|
ctaButtonLabel: { default: "Go to Docs" },
|
||||||
|
buttonUrl: "https://formbricks.com/docs",
|
||||||
|
buttonExternal: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: createId(),
|
||||||
|
type: "rating",
|
||||||
|
headline: { default: "Rate your overall experience" },
|
||||||
|
required: true,
|
||||||
|
scale: "star",
|
||||||
|
range: 5,
|
||||||
|
lowerLabel: { default: "Poor" },
|
||||||
|
upperLabel: { default: "Excellent" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: createId(),
|
||||||
|
type: "consent",
|
||||||
|
headline: { default: "Do you agree to our terms?" },
|
||||||
|
required: true,
|
||||||
|
label: { default: "I agree to the terms and conditions" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: createId(),
|
||||||
|
type: "date",
|
||||||
|
headline: { default: "When did you start using Formbricks?" },
|
||||||
|
required: true,
|
||||||
|
format: "M-d-y",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: createId(),
|
||||||
|
type: "matrix",
|
||||||
|
headline: { default: "How do you feel about these aspects?" },
|
||||||
|
required: true,
|
||||||
|
rows: [
|
||||||
|
{ id: createId(), label: { default: "UI Design" } },
|
||||||
|
{ id: createId(), label: { default: "Performance" } },
|
||||||
|
{ id: createId(), label: { default: "Documentation" } },
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
{ id: createId(), label: { default: "Disappointed" } },
|
||||||
|
{ id: createId(), label: { default: "Neutral" } },
|
||||||
|
{ id: createId(), label: { default: "Satisfied" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: createId(),
|
||||||
|
type: "address",
|
||||||
|
headline: { default: "Where are you located?" },
|
||||||
|
required: true,
|
||||||
|
addressLine1: { show: true, required: true, placeholder: { default: "Address Line 1" } },
|
||||||
|
addressLine2: { show: true, required: false, placeholder: { default: "Address Line 2" } },
|
||||||
|
city: { show: true, required: true, placeholder: { default: "City" } },
|
||||||
|
state: { show: true, required: true, placeholder: { default: "State" } },
|
||||||
|
zip: { show: true, required: true, placeholder: { default: "Zip" } },
|
||||||
|
country: { show: true, required: true, placeholder: { default: "Country" } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: createId(),
|
||||||
|
type: "ranking",
|
||||||
|
headline: { default: "Rank these features" },
|
||||||
|
required: true,
|
||||||
|
choices: [
|
||||||
|
{ id: createId(), label: { default: "Feature A" } },
|
||||||
|
{ id: createId(), label: { default: "Feature B" } },
|
||||||
|
{ id: createId(), label: { default: "Feature C" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: createId(),
|
||||||
|
type: "contactInfo",
|
||||||
|
headline: { default: "How can we reach you?" },
|
||||||
|
required: true,
|
||||||
|
firstName: { show: true, required: true, placeholder: { default: "First Name" } },
|
||||||
|
lastName: { show: true, required: true, placeholder: { default: "Last Name" } },
|
||||||
|
email: { show: true, required: true, placeholder: { default: "Email" } },
|
||||||
|
phone: { show: true, required: false, placeholder: { default: "Phone" } },
|
||||||
|
company: { show: true, required: false, placeholder: { default: "Company" } },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface SurveyBlock {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
elements: SurveyQuestion[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResponseValue = string | number | string[] | Record<string, string>;
|
||||||
|
|
||||||
|
const generateQuestionResponse = (q: SurveyQuestion, index: number): ResponseValue | undefined => {
|
||||||
|
const responseGenerators: Record<SurveyElementType, () => ResponseValue | undefined> = {
|
||||||
|
openText: () => `Sample response ${String(index)}`,
|
||||||
|
multipleChoiceSingle: () =>
|
||||||
|
q.choices ? q.choices[Math.floor(Math.random() * q.choices.length)].label.default : undefined,
|
||||||
|
multipleChoiceMulti: () =>
|
||||||
|
q.choices ? [q.choices[0].label.default, q.choices[1].label.default] : undefined,
|
||||||
|
nps: () => Math.floor(Math.random() * 11),
|
||||||
|
rating: () => (q.range ? Math.floor(Math.random() * q.range) + 1 : undefined),
|
||||||
|
cta: () => "clicked",
|
||||||
|
consent: () => "accepted",
|
||||||
|
date: () => new Date().toISOString().split("T")[0],
|
||||||
|
matrix: () => {
|
||||||
|
const matrixData: Record<string, string> = {};
|
||||||
|
if (q.rows && q.columns) {
|
||||||
|
for (const row of q.rows) {
|
||||||
|
matrixData[row.label.default] =
|
||||||
|
q.columns[Math.floor(Math.random() * q.columns.length)].label.default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matrixData;
|
||||||
|
},
|
||||||
|
ranking: () =>
|
||||||
|
q.choices ? q.choices.map((c) => c.label.default).sort(() => Math.random() - 0.5) : undefined,
|
||||||
|
address: () => ({
|
||||||
|
addressLine1: "Main St 1",
|
||||||
|
city: "Berlin",
|
||||||
|
state: "Berlin",
|
||||||
|
zip: "10115",
|
||||||
|
country: "Germany",
|
||||||
|
}),
|
||||||
|
contactInfo: () => ({
|
||||||
|
firstName: "John",
|
||||||
|
lastName: "Doe",
|
||||||
|
email: `john.doe.${String(index)}@example.com`,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return responseGenerators[q.type]();
|
||||||
|
};
|
||||||
|
|
||||||
|
async function generateResponses(surveyId: string, count: number): Promise<void> {
|
||||||
|
logger.info(`Generating ${String(count)} responses for survey ${surveyId}...`);
|
||||||
|
const survey = await prisma.survey.findUnique({
|
||||||
|
where: { id: surveyId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!survey) return;
|
||||||
|
|
||||||
|
const blocks = survey.blocks as unknown as SurveyBlock[];
|
||||||
|
const questions = blocks.flatMap((block) => block.elements);
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const data: Record<string, ResponseValue> = {};
|
||||||
|
for (const q of questions) {
|
||||||
|
const response = generateQuestionResponse(q, i);
|
||||||
|
if (response !== undefined) {
|
||||||
|
data[q.id] = response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.$transaction(async (tx) => {
|
||||||
|
const display = await tx.display.create({
|
||||||
|
data: {
|
||||||
|
surveyId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await tx.response.create({
|
||||||
|
data: {
|
||||||
|
surveyId,
|
||||||
|
finished: true,
|
||||||
|
// @ts-expect-error - data is not typed correctly
|
||||||
|
data: data as unknown as Prisma.InputJsonValue,
|
||||||
|
displayId: display.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate some displays without responses (e.g., 30% more)
|
||||||
|
const extraDisplays = Math.floor(count * 0.3);
|
||||||
|
logger.info(`Generating ${String(extraDisplays)} extra displays for survey ${surveyId}...`);
|
||||||
|
|
||||||
|
for (let i = 0; i < extraDisplays; i++) {
|
||||||
|
await prisma.display.create({
|
||||||
|
data: {
|
||||||
|
surveyId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
if (clearData) {
|
||||||
|
await deleteData();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Seeding base infrastructure...");
|
||||||
|
|
||||||
|
// Organization
|
||||||
|
const organization = await prisma.organization.upsert({
|
||||||
|
where: { id: SEED_IDS.ORGANIZATION },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
id: SEED_IDS.ORGANIZATION,
|
||||||
|
name: "Seed Organization",
|
||||||
|
billing: {
|
||||||
|
plan: "free",
|
||||||
|
limits: { projects: 3, monthly: { responses: 1500, miu: 2000 } },
|
||||||
|
stripeCustomerId: null,
|
||||||
|
periodStart: new Date(),
|
||||||
|
period: "monthly",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Users
|
||||||
|
const passwordHash = await bcrypt.hash(SEED_CREDENTIALS.ADMIN.password, 10);
|
||||||
|
|
||||||
|
await prisma.user.upsert({
|
||||||
|
where: { id: SEED_IDS.USER_ADMIN },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
id: SEED_IDS.USER_ADMIN,
|
||||||
|
name: "Admin User",
|
||||||
|
email: SEED_CREDENTIALS.ADMIN.email,
|
||||||
|
password: passwordHash,
|
||||||
|
emailVerified: new Date(),
|
||||||
|
memberships: {
|
||||||
|
create: {
|
||||||
|
organizationId: organization.id,
|
||||||
|
role: "owner",
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.user.upsert({
|
||||||
|
where: { id: SEED_IDS.USER_MANAGER },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
id: SEED_IDS.USER_MANAGER,
|
||||||
|
name: "Manager User",
|
||||||
|
email: SEED_CREDENTIALS.MANAGER.email,
|
||||||
|
password: passwordHash,
|
||||||
|
emailVerified: new Date(),
|
||||||
|
memberships: {
|
||||||
|
create: {
|
||||||
|
organizationId: organization.id,
|
||||||
|
role: "manager",
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.user.upsert({
|
||||||
|
where: { id: SEED_IDS.USER_MEMBER },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
id: SEED_IDS.USER_MEMBER,
|
||||||
|
name: "Member User",
|
||||||
|
email: SEED_CREDENTIALS.MEMBER.email,
|
||||||
|
password: passwordHash,
|
||||||
|
emailVerified: new Date(),
|
||||||
|
memberships: {
|
||||||
|
create: {
|
||||||
|
organizationId: organization.id,
|
||||||
|
role: "member",
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Project
|
||||||
|
const project = await prisma.project.upsert({
|
||||||
|
where: { id: SEED_IDS.PROJECT },
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
id: SEED_IDS.PROJECT,
|
||||||
|
name: "Seed Project",
|
||||||
|
organizationId: organization.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Environments
|
||||||
|
await prisma.environment.upsert({
|
||||||
|
where: { id: SEED_IDS.ENV_DEV },
|
||||||
|
update: { appSetupCompleted: false },
|
||||||
|
create: {
|
||||||
|
id: SEED_IDS.ENV_DEV,
|
||||||
|
type: "development",
|
||||||
|
projectId: project.id,
|
||||||
|
appSetupCompleted: false,
|
||||||
|
attributeKeys: {
|
||||||
|
create: [
|
||||||
|
{ name: "Email", key: "email", isUnique: true, type: "default" },
|
||||||
|
{ name: "First Name", key: "firstName", isUnique: false, type: "default" },
|
||||||
|
{ name: "Last Name", key: "lastName", isUnique: false, type: "default" },
|
||||||
|
{ name: "userId", key: "userId", isUnique: true, type: "default" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const prodEnv = await prisma.environment.upsert({
|
||||||
|
where: { id: SEED_IDS.ENV_PROD },
|
||||||
|
update: { appSetupCompleted: false },
|
||||||
|
create: {
|
||||||
|
id: SEED_IDS.ENV_PROD,
|
||||||
|
type: "production",
|
||||||
|
projectId: project.id,
|
||||||
|
appSetupCompleted: false,
|
||||||
|
attributeKeys: {
|
||||||
|
create: [
|
||||||
|
{ name: "Email", key: "email", isUnique: true, type: "default" },
|
||||||
|
{ name: "First Name", key: "firstName", isUnique: false, type: "default" },
|
||||||
|
{ name: "Last Name", key: "lastName", isUnique: false, type: "default" },
|
||||||
|
{ name: "userId", key: "userId", isUnique: true, type: "default" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info("Seeding surveys...");
|
||||||
|
|
||||||
|
const createSurveyWithBlocks = async (
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
environmentId: string,
|
||||||
|
status: "inProgress" | "draft" | "completed",
|
||||||
|
questions: SurveyQuestion[]
|
||||||
|
): Promise<void> => {
|
||||||
|
const blocks = [
|
||||||
|
{
|
||||||
|
id: createId(),
|
||||||
|
name: "Main Block",
|
||||||
|
elements: questions,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await prisma.survey.upsert({
|
||||||
|
where: { id },
|
||||||
|
update: {
|
||||||
|
environmentId,
|
||||||
|
type: "link",
|
||||||
|
// @ts-expect-error - blocks is not typed correctly
|
||||||
|
blocks: blocks as unknown as Prisma.InputJsonValue[],
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
environmentId,
|
||||||
|
status,
|
||||||
|
type: "link",
|
||||||
|
// @ts-expect-error - blocks is not typed correctly
|
||||||
|
blocks: blocks as unknown as Prisma.InputJsonValue[],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Kitchen Sink Survey
|
||||||
|
await createSurveyWithBlocks(
|
||||||
|
SEED_IDS.SURVEY_KITCHEN_SINK,
|
||||||
|
"Kitchen Sink Survey",
|
||||||
|
prodEnv.id,
|
||||||
|
"inProgress",
|
||||||
|
KITCHEN_SINK_QUESTIONS
|
||||||
|
);
|
||||||
|
|
||||||
|
// CSAT Survey
|
||||||
|
await createSurveyWithBlocks(SEED_IDS.SURVEY_CSAT, "CSAT Survey", prodEnv.id, "inProgress", [
|
||||||
|
{
|
||||||
|
id: createId(),
|
||||||
|
type: "rating",
|
||||||
|
headline: { default: "How satisfied are you with our product?" },
|
||||||
|
required: true,
|
||||||
|
scale: "smiley",
|
||||||
|
range: 5,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Draft Survey
|
||||||
|
await createSurveyWithBlocks(SEED_IDS.SURVEY_DRAFT, "Draft Survey", prodEnv.id, "draft", [
|
||||||
|
{
|
||||||
|
id: createId(),
|
||||||
|
type: "openText",
|
||||||
|
headline: { default: "Coming soon..." },
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Completed Survey
|
||||||
|
await createSurveyWithBlocks(SEED_IDS.SURVEY_COMPLETED, "Exit Survey", prodEnv.id, "completed", [
|
||||||
|
{
|
||||||
|
id: createId(),
|
||||||
|
type: "multipleChoiceSingle",
|
||||||
|
headline: { default: "Why are you leaving?" },
|
||||||
|
required: true,
|
||||||
|
choices: [
|
||||||
|
{ id: createId(), label: { default: "Too expensive" } },
|
||||||
|
{ id: createId(), label: { default: "Found a better alternative" } },
|
||||||
|
{ id: createId(), label: { default: "Missing features" } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
logger.info("Generating responses...");
|
||||||
|
|
||||||
|
await generateResponses(SEED_IDS.SURVEY_KITCHEN_SINK, 50);
|
||||||
|
await generateResponses(SEED_IDS.SURVEY_CSAT, 50);
|
||||||
|
await generateResponses(SEED_IDS.SURVEY_COMPLETED, 50);
|
||||||
|
|
||||||
|
logger.info(`\n${"=".repeat(50)}`);
|
||||||
|
logger.info("🚀 SEEDING COMPLETED SUCCESSFULLY");
|
||||||
|
logger.info("=".repeat(50));
|
||||||
|
logger.info("\nLog in with the following credentials:");
|
||||||
|
logger.info(`\n Admin (Owner):`);
|
||||||
|
logger.info(` Email: ${SEED_CREDENTIALS.ADMIN.email}`);
|
||||||
|
logger.info(` Password: (see SEED_CREDENTIALS configuration in constants.ts)`);
|
||||||
|
logger.info(`\n Manager:`);
|
||||||
|
logger.info(` Email: ${SEED_CREDENTIALS.MANAGER.email}`);
|
||||||
|
logger.info(` Password: (see SEED_CREDENTIALS configuration in constants.ts)`);
|
||||||
|
logger.info(`\n Member:`);
|
||||||
|
logger.info(` Email: ${SEED_CREDENTIALS.MEMBER.email}`);
|
||||||
|
logger.info(` Password: (see SEED_CREDENTIALS configuration in constants.ts)`);
|
||||||
|
logger.info(`\n${"=".repeat(50)}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch((e: unknown) => {
|
||||||
|
logger.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
prisma.$disconnect().catch((e: unknown) => {
|
||||||
|
logger.error(e, "Error disconnecting prisma");
|
||||||
|
});
|
||||||
|
});
|
||||||
19
packages/database/src/seed/constants.ts
Normal file
19
packages/database/src/seed/constants.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export const SEED_IDS = {
|
||||||
|
USER_ADMIN: "clseedadmin000000000000",
|
||||||
|
USER_MANAGER: "clseedmanager0000000000",
|
||||||
|
USER_MEMBER: "clseedmember00000000000",
|
||||||
|
ORGANIZATION: "clseedorg0000000000000",
|
||||||
|
PROJECT: "clseedproject000000000",
|
||||||
|
ENV_DEV: "clseedenvdev0000000000",
|
||||||
|
ENV_PROD: "clseedenvprod000000000",
|
||||||
|
SURVEY_KITCHEN_SINK: "clseedsurveykitchen00",
|
||||||
|
SURVEY_CSAT: "clseedsurveycsat000000",
|
||||||
|
SURVEY_DRAFT: "clseedsurveydraft00000",
|
||||||
|
SURVEY_COMPLETED: "clseedsurveycomplete00",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const SEED_CREDENTIALS = {
|
||||||
|
ADMIN: { email: "admin@formbricks.com", password: "Password#123" },
|
||||||
|
MANAGER: { email: "manager@formbricks.com", password: "Password#123" },
|
||||||
|
MEMBER: { email: "member@formbricks.com", password: "Password#123" },
|
||||||
|
} as const;
|
||||||
@@ -162,7 +162,7 @@ export const ZSurveyCTAElement = ZSurveyElementBase.extend({
|
|||||||
buttonUrl: z.string().optional(),
|
buttonUrl: z.string().optional(),
|
||||||
ctaButtonLabel: ZI18nString.optional(),
|
ctaButtonLabel: ZI18nString.optional(),
|
||||||
}).superRefine((data, ctx) => {
|
}).superRefine((data, ctx) => {
|
||||||
// When buttonExternal is true, buttonUrl is required and must be valid
|
// When buttonExternal is true, buttonUrl and ctaButtonLabel are required
|
||||||
if (data.buttonExternal) {
|
if (data.buttonExternal) {
|
||||||
if (!data.buttonUrl || data.buttonUrl.trim() === "") {
|
if (!data.buttonUrl || data.buttonUrl.trim() === "") {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
@@ -181,6 +181,14 @@ export const ZSurveyCTAElement = ZSurveyElementBase.extend({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!data.ctaButtonLabel?.default || data.ctaButtonLabel.default.trim() === "") {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Button label is required when external button is enabled",
|
||||||
|
path: ["ctaButtonLabel"],
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
1032
pnpm-lock.yaml
generated
1032
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -274,6 +274,7 @@
|
|||||||
"outputs": []
|
"outputs": []
|
||||||
},
|
},
|
||||||
"db:seed": {
|
"db:seed": {
|
||||||
|
"env": ["ALLOW_SEED"],
|
||||||
"outputs": []
|
"outputs": []
|
||||||
},
|
},
|
||||||
"db:setup": {
|
"db:setup": {
|
||||||
|
|||||||
Reference in New Issue
Block a user