mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-31 02:40:31 -06:00
Compare commits
7 Commits
v4.5.0-rc.
...
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:start": "turbo run db:start",
|
||||
"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:down": "docker compose -f docker-compose.dev.yml down",
|
||||
"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
|
||||
- Copies migration to Prisma's internal directory
|
||||
- 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
|
||||
|
||||
@@ -92,6 +98,8 @@ Run these commands from the `packages/database` directory:
|
||||
- Creates new subdirectory with appropriate timestamp
|
||||
- Generates `migration.ts` file with pre-configured ID and name
|
||||
- **Note**: Only use Prisma raw queries in data migrations for better performance and to avoid type errors
|
||||
- **`pnpm db:seed`**: Run the seeding script
|
||||
- **`pnpm db:seed:clear`**: Clear data and run the seeding script
|
||||
|
||||
### Available Scripts
|
||||
|
||||
@@ -102,13 +110,41 @@ Run these commands from the `packages/database` directory:
|
||||
"db:migrate:deploy": "Apply migrations in production",
|
||||
"db:migrate:dev": "Apply migrations in development",
|
||||
"db:push": "prisma db push --accept-data-loss",
|
||||
"db:setup": "pnpm db:migrate:dev && pnpm db:create-saml-database:dev",
|
||||
"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",
|
||||
"generate": "prisma generate",
|
||||
"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
|
||||
|
||||
### Adding a Schema Migration
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
},
|
||||
"./zod/*": {
|
||||
"import": "./zod/*.ts"
|
||||
},
|
||||
"./seed/constants": {
|
||||
"import": "./src/seed/constants.ts"
|
||||
}
|
||||
},
|
||||
"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:dev": "dotenv -e ../../.env -- node ./dist/scripts/create-saml-database.js",
|
||||
"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",
|
||||
"format": "prisma format",
|
||||
"generate": "prisma generate",
|
||||
@@ -45,17 +50,20 @@
|
||||
"@formbricks/logger": "workspace:*",
|
||||
"@paralleldrive/cuid2": "2.2.2",
|
||||
"@prisma/client": "6.14.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"zod": "3.24.4",
|
||||
"zod-openapi": "4.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formbricks/config-typescript": "workspace:*",
|
||||
"@formbricks/eslint-config": "workspace:*",
|
||||
"@types/bcryptjs": "2.4.6",
|
||||
"dotenv-cli": "8.0.0",
|
||||
"glob": "11.1.0",
|
||||
"prisma": "6.14.0",
|
||||
"prisma-json-types-generator": "3.5.4",
|
||||
"ts-node": "10.9.2",
|
||||
"tsx": "4.19.2",
|
||||
"vite": "6.4.1",
|
||||
"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(),
|
||||
ctaButtonLabel: ZI18nString.optional(),
|
||||
}).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.buttonUrl || data.buttonUrl.trim() === "") {
|
||||
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": []
|
||||
},
|
||||
"db:seed": {
|
||||
"env": ["ALLOW_SEED"],
|
||||
"outputs": []
|
||||
},
|
||||
"db:setup": {
|
||||
|
||||
Reference in New Issue
Block a user