mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-01 21:22:38 -05:00
Compare commits
3 Commits
chore/clie
...
revert/rem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5239dfd9b1 | ||
|
|
d45cbefcff | ||
|
|
f1c6180ae2 |
@@ -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);
|
||||
|
||||
@@ -5,7 +5,6 @@ import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { THandlerParams, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { resolveClientApiIds } from "@/lib/utils/resolve-client-id";
|
||||
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { createDisplay } from "./lib/display";
|
||||
|
||||
@@ -22,20 +21,10 @@ export const OPTIONS = async (): Promise<Response> => {
|
||||
export const POST = withV1ApiWrapper({
|
||||
handler: async ({ req, props }: THandlerParams<{ params: Promise<{ environmentId: string }> }>) => {
|
||||
const params = await props.params;
|
||||
|
||||
// Resolve: accepts either an environmentId (old SDK) or a workspaceId (new SDK)
|
||||
const resolved = await resolveClientApiIds(params.environmentId);
|
||||
if (!resolved) {
|
||||
return {
|
||||
response: responses.notFoundResponse("Environment", params.environmentId),
|
||||
};
|
||||
}
|
||||
const { environmentId } = resolved;
|
||||
|
||||
const jsonInput = await req.json();
|
||||
const inputValidation = ZDisplayCreateInput.safeParse({
|
||||
...jsonInput,
|
||||
environmentId,
|
||||
environmentId: params.environmentId,
|
||||
});
|
||||
|
||||
if (!inputValidation.success) {
|
||||
@@ -49,7 +38,7 @@ export const POST = withV1ApiWrapper({
|
||||
}
|
||||
|
||||
if (inputValidation.data.userId) {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(environmentId);
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(params.environmentId);
|
||||
const isContactsEnabled = await getIsContactsEnabled(organizationId);
|
||||
if (!isContactsEnabled) {
|
||||
return {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { getEnvironmentState } from "@/app/api/v1/client/[environmentId]/environment/lib/environmentState";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { THandlerParams, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
import { resolveClientApiIds } from "@/lib/utils/resolve-client-id";
|
||||
|
||||
export const OPTIONS = async (): Promise<Response> => {
|
||||
return responses.successResponse(
|
||||
@@ -30,10 +29,15 @@ export const GET = withV1ApiWrapper({
|
||||
};
|
||||
}
|
||||
|
||||
const idParam = params.environmentId.trim();
|
||||
const environmentId = params.environmentId.trim();
|
||||
|
||||
// Validate CUID format
|
||||
const cuidValidation = ZEnvironmentId.safeParse(idParam);
|
||||
// Validate CUID v1 format using Zod (matches Prisma schema @default(cuid()))
|
||||
// This catches all invalid formats including:
|
||||
// - null/undefined passed as string "null" or "undefined"
|
||||
// - HTML-encoded placeholders like <environmentId> or %3C...%3E
|
||||
// - Empty or whitespace-only IDs
|
||||
// - Any other invalid CUID v1 format
|
||||
const cuidValidation = ZEnvironmentId.safeParse(environmentId);
|
||||
if (!cuidValidation.success) {
|
||||
logger.warn(
|
||||
{
|
||||
@@ -41,23 +45,13 @@ export const GET = withV1ApiWrapper({
|
||||
url: req.url,
|
||||
validationError: cuidValidation.error.issues[0]?.message,
|
||||
},
|
||||
"Invalid CUID format detected"
|
||||
"Invalid CUID v1 format detected"
|
||||
);
|
||||
return {
|
||||
response: responses.badRequestResponse("Invalid environment ID format", undefined, true),
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve: accepts either an environmentId (old SDK) or a workspaceId (new SDK)
|
||||
const resolved = await resolveClientApiIds(idParam);
|
||||
if (!resolved) {
|
||||
return {
|
||||
response: responses.notFoundResponse("Environment", idParam),
|
||||
};
|
||||
}
|
||||
|
||||
const { environmentId } = resolved;
|
||||
|
||||
// Use optimized environment state fetcher with new caching approach
|
||||
const environmentState = await getEnvironmentState(environmentId);
|
||||
const { data } = environmentState;
|
||||
|
||||
@@ -45,6 +45,7 @@ export const responseSelection = {
|
||||
updatedAt: true,
|
||||
name: true,
|
||||
environmentId: true,
|
||||
workspaceId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { headers } from "next/headers";
|
||||
import { UAParser } from "ua-parser-js";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { ZEnvironmentId } from "@formbricks/types/environment";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { TResponseWithQuotaFull } from "@formbricks/types/quota";
|
||||
import { TResponseInput, ZResponseInput } from "@formbricks/types/responses";
|
||||
@@ -12,7 +13,6 @@ import { sendToPipeline } from "@/app/lib/pipelines";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { getClientIpFromHeaders } from "@/lib/utils/client-ip";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { resolveClientApiIds } from "@/lib/utils/resolve-client-id";
|
||||
import { formatValidationErrorsForV1Api, validateResponseData } from "@/modules/api/lib/validation";
|
||||
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { createQuotaFullObject } from "@/modules/ee/quotas/lib/helpers";
|
||||
@@ -66,16 +66,19 @@ export const POST = withV1ApiWrapper({
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve: accepts either an environmentId (old SDK) or a workspaceId (new SDK)
|
||||
const resolved = await resolveClientApiIds(params.environmentId);
|
||||
if (!resolved) {
|
||||
const { environmentId } = params;
|
||||
const environmentIdValidation = ZEnvironmentId.safeParse(environmentId);
|
||||
const responseInputValidation = ZResponseInput.safeParse({ ...responseInput, environmentId });
|
||||
|
||||
if (!environmentIdValidation.success) {
|
||||
return {
|
||||
response: responses.notFoundResponse("Environment", params.environmentId),
|
||||
response: responses.badRequestResponse(
|
||||
"Fields are missing or incorrectly formatted",
|
||||
transformErrorToDetails(environmentIdValidation.error),
|
||||
true
|
||||
),
|
||||
};
|
||||
}
|
||||
const { environmentId } = resolved;
|
||||
|
||||
const responseInputValidation = ZResponseInput.safeParse({ ...responseInput, environmentId });
|
||||
|
||||
if (!responseInputValidation.success) {
|
||||
return {
|
||||
|
||||
@@ -6,7 +6,6 @@ import { THandlerParams, withV1ApiWrapper } from "@/app/lib/api/with-api-logging
|
||||
import { MAX_FILE_UPLOAD_SIZES } from "@/lib/constants";
|
||||
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { resolveClientApiIds } from "@/lib/utils/resolve-client-id";
|
||||
import { rateLimitConfigs } from "@/modules/core/rate-limit/rate-limit-configs";
|
||||
import { getBiggerUploadFileSizePermission } from "@/modules/ee/license-check/lib/utils";
|
||||
import { getSignedUrlForUpload } from "@/modules/storage/service";
|
||||
@@ -30,16 +29,7 @@ export const OPTIONS = async (): Promise<Response> => {
|
||||
export const POST = withV1ApiWrapper({
|
||||
handler: async ({ req, props }: THandlerParams<{ params: Promise<{ environmentId: string }> }>) => {
|
||||
const params = await props.params;
|
||||
|
||||
// Resolve: accepts either an environmentId (old SDK) or a workspaceId (new SDK)
|
||||
const resolved = await resolveClientApiIds(params.environmentId);
|
||||
if (!resolved) {
|
||||
return {
|
||||
response: responses.notFoundResponse("Environment", params.environmentId),
|
||||
};
|
||||
}
|
||||
const { environmentId } = resolved;
|
||||
|
||||
const { environmentId } = params;
|
||||
let jsonInput: TUploadPrivateFileRequest;
|
||||
|
||||
try {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -4,7 +4,6 @@ import { ZDisplayCreateInputV2 } from "@/app/api/v2/client/[environmentId]/displ
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { resolveClientApiIds } from "@/lib/utils/resolve-client-id";
|
||||
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { createDisplay } from "./lib/display";
|
||||
|
||||
@@ -26,18 +25,10 @@ export const OPTIONS = async (): Promise<Response> => {
|
||||
|
||||
export const POST = async (request: Request, context: Context): Promise<Response> => {
|
||||
const params = await context.params;
|
||||
|
||||
// Resolve: accepts either an environmentId (old SDK) or a workspaceId (new SDK)
|
||||
const resolved = await resolveClientApiIds(params.environmentId);
|
||||
if (!resolved) {
|
||||
return responses.notFoundResponse("Environment", params.environmentId);
|
||||
}
|
||||
const { environmentId } = resolved;
|
||||
|
||||
const jsonInput = await request.json();
|
||||
const inputValidation = ZDisplayCreateInputV2.safeParse({
|
||||
...jsonInput,
|
||||
environmentId,
|
||||
environmentId: params.environmentId,
|
||||
});
|
||||
|
||||
if (!inputValidation.success) {
|
||||
@@ -49,7 +40,7 @@ export const POST = async (request: Request, context: Context): Promise<Response
|
||||
}
|
||||
|
||||
if (inputValidation.data.contactId) {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(environmentId);
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(params.environmentId);
|
||||
const isContactsEnabled = await getIsContactsEnabled(organizationId);
|
||||
if (!isContactsEnabled) {
|
||||
return responses.forbiddenResponse("User identification is only available for enterprise users.", true);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { headers } from "next/headers";
|
||||
import { UAParser } from "ua-parser-js";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { ZEnvironmentId } from "@formbricks/types/environment";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { TResponseWithQuotaFull } from "@formbricks/types/quota";
|
||||
import { checkSurveyValidity } from "@/app/api/v2/client/[environmentId]/responses/lib/utils";
|
||||
@@ -11,7 +12,6 @@ import { getSurvey } from "@/lib/survey/service";
|
||||
import { getElementsFromBlocks } from "@/lib/survey/utils";
|
||||
import { getClientIpFromHeaders } from "@/lib/utils/client-ip";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { resolveClientApiIds } from "@/lib/utils/resolve-client-id";
|
||||
import { formatValidationErrorsForV1Api, validateResponseData } from "@/modules/api/lib/validation";
|
||||
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/element";
|
||||
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
@@ -49,15 +49,18 @@ export const POST = async (request: Request, context: Context): Promise<Response
|
||||
);
|
||||
}
|
||||
|
||||
// Resolve: accepts either an environmentId (old SDK) or a workspaceId (new SDK)
|
||||
const resolved = await resolveClientApiIds(params.environmentId);
|
||||
if (!resolved) {
|
||||
return responses.notFoundResponse("Environment", params.environmentId);
|
||||
}
|
||||
const { environmentId } = resolved;
|
||||
|
||||
const { environmentId } = params;
|
||||
const environmentIdValidation = ZEnvironmentId.safeParse(environmentId);
|
||||
const responseInputValidation = ZResponseInputV2.safeParse({ ...responseInput, environmentId });
|
||||
|
||||
if (!environmentIdValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
"Fields are missing or incorrectly formatted",
|
||||
transformErrorToDetails(environmentIdValidation.error),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
if (!responseInputValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
"Fields are missing or incorrectly formatted",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -21,6 +21,7 @@ const selectActionClass = {
|
||||
key: true,
|
||||
noCodeConfig: true,
|
||||
environmentId: true,
|
||||
workspaceId: true,
|
||||
} satisfies Prisma.ActionClassSelect;
|
||||
|
||||
export const getActionClasses = reactCache(
|
||||
|
||||
@@ -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<{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { resolveClientApiIds } from "./resolve-client-id";
|
||||
|
||||
vi.mock("server-only", () => ({}));
|
||||
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
prisma: {
|
||||
environment: {
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
workspace: {
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe("resolveClientApiIds", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("resolves an environmentId to environmentId + workspaceId", async () => {
|
||||
vi.mocked(prisma.environment.findUnique).mockResolvedValue({
|
||||
id: "env-123",
|
||||
workspaceId: "ws-456",
|
||||
} as any);
|
||||
|
||||
const result = await resolveClientApiIds("env-123");
|
||||
|
||||
expect(result).toEqual({
|
||||
environmentId: "env-123",
|
||||
workspaceId: "ws-456",
|
||||
});
|
||||
expect(prisma.environment.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: "env-123" },
|
||||
select: { id: true, workspaceId: true },
|
||||
});
|
||||
expect(prisma.workspace.findUnique).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resolves a workspaceId to workspaceId + production environmentId", async () => {
|
||||
vi.mocked(prisma.environment.findUnique).mockResolvedValue(null);
|
||||
vi.mocked(prisma.workspace.findUnique).mockResolvedValue({
|
||||
id: "ws-456",
|
||||
environments: [{ id: "env-prod-789" }],
|
||||
} as any);
|
||||
|
||||
const result = await resolveClientApiIds("ws-456");
|
||||
|
||||
expect(result).toEqual({
|
||||
environmentId: "env-prod-789",
|
||||
workspaceId: "ws-456",
|
||||
});
|
||||
expect(prisma.workspace.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: "ws-456" },
|
||||
select: {
|
||||
id: true,
|
||||
environments: {
|
||||
where: { type: "production" },
|
||||
select: { id: true },
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null when neither environment nor workspace is found", async () => {
|
||||
vi.mocked(prisma.environment.findUnique).mockResolvedValue(null);
|
||||
vi.mocked(prisma.workspace.findUnique).mockResolvedValue(null);
|
||||
|
||||
const result = await resolveClientApiIds("unknown-id");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when workspace exists but has no production environment", async () => {
|
||||
vi.mocked(prisma.environment.findUnique).mockResolvedValue(null);
|
||||
vi.mocked(prisma.workspace.findUnique).mockResolvedValue({
|
||||
id: "ws-456",
|
||||
environments: [],
|
||||
} as any);
|
||||
|
||||
const result = await resolveClientApiIds("ws-456");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
import "server-only";
|
||||
import { prisma } from "@formbricks/database";
|
||||
|
||||
export type TResolvedClientIds = {
|
||||
workspaceId: string;
|
||||
environmentId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves a URL parameter that may be an environmentId (old SDK) or workspaceId (new SDK).
|
||||
*
|
||||
* - If the id matches an Environment, returns the environment's id and its parent workspaceId.
|
||||
* - If not, checks the Workspace table and returns the workspace's production environment id.
|
||||
* - Returns null if neither lookup succeeds.
|
||||
*/
|
||||
export const resolveClientApiIds = async (id: string): Promise<TResolvedClientIds | null> => {
|
||||
// Try as environmentId first (existing SDKs)
|
||||
const environment = await prisma.environment.findUnique({
|
||||
where: { id },
|
||||
select: { id: true, workspaceId: true },
|
||||
});
|
||||
|
||||
if (environment) {
|
||||
return { workspaceId: environment.workspaceId, environmentId: environment.id };
|
||||
}
|
||||
|
||||
// Try as workspaceId (new SDKs sending workspaceId)
|
||||
const workspace = await prisma.workspace.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
id: true,
|
||||
environments: {
|
||||
where: { type: "production" },
|
||||
select: { id: true },
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (workspace && workspace.environments[0]) {
|
||||
return { workspaceId: workspace.id, environmentId: workspace.environments[0].id };
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -7,7 +7,6 @@ import { TJsPersonState } from "@formbricks/types/js";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { THandlerParams, withV1ApiWrapper } from "@/app/lib/api/with-api-logging";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { resolveClientApiIds } from "@/lib/utils/resolve-client-id";
|
||||
import { getIsContactsEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { updateUser } from "./lib/update-user";
|
||||
|
||||
@@ -46,10 +45,15 @@ export const POST = withV1ApiWrapper({
|
||||
};
|
||||
}
|
||||
|
||||
const idParam = params.environmentId.trim();
|
||||
const environmentId = params.environmentId.trim();
|
||||
|
||||
// Validate CUID format
|
||||
const cuidValidation = ZEnvironmentId.safeParse(idParam);
|
||||
// Validate CUID v1 format using Zod (matches Prisma schema @default(cuid()))
|
||||
// This catches all invalid formats including:
|
||||
// - null/undefined passed as string "null" or "undefined"
|
||||
// - HTML-encoded placeholders like <environmentId> or %3C...%3E
|
||||
// - Empty or whitespace-only IDs
|
||||
// - Any other invalid CUID v1 format
|
||||
const cuidValidation = ZEnvironmentId.safeParse(environmentId);
|
||||
if (!cuidValidation.success) {
|
||||
logger.warn(
|
||||
{
|
||||
@@ -57,22 +61,13 @@ export const POST = withV1ApiWrapper({
|
||||
url: req.url,
|
||||
validationError: cuidValidation.error.issues[0]?.message,
|
||||
},
|
||||
"Invalid CUID format detected"
|
||||
"Invalid CUID v1 format detected"
|
||||
);
|
||||
return {
|
||||
response: responses.badRequestResponse("Invalid environment ID format", undefined, true),
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve: accepts either an environmentId (old SDK) or a workspaceId (new SDK)
|
||||
const resolved = await resolveClientApiIds(idParam);
|
||||
if (!resolved) {
|
||||
return {
|
||||
response: responses.notFoundResponse("Environment", idParam),
|
||||
};
|
||||
}
|
||||
const { environmentId } = resolved;
|
||||
|
||||
const jsonInput = await req.json();
|
||||
|
||||
// Basic input validation without Zod overhead
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user