Compare commits

..

3 Commits

Author SHA1 Message Date
Dhruwang 5239dfd9b1 chore(db): add nullable workspaceId to environment-owned models
Phase 1 of environment deprecation: adds optional workspaceId column
with FK, index, and cascade delete to all 9 environment-owned models
(Survey, Contact, ActionClass, ContactAttributeKey, Webhook, Tag,
Segment, Integration, ApiKeyEnvironment) and reverse relations on
Workspace.

Replaces the previous projectId approach (reverted in the prior commit)
to align with the Project → Workspace rename from epic/v5.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 10:57:59 +05:30
Dhruwang d45cbefcff Merge remote-tracking branch 'origin/epic/v5' into revert/remove-projectid-from-env-models
# Conflicts:
#	apps/web/app/(app)/(onboarding)/environments/[environmentId]/connect/page.tsx
#	apps/web/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/page.tsx
#	apps/web/app/(app)/(onboarding)/organizations/[organizationId]/workspaces/new/settings/page.tsx
#	apps/web/app/(app)/environments/[environmentId]/actions.ts
#	apps/web/app/(app)/environments/[environmentId]/components/EnvironmentLayout.tsx
#	apps/web/app/(app)/environments/[environmentId]/settings/(account)/layout.tsx
#	apps/web/app/(app)/environments/[environmentId]/settings/(organization)/layout.tsx
#	apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/emailTemplate.tsx
#	apps/web/modules/ee/contacts/[contactId]/components/activity-section.tsx
#	apps/web/modules/ee/contacts/components/contacts-secondary-navigation.tsx
#	apps/web/modules/ee/contacts/layout.tsx
#	apps/web/modules/ee/whitelabel/remove-branding/actions.ts
#	apps/web/modules/environments/lib/utils.test.ts
#	apps/web/modules/environments/lib/utils.ts
#	apps/web/modules/projects/settings/general/components/delete-project.tsx
#	apps/web/modules/survey/editor/page.tsx
#	apps/web/modules/survey/list/page.tsx
#	apps/web/modules/survey/templates/page.tsx
#	apps/web/modules/workspaces/settings/actions.ts
#	apps/web/modules/workspaces/settings/look/page.tsx
2026-04-01 10:47:48 +05:30
Dhruwang f1c6180ae2 Revert "chore(db): add nullable projectId to environment-owned models (#7588)"
This reverts commit 71cca557fc.
2026-04-01 10:27:41 +05:30
51 changed files with 130 additions and 71 deletions
@@ -26,7 +26,7 @@ const Page = async (props: ConnectPageProps) => {
const workspace = await getWorkspaceByEnvironmentId(environment.id);
if (!workspace) {
throw new ResourceNotFoundError(t("common.workspace"), null);
throw new Error(t("common.workspace_not_found"));
}
const channel = workspace.config.channel || null;
@@ -39,7 +39,7 @@ const Page = async (props: XMTemplatePageProps) => {
const workspace = await getWorkspaceByEnvironmentId(environment.id);
if (!workspace) {
throw new ResourceNotFoundError(t("common.workspace"), null);
throw new Error(t("common.workspace_not_found"));
}
const workspaces = await getUserWorkspaces(session.user.id, organizationId);
@@ -43,7 +43,7 @@ export const EnvironmentLayout = async ({ layoutData, children }: EnvironmentLay
// Validate that workspace permission exists for members
if (isMember && !workspacePermission) {
throw new ResourceNotFoundError(t("common.workspace"), null);
throw new Error(t("common.workspace_permission_not_found"));
}
return (
@@ -25,7 +25,7 @@ const AccountSettingsLayout = async (props: {
}
if (!workspace) {
throw new ResourceNotFoundError(t("common.workspace"), null);
throw new Error(t("common.workspace_not_found"));
}
if (!session) {
@@ -22,7 +22,7 @@ const Layout = async (props: { params: Promise<{ environmentId: string }>; child
}
if (!workspace) {
throw new ResourceNotFoundError(t("common.workspace"), null);
throw new Error(t("common.workspace_not_found"));
}
if (!session) {
@@ -14,7 +14,7 @@ export const getEmailTemplateHtml = async (surveyId: string, locale: string) =>
}
const workspace = await getWorkspaceByEnvironmentId(survey.environmentId);
if (!workspace) {
throw new ResourceNotFoundError(t("common.workspace"), null);
throw new Error("Workspace not found");
}
const styling = getStyling(workspace, survey);
@@ -45,6 +45,7 @@ export const responseSelection = {
updatedAt: true,
name: true,
environmentId: true,
workspaceId: true,
},
},
},
@@ -19,6 +19,7 @@ const selectActionClass = {
key: true,
noCodeConfig: true,
environmentId: true,
workspaceId: true,
} satisfies Prisma.ActionClassSelect;
export const getActionClasses = reactCache(async (environmentIds: string[]): Promise<TActionClass[]> => {
@@ -50,6 +50,7 @@ export const responseSelection = {
updatedAt: true,
name: true,
environmentId: true,
workspaceId: true,
},
},
},
+1
View File
@@ -4823,6 +4823,7 @@ export const previewSurvey = (workspaceName: string, t: TFunction): TSurvey => {
name: t("templates.preview_survey_name"),
type: "link" as const,
environmentId: "cltwumfcz0009echxg02fh7oa",
workspaceId: null,
createdBy: "cltwumfbz0000echxysz6ptvq",
status: "inProgress" as const,
welcomeCard: {
+1
View File
@@ -21,6 +21,7 @@ const selectActionClass = {
key: true,
noCodeConfig: true,
environmentId: true,
workspaceId: true,
} satisfies Prisma.ActionClassSelect;
export const getActionClasses = reactCache(
+1
View File
@@ -75,6 +75,7 @@ export const responseSelection = {
updatedAt: true,
name: true,
environmentId: true,
workspaceId: true,
},
},
},
@@ -19,6 +19,7 @@ const selectContact = {
createdAt: true,
updatedAt: true,
environmentId: true,
workspaceId: true,
attributes: {
select: {
value: true,
@@ -41,6 +42,7 @@ const commonMockProperties = {
createdAt: currentDate,
updatedAt: currentDate,
environmentId: mockId,
workspaceId: null,
};
type SurveyMock = Prisma.SurveyGetPayload<{
+2
View File
@@ -30,6 +30,7 @@ export const selectSurvey = {
name: true,
type: true,
environmentId: true,
workspaceId: true,
createdBy: true,
status: true,
welcomeCard: true,
@@ -84,6 +85,7 @@ export const selectSurvey = {
createdAt: true,
updatedAt: true,
environmentId: true,
workspaceId: true,
name: true,
description: true,
type: true,
@@ -58,6 +58,7 @@ export const getResponseForPipeline = async (
updatedAt: true,
name: true,
environmentId: true,
workspaceId: true,
},
},
},
@@ -140,6 +140,7 @@ describe("Response Lib", () => {
updatedAt: new Date(),
name: "important",
environmentId: "env123",
workspaceId: null,
},
},
],
@@ -163,6 +164,7 @@ describe("Response Lib", () => {
updatedAt: mockPrismaResponse.tags[0].tag.updatedAt,
name: "important",
environmentId: "env123",
workspaceId: null,
},
],
});
@@ -184,6 +186,7 @@ describe("Response Lib", () => {
updatedAt: true,
name: true,
environmentId: true,
workspaceId: true,
},
},
},
@@ -17,6 +17,7 @@ export const ZWebhookUpdateSchema = ZWebhook.omit({
createdAt: true,
updatedAt: true,
environmentId: true,
workspaceId: true,
secret: true,
}).meta({
id: "webhookUpdate",
@@ -50,7 +50,7 @@ export const ActivitySection = async ({ environment, contactId, environmentTags
const workspace = await getWorkspaceByEnvironmentId(environment.id);
if (!workspace) {
throw new ResourceNotFoundError(t("common.workspace"), null);
throw new Error(t("common.workspace_not_found"));
}
const workspacePermission = await getWorkspacePermissionByUserId(session.user.id, workspace.id);
@@ -1,4 +1,3 @@
import { ResourceNotFoundError } from "@formbricks/types/errors";
import { TWorkspace } from "@formbricks/types/workspace";
import { getWorkspaceByEnvironmentId } from "@/lib/workspace/service";
import { getTranslate } from "@/lingodotdev/server";
@@ -21,7 +20,7 @@ export const ContactsSecondaryNavigation = async ({
workspace = await getWorkspaceByEnvironmentId(environmentId);
if (!workspace) {
throw new ResourceNotFoundError(t("common.workspace"), null);
throw new Error(t("common.workspace_not_found"));
}
}
+1 -1
View File
@@ -45,7 +45,7 @@ const ConfigLayout = async (props: {
const workspace = await getWorkspaceByEnvironmentId(params.environmentId);
if (!workspace) {
throw new ResourceNotFoundError(t("common.workspace"), null);
throw new Error(t("common.workspace_not_found"));
}
return children;
@@ -98,6 +98,7 @@ const selectContact = {
createdAt: true,
updatedAt: true,
environmentId: true,
workspaceId: true,
attributes: {
select: {
value: true,
@@ -45,6 +45,7 @@ export function CreateSegmentModal({
isPrivate: false,
filters: [],
environmentId,
workspaceId: null,
id: "",
surveys: [],
createdAt: new Date(),
@@ -55,6 +55,7 @@ export const selectSegment = {
title: true,
description: true,
environmentId: true,
workspaceId: true,
filters: true,
isPrivate: true,
surveys: {
@@ -47,7 +47,7 @@ export const updateWorkspaceBrandingAction = authenticatedActionClient
const organization = await getOrganization(organizationId);
if (!organization) {
throw new ResourceNotFoundError("Organization", organizationId);
throw new Error("Organization not found");
}
const canRemoveBranding = await getRemoveBrandingPermission(organizationId);
@@ -168,7 +168,7 @@ describe("utils.ts", () => {
test("throws error if workspace not found", async () => {
vi.mocked(getWorkspaceByEnvironmentId).mockResolvedValueOnce(null);
await expect(getEnvironmentAuth("env123")).rejects.toThrow(ResourceNotFoundError);
await expect(getEnvironmentAuth("env123")).rejects.toThrow("common.workspace_not_found");
});
test("throws error if environment not found", async () => {
+1 -1
View File
@@ -48,7 +48,7 @@ export const getEnvironmentAuth = reactCache(async (environmentId: string): Prom
]);
if (!workspace) {
throw new ResourceNotFoundError(t("common.workspace"), null);
throw new Error(t("common.workspace_not_found"));
}
if (!environment) {
@@ -27,6 +27,7 @@ export const WebhookTable = ({
const { t } = useTranslation();
const [activeWebhook, setActiveWebhook] = useState<Webhook>({
environmentId: environment.id,
workspaceId: null,
id: "",
name: "",
url: "",
@@ -49,6 +49,7 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowT
isPrivate: true,
title: localSurvey.id,
environmentId: environment.id,
workspaceId: null,
surveys: [localSurvey.id],
filters: [],
createdAt: new Date(),
+1 -1
View File
@@ -61,7 +61,7 @@ export const SurveyEditorPage = async (props: {
]);
if (!workspaceWithTeamIds) {
throw new ResourceNotFoundError(t("common.workspace"), null);
throw new Error(t("common.workspace_not_found"));
}
const organizationBilling = await getOrganizationBilling(workspaceWithTeamIds.organizationId);
+3
View File
@@ -14,6 +14,7 @@ export const selectSurvey = {
name: true,
type: true,
environmentId: true,
workspaceId: true,
createdBy: true,
status: true,
welcomeCard: true,
@@ -69,6 +70,7 @@ export const selectSurvey = {
createdAt: true,
updatedAt: true,
environmentId: true,
workspaceId: true,
name: true,
description: true,
type: true,
@@ -84,6 +86,7 @@ export const selectSurvey = {
createdAt: true,
updatedAt: true,
environmentId: true,
workspaceId: true,
title: true,
description: true,
isPrivate: true,
@@ -15,6 +15,7 @@ export const surveySelect = {
status: true,
singleUse: true,
environmentId: true,
workspaceId: true,
_count: {
select: { responses: true },
},
+1 -1
View File
@@ -34,7 +34,7 @@ export const SurveysPage = async ({ params: paramsProps }: SurveyTemplateProps)
const workspace = await getWorkspaceWithTeamIdsByEnvironmentId(params.environmentId);
if (!workspace) {
throw new ResourceNotFoundError(t("common.workspace"), null);
throw new Error(t("common.workspace_not_found"));
}
const { session, isBilling, environment, isReadOnly } = await getEnvironmentAuth(params.environmentId);
@@ -9,6 +9,7 @@ export const getMinimalSurvey = (t: TFunction): TSurvey => ({
name: "Minimal Survey",
type: "app",
environmentId: "someEnvId1",
workspaceId: null,
createdBy: null,
status: "draft",
displayOption: "displayOnce",
+1 -1
View File
@@ -22,7 +22,7 @@ export const SurveyTemplatesPage = async (props: SurveyTemplateProps) => {
const workspace = await getWorkspaceWithTeamIdsByEnvironmentId(environmentId);
if (!workspace) {
throw new ResourceNotFoundError(t("common.workspace"), null);
throw new Error(t("common.workspace_not_found"));
}
if (isReadOnly) {
@@ -25,7 +25,7 @@ export const WorkspaceLookSettingsPage = async (props: { params: Promise<{ envir
const workspace = await getWorkspaceByEnvironmentId(params.environmentId);
if (!workspace) {
throw new ResourceNotFoundError(t("common.workspace"), null);
throw new Error("Workspace not found");
}
const canRemoveBranding = await getRemoveBrandingPermission(organization.id);
@@ -47,7 +47,12 @@ export const xmSegmentMigration: MigrationScript = {
id: "s644oyyqccstfdeejc4fluye",
name: "20241209110456_xm_segment_migration",
run: async ({ tx }) => {
const allSegments = await tx.segment.findMany();
const allSegments = await tx.segment.findMany({
select: {
id: true,
filters: true,
},
});
const updationPromises = [];
for (const segment of allSegments) {
updationPromises.push(
@@ -56,6 +61,7 @@ export const xmSegmentMigration: MigrationScript = {
data: {
filters: findAndReplace(segment.filters),
},
select: { id: true },
})
);
}
@@ -0,0 +1,32 @@
-- AlterTable: Add nullable workspaceId to all environment-owned models
ALTER TABLE "Webhook" ADD COLUMN "workspaceId" TEXT;
ALTER TABLE "ContactAttributeKey" ADD COLUMN "workspaceId" TEXT;
ALTER TABLE "Contact" ADD COLUMN "workspaceId" TEXT;
ALTER TABLE "Tag" ADD COLUMN "workspaceId" TEXT;
ALTER TABLE "Survey" ADD COLUMN "workspaceId" TEXT;
ALTER TABLE "ActionClass" ADD COLUMN "workspaceId" TEXT;
ALTER TABLE "Integration" ADD COLUMN "workspaceId" TEXT;
ALTER TABLE "ApiKeyEnvironment" ADD COLUMN "workspaceId" TEXT;
ALTER TABLE "Segment" ADD COLUMN "workspaceId" TEXT;
-- AddForeignKey
ALTER TABLE "Webhook" ADD CONSTRAINT "Webhook_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ContactAttributeKey" ADD CONSTRAINT "ContactAttributeKey_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Contact" ADD CONSTRAINT "Contact_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Tag" ADD CONSTRAINT "Tag_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Survey" ADD CONSTRAINT "Survey_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ActionClass" ADD CONSTRAINT "ActionClass_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Integration" ADD CONSTRAINT "Integration_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "ApiKeyEnvironment" ADD CONSTRAINT "ApiKeyEnvironment_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Segment" ADD CONSTRAINT "Segment_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- CreateIndex
CREATE INDEX "Webhook_workspaceId_idx" ON "Webhook"("workspaceId");
CREATE INDEX "ContactAttributeKey_workspaceId_created_at_idx" ON "ContactAttributeKey"("workspaceId", "created_at");
CREATE INDEX "Contact_workspaceId_idx" ON "Contact"("workspaceId");
CREATE INDEX "Tag_workspaceId_idx" ON "Tag"("workspaceId");
CREATE INDEX "Survey_workspaceId_updated_at_idx" ON "Survey"("workspaceId", "updated_at");
CREATE INDEX "ActionClass_workspaceId_created_at_idx" ON "ActionClass"("workspaceId", "created_at");
CREATE INDEX "Integration_workspaceId_idx" ON "Integration"("workspaceId");
CREATE INDEX "ApiKeyEnvironment_workspaceId_idx" ON "ApiKeyEnvironment"("workspaceId");
CREATE INDEX "Segment_workspaceId_idx" ON "Segment"("workspaceId");
@@ -1,53 +0,0 @@
import { logger } from "@formbricks/logger";
import type { MigrationScript } from "../../src/scripts/migration-runner";
// Table names are from a hardcoded const array, not user input.
// $executeRawUnsafe is required because Postgres does not support parameterized identifiers.
const TABLES_TO_BACKFILL = [
"Survey",
"Contact",
"ActionClass",
"ContactAttributeKey",
"Webhook",
"Tag",
"Segment",
"Integration",
"ApiKeyEnvironment",
] as const;
export const backfillWorkspaceId: MigrationScript = {
type: "data",
id: "snae9apsx7e74yo9ncmhjl47",
name: "20260401000001_backfill_workspace_id",
run: async ({ tx }) => {
for (const table of TABLES_TO_BACKFILL) {
const updatedRows = await tx.$executeRawUnsafe(`
UPDATE "${table}" t
SET "workspaceId" = e."workspaceId"
FROM "Environment" e
WHERE t."environmentId" = e."id"
AND t."workspaceId" IS NULL
`);
logger.info(`Backfilled ${updatedRows.toString()} rows in ${table}`);
}
// Verify no rows were missed.
// Any remaining NULL workspaceId indicates orphaned rows (environmentId references a
// non-existent Environment). The FK cascade should prevent this, but we check anyway.
const failures: string[] = [];
for (const table of TABLES_TO_BACKFILL) {
const nullCount: [{ count: bigint }] = await tx.$queryRawUnsafe(`
SELECT COUNT(*) as count FROM "${table}" WHERE "workspaceId" IS NULL
`);
if (nullCount[0].count > 0n) {
failures.push(`${table}: ${nullCount[0].count.toString()} rows with NULL workspaceId`);
}
}
if (failures.length > 0) {
throw new Error(`Backfill verification failed:\n${failures.join("\n")}`);
}
},
};
+38
View File
@@ -49,11 +49,14 @@ model Webhook {
source WebhookSource @default(user)
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
environmentId String
workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
workspaceId String?
triggers PipelineTriggers[]
surveyIds String[]
secret String?
@@index([environmentId])
@@index([workspaceId])
}
/// Represents an attribute value associated with a contact.
@@ -116,11 +119,14 @@ model ContactAttributeKey {
dataType ContactAttributeDataType @default(string)
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
environmentId String
workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
workspaceId String?
attributes ContactAttribute[]
attributeFilters SurveyAttributeFilter[]
@@unique([key, environmentId])
@@index([environmentId, createdAt])
@@index([workspaceId, createdAt])
}
/// Represents a person or user who can receive and respond to surveys.
@@ -137,11 +143,14 @@ model Contact {
updatedAt DateTime @updatedAt @map(name: "updated_at")
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
environmentId String
workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
workspaceId String?
responses Response[]
attributes ContactAttribute[]
displays Display[]
@@index([environmentId])
@@index([workspaceId])
}
/// Stores a user's response to a survey, including their answers and metadata.
@@ -204,8 +213,11 @@ model Tag {
responses TagsOnResponses[]
environmentId String
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
workspaceId String?
@@unique([environmentId, name])
@@index([workspaceId])
}
/// Junction table linking tags to responses.
@@ -350,6 +362,8 @@ model Survey {
type SurveyType @default(app)
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
environmentId String
workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
workspaceId String?
creator User? @relation(fields: [createdBy], references: [id])
createdBy String?
status SurveyStatus @default(draft)
@@ -413,6 +427,7 @@ model Survey {
@@index([environmentId, updatedAt])
@@index([segmentId])
@@index([workspaceId, updatedAt])
}
/// Represents a quota configuration for a survey.
@@ -507,11 +522,14 @@ model ActionClass {
noCodeConfig Json?
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
environmentId String
workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
workspaceId String?
surveyTriggers SurveyTrigger[]
@@unique([key, environmentId])
@@unique([name, environmentId])
@@index([environmentId, createdAt])
@@index([workspaceId, createdAt])
}
enum EnvironmentType {
@@ -540,9 +558,12 @@ model Integration {
/// [IntegrationConfig]
config Json
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
workspaceId String?
@@unique([type, environmentId])
@@index([environmentId])
@@index([workspaceId])
}
enum DataMigrationStatus {
@@ -649,6 +670,17 @@ model Workspace {
customHeadScripts String? // Custom HTML scripts for link surveys (self-hosted only)
feedbackRecordDirectoryWorkspaces FeedbackRecordDirectoryWorkspace[]
// Direct resource relations (for environment deprecation migration)
surveys Survey[]
contacts Contact[]
actionClasses ActionClass[]
contactAttributeKeys ContactAttributeKey[]
webhooks Webhook[]
tags Tag[]
segments Segment[]
integrations Integration[]
apiKeyEnvironments ApiKeyEnvironment[]
@@unique([organizationId, name])
}
@@ -809,10 +841,13 @@ model ApiKeyEnvironment {
apiKey ApiKey @relation(fields: [apiKeyId], references: [id], onDelete: Cascade)
environmentId String
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
workspaceId String?
permission ApiKeyPermission
@@unique([apiKeyId, environmentId])
@@index([environmentId])
@@index([workspaceId])
}
enum IdentityProvider {
@@ -912,9 +947,12 @@ model Segment {
filters Json @default("[]")
environmentId String
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
workspace Workspace? @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
workspaceId String?
surveys Survey[]
@@unique([environmentId, title])
@@index([workspaceId])
}
/// Represents a supported language in the system.
@@ -54,6 +54,7 @@ export const ZContactAttributeKey = z.object({
})
.describe("The data type of the attribute (string, number, date)"),
environmentId: z.cuid2().describe("The ID of the environment this attribute belongs to"),
workspaceId: z.string().nullable(),
}) satisfies z.ZodType<ContactAttributeKey>;
ZContactAttributeKey.meta({
+1
View File
@@ -17,6 +17,7 @@ export const ZContact = z.object({
})
.describe("When the contact was last updated"),
environmentId: z.string().describe("The environment this contact belongs to"),
workspaceId: z.string().nullable(),
}) satisfies z.ZodType<Contact>;
ZContact.meta({
+1
View File
@@ -72,6 +72,7 @@ const ZSurveyBase = z.object({
pin: z.string().nullable().describe("The pin of the survey"),
createdBy: z.string().nullable().describe("The user who created the survey"),
environmentId: z.cuid2().describe("The environment ID of the survey"),
workspaceId: z.string().nullable(),
questions: z.array(ZSurveyQuestion).describe("The questions of the survey"),
blocks: ZSurveyBlocks.prefault([]).describe("The blocks of the survey"),
endings: z.array(ZSurveyEnding).prefault([]).describe("The endings of the survey"),
+1
View File
@@ -19,6 +19,7 @@ export const ZWebhook = z.object({
url: z.url().describe("The URL of the webhook"),
source: z.enum(["user", "zapier", "make", "n8n"]).describe("The source of the webhook"),
environmentId: z.cuid2().describe("The ID of the environment"),
workspaceId: z.string().nullable(),
triggers: z
.array(z.enum(["responseFinished", "responseCreated", "responseUpdated"]))
.describe("The triggers of the webhook")
@@ -62,6 +62,7 @@ export const mockSurvey: TEnvironmentStateSurvey = {
createdAt: new Date("2025-01-01T10:00:00Z"),
updatedAt: new Date("2025-01-01T10:00:00Z"),
environmentId: mockEnvironmentId,
workspaceId: null,
description: "Manual Trigger",
noCodeConfig: {
elementSelector: { cssSelector: ".btn", innerHtml: "Click me" },
+1
View File
@@ -135,6 +135,7 @@ export const ZActionClass = z.object({
key: z.string().trim().min(1).nullable(),
noCodeConfig: ZActionClassNoCodeConfig.nullable(),
environmentId: z.string(),
workspaceId: z.string().nullable(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
});
+1
View File
@@ -19,6 +19,7 @@ export const ZContactAttributeKey = z.object({
type: ZContactAttributeKeyType,
dataType: ZContactAttributeDataType.prefault("string"),
environmentId: z.string(),
workspaceId: z.string().nullable(),
});
export type TContactAttributeKey = z.infer<typeof ZContactAttributeKey>;
+1
View File
@@ -19,6 +19,7 @@ export type TIntegrationConfig = z.infer<typeof ZIntegrationConfig>;
export const ZIntegrationBase = z.object({
id: z.string(),
environmentId: z.string(),
workspaceId: z.string().nullable(),
});
export const ZIntegration = ZIntegrationBase.extend({
@@ -3,6 +3,7 @@ import { z } from "zod";
export const ZIntegrationBase = z.object({
id: z.string(),
environmentId: z.string(),
workspaceId: z.string().nullable(),
});
export const ZIntegrationBaseSurveyData = z.object({
+1
View File
@@ -344,6 +344,7 @@ export const ZSegment = z.object({
isPrivate: z.boolean().prefault(true),
filters: ZSegmentFilters,
environmentId: z.string(),
workspaceId: z.string().nullable(),
createdAt: z.date(),
updatedAt: z.date(),
surveys: z.array(z.string()),
+1
View File
@@ -826,6 +826,7 @@ export const ZSurveyBase = z.object({
name: z.string(),
type: ZSurveyType,
environmentId: z.string(),
workspaceId: z.string().nullable(),
createdBy: z.string().nullable(),
status: ZSurveyStatus,
displayOption: ZSurveyDisplayOption,
+1
View File
@@ -6,6 +6,7 @@ export const ZTag = z.object({
updatedAt: z.date(),
name: z.string(),
environmentId: z.string(),
workspaceId: z.string().nullable(),
});
export type TTag = z.infer<typeof ZTag>;