mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-19 02:10:33 -05:00
chore: client API backwards compat — accept workspaceId or environmentId (#7609)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
committed by
GitHub
parent
c544bb0b22
commit
01ee015086
@@ -15,7 +15,7 @@ import { getOrganization } from "@/lib/organization/service";
|
||||
import { getResponseCountBySurveyId } from "@/lib/response/service";
|
||||
import { getSurvey, updateSurvey } from "@/lib/survey/service";
|
||||
import { convertDatesInObject } from "@/lib/time";
|
||||
import { getOrganizationIdFromWorkspaceId, getWorkspaceIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { getOrganizationIdFromWorkspaceId } from "@/lib/utils/helper";
|
||||
import { validateWebhookUrl } from "@/lib/utils/validate-webhook-url";
|
||||
import { queueAuditEvent } from "@/modules/ee/audit-logs/lib/handler";
|
||||
import { TAuditStatus, UNKNOWN_DATA } from "@/modules/ee/audit-logs/types/audit-log";
|
||||
@@ -53,9 +53,8 @@ export const POST = async (request: Request) => {
|
||||
);
|
||||
}
|
||||
|
||||
const { environmentId, surveyId, event, response } = inputValidation.data;
|
||||
const { environmentId, workspaceId, surveyId, event, response } = inputValidation.data;
|
||||
|
||||
const workspaceId = await getWorkspaceIdFromEnvironmentId(environmentId);
|
||||
const organizationId = await getOrganizationIdFromWorkspaceId(workspaceId);
|
||||
const organization = await getOrganization(organizationId);
|
||||
if (!organization) {
|
||||
@@ -70,19 +69,19 @@ export const POST = async (request: Request) => {
|
||||
return responses.notFoundResponse("Survey", surveyId, true);
|
||||
}
|
||||
|
||||
if (survey.environmentId !== environmentId) {
|
||||
if (survey.workspaceId !== workspaceId) {
|
||||
logger.error(
|
||||
{ url: request.url, surveyId, environmentId, surveyEnvironmentId: survey.environmentId },
|
||||
`Survey ${surveyId} does not belong to environment ${environmentId}`
|
||||
{ url: request.url, surveyId, workspaceId, surveyWorkspaceId: survey.workspaceId },
|
||||
`Survey ${surveyId} does not belong to workspace ${workspaceId}`
|
||||
);
|
||||
return responses.badRequestResponse("Survey not found in this environment");
|
||||
return responses.badRequestResponse("Survey not found in this workspace");
|
||||
}
|
||||
|
||||
// Fetch webhooks
|
||||
const getWebhooksForPipeline = async (environmentId: string, event: PipelineTriggers, surveyId: string) => {
|
||||
const getWebhooksForPipeline = async (workspaceId: string, event: PipelineTriggers, surveyId: string) => {
|
||||
const webhooks = await prisma.webhook.findMany({
|
||||
where: {
|
||||
environmentId,
|
||||
workspaceId,
|
||||
triggers: { has: event },
|
||||
OR: [{ surveyIds: { has: surveyId } }, { surveyIds: { isEmpty: true } }],
|
||||
},
|
||||
@@ -90,7 +89,7 @@ export const POST = async (request: Request) => {
|
||||
return webhooks;
|
||||
};
|
||||
|
||||
const webhooks: Webhook[] = await getWebhooksForPipeline(environmentId, event, surveyId);
|
||||
const webhooks: Webhook[] = await getWebhooksForPipeline(workspaceId, event, surveyId);
|
||||
// Prepare webhook and email promises
|
||||
|
||||
// Fetch with timeout of 5 seconds to prevent hanging
|
||||
@@ -165,7 +164,6 @@ export const POST = async (request: Request) => {
|
||||
}
|
||||
|
||||
// Fetch users with notifications in a single query
|
||||
// TODO: add cache for this query. Not possible at the moment since we can't get the membership cache by environmentId
|
||||
const usersWithNotifications = await prisma.user.findMany({
|
||||
where: {
|
||||
memberships: {
|
||||
@@ -173,9 +171,7 @@ export const POST = async (request: Request) => {
|
||||
organization: {
|
||||
workspaces: {
|
||||
some: {
|
||||
environments: {
|
||||
some: { id: environmentId },
|
||||
},
|
||||
id: workspaceId,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -198,11 +194,7 @@ export const POST = async (request: Request) => {
|
||||
workspaceTeams: {
|
||||
some: {
|
||||
workspace: {
|
||||
environments: {
|
||||
some: {
|
||||
id: environmentId,
|
||||
},
|
||||
},
|
||||
id: workspaceId,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ export const ZPipelineInput = z.object({
|
||||
event: ZWebhook.shape.triggers.element,
|
||||
response: ZResponse,
|
||||
environmentId: z.string(),
|
||||
workspaceId: z.string(),
|
||||
surveyId: z.string(),
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { prisma } from "@formbricks/database";
|
||||
|
||||
export const getContactByUserId = reactCache(
|
||||
async (
|
||||
environmentId: string,
|
||||
workspaceId: string,
|
||||
userId: string
|
||||
): Promise<{
|
||||
id: string;
|
||||
@@ -14,7 +14,7 @@ export const getContactByUserId = reactCache(
|
||||
some: {
|
||||
attributeKey: {
|
||||
key: "userId",
|
||||
environmentId,
|
||||
workspaceId,
|
||||
},
|
||||
value: userId,
|
||||
},
|
||||
|
||||
@@ -3,15 +3,10 @@ import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { TDisplayCreateInput } from "@formbricks/types/displays";
|
||||
import { DatabaseError, ResourceNotFoundError, ValidationError } from "@formbricks/types/errors";
|
||||
import { getWorkspaceIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
import { getContactByUserId } from "./contact";
|
||||
import { createDisplay } from "./display";
|
||||
|
||||
vi.mock("@/lib/utils/helper", () => ({
|
||||
getWorkspaceIdFromEnvironmentId: vi.fn().mockResolvedValue("workspace-id-mock"),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils/validate", () => ({
|
||||
validateInputs: vi.fn((inputs: [unknown, unknown][]) =>
|
||||
inputs.map((input: [unknown, unknown]) => input[0])
|
||||
@@ -36,26 +31,29 @@ vi.mock("./contact", () => ({
|
||||
getContactByUserId: vi.fn(),
|
||||
}));
|
||||
|
||||
const environmentId = "test-env-id";
|
||||
const surveyId = "test-survey-id";
|
||||
const environmentId = "scgavd0rtce5xgahiresk4p0";
|
||||
const workspaceId = "cqiu9au22kgzgqjossdlp5qh";
|
||||
const surveyId = "kf4w7x11wut39ttl4j5v9ccg";
|
||||
const userId = "test-user-id";
|
||||
const contactId = "test-contact-id";
|
||||
const displayId = "test-display-id";
|
||||
const contactId = "f9bufd72cffj19a7qj67z5fm";
|
||||
const displayId = "apbycx5war0mfsyztgpwb8wr";
|
||||
|
||||
const displayInput: TDisplayCreateInput = {
|
||||
environmentId,
|
||||
workspaceId,
|
||||
surveyId,
|
||||
userId,
|
||||
};
|
||||
|
||||
const displayInputWithoutUserId: TDisplayCreateInput = {
|
||||
environmentId,
|
||||
workspaceId,
|
||||
surveyId,
|
||||
};
|
||||
|
||||
const mockContact = {
|
||||
id: contactId,
|
||||
environmentId,
|
||||
workspaceId,
|
||||
userId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
@@ -88,7 +86,9 @@ const mockSurvey = {
|
||||
describe("createDisplay", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(getWorkspaceIdFromEnvironmentId).mockResolvedValue("workspace-id-mock");
|
||||
vi.mocked(validateInputs).mockImplementation((inputs: [unknown, unknown][]) =>
|
||||
inputs.map((input: [unknown, unknown]) => input[0])
|
||||
);
|
||||
vi.mocked(prisma.survey.findUnique).mockResolvedValue(mockSurvey);
|
||||
});
|
||||
|
||||
@@ -99,7 +99,7 @@ describe("createDisplay", () => {
|
||||
const result = await createDisplay(displayInput);
|
||||
|
||||
expect(validateInputs).toHaveBeenCalledWith([displayInput, expect.any(Object)]);
|
||||
expect(getContactByUserId).toHaveBeenCalledWith(environmentId, userId);
|
||||
expect(getContactByUserId).toHaveBeenCalledWith(workspaceId, userId);
|
||||
expect(prisma.contact.create).not.toHaveBeenCalled();
|
||||
expect(prisma.display.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
@@ -119,11 +119,11 @@ describe("createDisplay", () => {
|
||||
const result = await createDisplay(displayInput);
|
||||
|
||||
expect(validateInputs).toHaveBeenCalledWith([displayInput, expect.any(Object)]);
|
||||
expect(getContactByUserId).toHaveBeenCalledWith(environmentId, userId);
|
||||
expect(getContactByUserId).toHaveBeenCalledWith(workspaceId, userId);
|
||||
expect(prisma.contact.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
environment: { connect: { id: environmentId } },
|
||||
workspace: { connect: { id: "workspace-id-mock" } },
|
||||
workspace: { connect: { id: workspaceId } },
|
||||
attributes: {
|
||||
create: {
|
||||
attributeKey: {
|
||||
@@ -177,9 +177,9 @@ describe("createDisplay", () => {
|
||||
vi.mocked(prisma.survey.findUnique).mockResolvedValue(null);
|
||||
|
||||
await expect(createDisplay(displayInput)).rejects.toThrow(new ResourceNotFoundError("Survey", surveyId));
|
||||
expect(getContactByUserId).toHaveBeenCalledWith(environmentId, userId);
|
||||
expect(getContactByUserId).toHaveBeenCalledWith(workspaceId, userId);
|
||||
expect(prisma.survey.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: surveyId, environmentId },
|
||||
where: { id: surveyId, workspaceId },
|
||||
});
|
||||
expect(prisma.display.create).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -193,7 +193,7 @@ describe("createDisplay", () => {
|
||||
vi.mocked(prisma.display.create).mockRejectedValue(prismaError);
|
||||
|
||||
await expect(createDisplay(displayInput)).rejects.toThrow(DatabaseError);
|
||||
expect(getContactByUserId).toHaveBeenCalledWith(environmentId, userId);
|
||||
expect(getContactByUserId).toHaveBeenCalledWith(workspaceId, userId);
|
||||
expect(prisma.display.create).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -203,7 +203,7 @@ describe("createDisplay", () => {
|
||||
vi.mocked(prisma.display.create).mockRejectedValue(genericError);
|
||||
|
||||
await expect(createDisplay(displayInput)).rejects.toThrow(genericError);
|
||||
expect(getContactByUserId).toHaveBeenCalledWith(environmentId, userId);
|
||||
expect(getContactByUserId).toHaveBeenCalledWith(workspaceId, userId);
|
||||
expect(prisma.display.create).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -212,7 +212,7 @@ describe("createDisplay", () => {
|
||||
vi.mocked(getContactByUserId).mockRejectedValue(contactError);
|
||||
|
||||
await expect(createDisplay(displayInput)).rejects.toThrow(contactError);
|
||||
expect(getContactByUserId).toHaveBeenCalledWith(environmentId, userId);
|
||||
expect(getContactByUserId).toHaveBeenCalledWith(workspaceId, userId);
|
||||
expect(prisma.display.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -222,7 +222,7 @@ describe("createDisplay", () => {
|
||||
vi.mocked(prisma.contact.create).mockRejectedValue(contactCreateError);
|
||||
|
||||
await expect(createDisplay(displayInput)).rejects.toThrow(contactCreateError);
|
||||
expect(getContactByUserId).toHaveBeenCalledWith(environmentId, userId);
|
||||
expect(getContactByUserId).toHaveBeenCalledWith(workspaceId, userId);
|
||||
expect(prisma.contact.create).toHaveBeenCalled();
|
||||
expect(prisma.display.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -2,21 +2,19 @@ import { Prisma } from "@prisma/client";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { TDisplayCreateInput, ZDisplayCreateInput } from "@formbricks/types/displays";
|
||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { getWorkspaceIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
import { getContactByUserId } from "./contact";
|
||||
|
||||
export const createDisplay = async (displayInput: TDisplayCreateInput): Promise<{ id: string }> => {
|
||||
validateInputs([displayInput, ZDisplayCreateInput]);
|
||||
|
||||
const { environmentId, userId, surveyId } = displayInput;
|
||||
const { environmentId, workspaceId, userId, surveyId } = displayInput;
|
||||
|
||||
try {
|
||||
let contact: { id: string } | null = null;
|
||||
if (userId) {
|
||||
contact = await getContactByUserId(environmentId, userId);
|
||||
contact = await getContactByUserId(workspaceId, userId);
|
||||
if (!contact) {
|
||||
const workspaceId = await getWorkspaceIdFromEnvironmentId(environmentId);
|
||||
contact = await prisma.contact.create({
|
||||
data: {
|
||||
environment: { connect: { id: environmentId } },
|
||||
@@ -37,7 +35,7 @@ export const createDisplay = async (displayInput: TDisplayCreateInput): Promise<
|
||||
const survey = await prisma.survey.findUnique({
|
||||
where: {
|
||||
id: surveyId,
|
||||
environmentId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
if (!survey) {
|
||||
|
||||
@@ -4,7 +4,8 @@ import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
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 { getOrganizationIdFromWorkspaceId } 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";
|
||||
|
||||
@@ -21,10 +22,21 @@ 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, workspaceId } = resolved;
|
||||
|
||||
const jsonInput = await req.json();
|
||||
const inputValidation = ZDisplayCreateInput.safeParse({
|
||||
...jsonInput,
|
||||
environmentId: params.environmentId,
|
||||
environmentId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (!inputValidation.success) {
|
||||
@@ -38,7 +50,7 @@ export const POST = withV1ApiWrapper({
|
||||
}
|
||||
|
||||
if (inputValidation.data.userId) {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(params.environmentId);
|
||||
const organizationId = await getOrganizationIdFromWorkspaceId(workspaceId);
|
||||
const isContactsEnabled = await getIsContactsEnabled(organizationId);
|
||||
if (!isContactsEnabled) {
|
||||
return {
|
||||
|
||||
@@ -4,6 +4,7 @@ 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(
|
||||
@@ -29,15 +30,10 @@ export const GET = withV1ApiWrapper({
|
||||
};
|
||||
}
|
||||
|
||||
const environmentId = params.environmentId.trim();
|
||||
const idParam = params.environmentId.trim();
|
||||
|
||||
// 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);
|
||||
// Validate CUID format
|
||||
const cuidValidation = ZEnvironmentId.safeParse(idParam);
|
||||
if (!cuidValidation.success) {
|
||||
logger.warn(
|
||||
{
|
||||
@@ -45,13 +41,23 @@ export const GET = withV1ApiWrapper({
|
||||
url: req.url,
|
||||
validationError: cuidValidation.error.issues[0]?.message,
|
||||
},
|
||||
"Invalid CUID v1 format detected"
|
||||
"Invalid CUID 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;
|
||||
|
||||
@@ -206,6 +206,7 @@ export const PUT = withV1ApiWrapper({
|
||||
sendToPipeline({
|
||||
event: "responseUpdated",
|
||||
environmentId: survey.environmentId,
|
||||
workspaceId: survey.workspaceId,
|
||||
surveyId: survey.id,
|
||||
response: responseData,
|
||||
});
|
||||
@@ -216,6 +217,7 @@ export const PUT = withV1ApiWrapper({
|
||||
sendToPipeline({
|
||||
event: "responseFinished",
|
||||
environmentId: survey.environmentId,
|
||||
workspaceId: survey.workspaceId,
|
||||
surveyId: survey.id,
|
||||
response: responseData,
|
||||
});
|
||||
|
||||
@@ -102,7 +102,7 @@ describe("Contact API Lib", () => {
|
||||
some: {
|
||||
attributeKey: {
|
||||
key: "userId",
|
||||
environmentId: mockEnvironmentId,
|
||||
workspaceId: mockEnvironmentId,
|
||||
},
|
||||
value: mockUserId,
|
||||
},
|
||||
@@ -138,7 +138,7 @@ describe("Contact API Lib", () => {
|
||||
some: {
|
||||
attributeKey: {
|
||||
key: "userId",
|
||||
environmentId: mockEnvironmentId,
|
||||
workspaceId: mockEnvironmentId,
|
||||
},
|
||||
value: mockUserId,
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@ export const getContact = reactCache(async (contactId: string) => {
|
||||
|
||||
export const getContactByUserId = reactCache(
|
||||
async (
|
||||
environmentId: string,
|
||||
workspaceId: string,
|
||||
userId: string
|
||||
): Promise<{
|
||||
id: string;
|
||||
@@ -33,7 +33,7 @@ export const getContactByUserId = reactCache(
|
||||
some: {
|
||||
attributeKey: {
|
||||
key: "userId",
|
||||
environmentId,
|
||||
workspaceId,
|
||||
},
|
||||
value: userId,
|
||||
},
|
||||
|
||||
@@ -4,8 +4,9 @@ import { prisma } from "@formbricks/database";
|
||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { TSurveyQuota } from "@formbricks/types/quota";
|
||||
import { TResponseInput } from "@formbricks/types/responses";
|
||||
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||
import { getOrganization } from "@/lib/organization/service";
|
||||
import { calculateTtcTotal } from "@/lib/response/utils";
|
||||
import { getOrganizationIdFromWorkspaceId } from "@/lib/utils/helper";
|
||||
import { evaluateResponseQuotas } from "@/modules/ee/quotas/lib/evaluation-service";
|
||||
import { createResponse, createResponseWithQuotaEvaluation } from "./response";
|
||||
|
||||
@@ -19,7 +20,11 @@ vi.mock("@/lib/constants", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/organization/service", () => ({
|
||||
getOrganizationByEnvironmentId: vi.fn(),
|
||||
getOrganization: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils/helper", () => ({
|
||||
getOrganizationIdFromWorkspaceId: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/response/utils", () => ({
|
||||
@@ -54,6 +59,7 @@ vi.mock("@/modules/ee/quotas/lib/evaluation-service", () => ({
|
||||
}));
|
||||
|
||||
const environmentId = "test-environment-id";
|
||||
const workspaceId = "test-workspace-id";
|
||||
const surveyId = "test-survey-id";
|
||||
const organizationId = "test-organization-id";
|
||||
const responseId = "test-response-id";
|
||||
@@ -68,6 +74,7 @@ const mockOrganization = {
|
||||
|
||||
const mockResponseInput: TResponseInput = {
|
||||
environmentId,
|
||||
workspaceId,
|
||||
surveyId,
|
||||
userId: null,
|
||||
finished: false,
|
||||
@@ -103,7 +110,8 @@ let mockTx: MockTx;
|
||||
describe("createResponse", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrganization as any);
|
||||
vi.mocked(getOrganizationIdFromWorkspaceId).mockResolvedValue(organizationId);
|
||||
vi.mocked(getOrganization).mockResolvedValue(mockOrganization as any);
|
||||
vi.mocked(prisma.response.create).mockResolvedValue(mockResponsePrisma as any);
|
||||
vi.mocked(calculateTtcTotal).mockImplementation((ttc) => ttc);
|
||||
});
|
||||
@@ -124,7 +132,7 @@ describe("createResponse", () => {
|
||||
});
|
||||
|
||||
test("should throw ResourceNotFoundError if organization not found", async () => {
|
||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(null);
|
||||
vi.mocked(getOrganization).mockResolvedValue(null);
|
||||
await expect(createResponse(mockResponseInput, prisma)).rejects.toThrow(ResourceNotFoundError);
|
||||
});
|
||||
|
||||
@@ -153,7 +161,8 @@ describe("createResponseWithQuotaEvaluation", () => {
|
||||
},
|
||||
};
|
||||
prisma.$transaction = vi.fn(async (cb: any) => cb(mockTx));
|
||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrganization as any);
|
||||
vi.mocked(getOrganizationIdFromWorkspaceId).mockResolvedValue(organizationId);
|
||||
vi.mocked(getOrganization).mockResolvedValue(mockOrganization as any);
|
||||
vi.mocked(mockTx.response.create).mockResolvedValue(mockResponsePrisma as any);
|
||||
vi.mocked(calculateTtcTotal).mockImplementation((ttc) => ttc);
|
||||
});
|
||||
|
||||
@@ -7,8 +7,9 @@ import { TResponseWithQuotaFull } from "@formbricks/types/quota";
|
||||
import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/responses";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { buildPrismaResponseData } from "@/app/api/v1/lib/utils";
|
||||
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||
import { getOrganization } from "@/lib/organization/service";
|
||||
import { calculateTtcTotal } from "@/lib/response/utils";
|
||||
import { getOrganizationIdFromWorkspaceId } from "@/lib/utils/helper";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
import { evaluateResponseQuotas } from "@/modules/ee/quotas/lib/evaluation-service";
|
||||
import { getContactByUserId } from "./contact";
|
||||
@@ -83,18 +84,19 @@ export const createResponse = async (
|
||||
): Promise<TResponse> => {
|
||||
validateInputs([responseInput, ZResponseInput]);
|
||||
|
||||
const { environmentId, userId, finished, ttc: initialTtc } = responseInput;
|
||||
const { workspaceId, userId, finished, ttc: initialTtc } = responseInput;
|
||||
|
||||
try {
|
||||
let contact: { id: string; attributes: TContactAttributes } | null = null;
|
||||
|
||||
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||
const organizationId = await getOrganizationIdFromWorkspaceId(workspaceId);
|
||||
const organization = await getOrganization(organizationId);
|
||||
if (!organization) {
|
||||
throw new ResourceNotFoundError("Organization", environmentId);
|
||||
throw new ResourceNotFoundError("Organization", organizationId);
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
contact = await getContactByUserId(environmentId, userId);
|
||||
contact = await getContactByUserId(workspaceId, userId);
|
||||
}
|
||||
|
||||
const ttc = initialTtc ? (finished ? calculateTtcTotal(initialTtc) : initialTtc) : {};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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 +11,8 @@ import { THandlerParams, withV1ApiWrapper } from "@/app/lib/api/with-api-logging
|
||||
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 { getOrganizationIdFromWorkspaceId } 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,19 +66,20 @@ export const POST = withV1ApiWrapper({
|
||||
};
|
||||
}
|
||||
|
||||
const { environmentId } = params;
|
||||
const environmentIdValidation = ZEnvironmentId.safeParse(environmentId);
|
||||
const responseInputValidation = ZResponseInput.safeParse({ ...responseInput, environmentId });
|
||||
|
||||
if (!environmentIdValidation.success) {
|
||||
// Resolve: accepts either an environmentId (old SDK) or a workspaceId (new SDK)
|
||||
const resolved = await resolveClientApiIds(params.environmentId);
|
||||
if (!resolved) {
|
||||
return {
|
||||
response: responses.badRequestResponse(
|
||||
"Fields are missing or incorrectly formatted",
|
||||
transformErrorToDetails(environmentIdValidation.error),
|
||||
true
|
||||
),
|
||||
response: responses.notFoundResponse("Environment", params.environmentId),
|
||||
};
|
||||
}
|
||||
const { environmentId, workspaceId } = resolved;
|
||||
|
||||
const responseInputValidation = ZResponseInput.safeParse({
|
||||
...responseInput,
|
||||
environmentId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (!responseInputValidation.success) {
|
||||
return {
|
||||
@@ -102,7 +103,7 @@ export const POST = withV1ApiWrapper({
|
||||
const responseInputData = responseInputValidation.data;
|
||||
|
||||
if (responseInputData.userId) {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(environmentId);
|
||||
const organizationId = await getOrganizationIdFromWorkspaceId(workspaceId);
|
||||
const isContactsEnabled = await getIsContactsEnabled(organizationId);
|
||||
if (!isContactsEnabled) {
|
||||
return {
|
||||
@@ -121,13 +122,13 @@ export const POST = withV1ApiWrapper({
|
||||
response: responses.notFoundResponse("Survey", responseInputData.surveyId, true),
|
||||
};
|
||||
}
|
||||
if (survey.environmentId !== environmentId) {
|
||||
if (survey.workspaceId !== workspaceId) {
|
||||
return {
|
||||
response: responses.badRequestResponse(
|
||||
"Survey is part of another environment",
|
||||
"Survey is part of another workspace",
|
||||
{
|
||||
"survey.environmentId": survey.environmentId,
|
||||
environmentId,
|
||||
"survey.workspaceId": survey.workspaceId,
|
||||
workspaceId,
|
||||
},
|
||||
true
|
||||
),
|
||||
@@ -190,6 +191,7 @@ export const POST = withV1ApiWrapper({
|
||||
sendToPipeline({
|
||||
event: "responseCreated",
|
||||
environmentId: survey.environmentId,
|
||||
workspaceId,
|
||||
surveyId: responseData.surveyId,
|
||||
response: responseData,
|
||||
});
|
||||
@@ -198,6 +200,7 @@ export const POST = withV1ApiWrapper({
|
||||
sendToPipeline({
|
||||
event: "responseFinished",
|
||||
environmentId: survey.environmentId,
|
||||
workspaceId,
|
||||
surveyId: responseData.surveyId,
|
||||
response: responseData,
|
||||
});
|
||||
|
||||
@@ -4,8 +4,10 @@ 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 { MAX_FILE_UPLOAD_SIZES } from "@/lib/constants";
|
||||
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||
import { getOrganization } from "@/lib/organization/service";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { getOrganizationIdFromWorkspaceId } from "@/lib/utils/helper";
|
||||
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";
|
||||
@@ -29,7 +31,16 @@ export const OPTIONS = async (): Promise<Response> => {
|
||||
export const POST = withV1ApiWrapper({
|
||||
handler: async ({ req, props }: THandlerParams<{ params: Promise<{ environmentId: string }> }>) => {
|
||||
const params = await props.params;
|
||||
const { environmentId } = 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, workspaceId } = resolved;
|
||||
|
||||
let jsonInput: TUploadPrivateFileRequest;
|
||||
|
||||
try {
|
||||
@@ -44,6 +55,7 @@ export const POST = withV1ApiWrapper({
|
||||
const parsedInputResult = ZUploadPrivateFileRequest.safeParse({
|
||||
...jsonInput,
|
||||
environmentId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (!parsedInputResult.success) {
|
||||
@@ -62,10 +74,11 @@ export const POST = withV1ApiWrapper({
|
||||
|
||||
const { fileName, fileType, surveyId } = parsedInputResult.data;
|
||||
|
||||
const [survey, organization] = await Promise.all([
|
||||
const [survey, organizationId] = await Promise.all([
|
||||
getSurvey(surveyId),
|
||||
getOrganizationByEnvironmentId(environmentId),
|
||||
getOrganizationIdFromWorkspaceId(workspaceId),
|
||||
]);
|
||||
const organization = await getOrganization(organizationId);
|
||||
|
||||
if (!survey) {
|
||||
return {
|
||||
@@ -79,11 +92,11 @@ export const POST = withV1ApiWrapper({
|
||||
};
|
||||
}
|
||||
|
||||
if (survey.environmentId !== environmentId) {
|
||||
if (survey.workspaceId !== workspaceId) {
|
||||
return {
|
||||
response: responses.badRequestResponse(
|
||||
"Survey does not belong to the environment",
|
||||
{ surveyId, environmentId },
|
||||
"Survey does not belong to the workspace",
|
||||
{ surveyId, workspaceId },
|
||||
true
|
||||
),
|
||||
};
|
||||
|
||||
@@ -172,6 +172,7 @@ export const PUT = withV1ApiWrapper({
|
||||
sendToPipeline({
|
||||
event: "responseUpdated",
|
||||
environmentId: result.survey.environmentId,
|
||||
workspaceId: result.survey.workspaceId,
|
||||
surveyId: result.survey.id,
|
||||
response: updated,
|
||||
});
|
||||
@@ -180,6 +181,7 @@ export const PUT = withV1ApiWrapper({
|
||||
sendToPipeline({
|
||||
event: "responseFinished",
|
||||
environmentId: result.survey.environmentId,
|
||||
workspaceId: result.survey.workspaceId,
|
||||
surveyId: result.survey.id,
|
||||
response: updated,
|
||||
});
|
||||
|
||||
@@ -100,7 +100,7 @@ export const createResponse = async (
|
||||
|
||||
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||
if (!organization) {
|
||||
throw new ResourceNotFoundError("Organization", environmentId);
|
||||
throw new ResourceNotFoundError("Organization", null);
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
|
||||
@@ -177,6 +177,7 @@ export const POST = withV1ApiWrapper({
|
||||
sendToPipeline({
|
||||
event: "responseCreated",
|
||||
environmentId: surveyResult.survey.environmentId,
|
||||
workspaceId: surveyResult.survey.workspaceId,
|
||||
surveyId: response.surveyId,
|
||||
response: response,
|
||||
});
|
||||
@@ -185,6 +186,7 @@ export const POST = withV1ApiWrapper({
|
||||
sendToPipeline({
|
||||
event: "responseFinished",
|
||||
environmentId: surveyResult.survey.environmentId,
|
||||
workspaceId: surveyResult.survey.workspaceId,
|
||||
surveyId: response.surveyId,
|
||||
response: response,
|
||||
});
|
||||
|
||||
@@ -29,18 +29,21 @@ vi.mock("./contact", () => ({
|
||||
}));
|
||||
|
||||
const environmentId = "test-env-id";
|
||||
const workspaceId = "workspace-id-mock";
|
||||
const surveyId = "test-survey-id";
|
||||
const contactId = "test-contact-id";
|
||||
const displayId = "test-display-id";
|
||||
|
||||
const displayInput: TDisplayCreateInputV2 = {
|
||||
environmentId,
|
||||
workspaceId,
|
||||
surveyId,
|
||||
contactId,
|
||||
};
|
||||
|
||||
const displayInputWithoutContact: TDisplayCreateInputV2 = {
|
||||
environmentId,
|
||||
workspaceId,
|
||||
surveyId,
|
||||
};
|
||||
|
||||
@@ -144,7 +147,7 @@ describe("createDisplay", () => {
|
||||
await expect(createDisplay(displayInput)).rejects.toThrow(new ResourceNotFoundError("Survey", surveyId));
|
||||
expect(doesContactExist).toHaveBeenCalledWith(contactId);
|
||||
expect(prisma.survey.findUnique).toHaveBeenCalledWith({
|
||||
where: { id: surveyId, environmentId },
|
||||
where: { id: surveyId, workspaceId },
|
||||
});
|
||||
expect(prisma.display.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import { doesContactExist } from "./contact";
|
||||
export const createDisplay = async (displayInput: TDisplayCreateInputV2): Promise<{ id: string }> => {
|
||||
validateInputs([displayInput, ZDisplayCreateInputV2]);
|
||||
|
||||
const { contactId, surveyId, environmentId } = displayInput;
|
||||
const { contactId, surveyId, workspaceId } = displayInput;
|
||||
|
||||
try {
|
||||
const contactExists = contactId ? await doesContactExist(contactId) : false;
|
||||
@@ -19,7 +19,7 @@ export const createDisplay = async (displayInput: TDisplayCreateInputV2): Promis
|
||||
const survey = await prisma.survey.findUnique({
|
||||
where: {
|
||||
id: surveyId,
|
||||
environmentId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
if (!survey) {
|
||||
|
||||
@@ -3,7 +3,8 @@ import { ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { ZDisplayCreateInputV2 } from "@/app/api/v2/client/[environmentId]/displays/types/display";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { getOrganizationIdFromWorkspaceId } 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";
|
||||
|
||||
@@ -25,10 +26,19 @@ 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, workspaceId } = resolved;
|
||||
|
||||
const jsonInput = await request.json();
|
||||
const inputValidation = ZDisplayCreateInputV2.safeParse({
|
||||
...jsonInput,
|
||||
environmentId: params.environmentId,
|
||||
environmentId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (!inputValidation.success) {
|
||||
@@ -40,7 +50,7 @@ export const POST = async (request: Request, context: Context): Promise<Response
|
||||
}
|
||||
|
||||
if (inputValidation.data.contactId) {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(params.environmentId);
|
||||
const organizationId = await getOrganizationIdFromWorkspaceId(workspaceId);
|
||||
const isContactsEnabled = await getIsContactsEnabled(organizationId);
|
||||
if (!isContactsEnabled) {
|
||||
return responses.forbiddenResponse("User identification is only available for enterprise users.", true);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { TOrganizationBilling } from "@formbricks/types/organizations";
|
||||
import { getOrganizationBillingByEnvironmentId } from "./organization";
|
||||
import { getOrganizationBillingByWorkspaceId } from "./organization";
|
||||
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
prisma: {
|
||||
@@ -17,8 +17,8 @@ vi.mock("@formbricks/logger", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
describe("getOrganizationBillingByEnvironmentId", () => {
|
||||
const environmentId = "env-123";
|
||||
describe("getOrganizationBillingByWorkspaceId", () => {
|
||||
const workspaceId = "ws-123";
|
||||
const mockBillingData: TOrganizationBilling = {
|
||||
limits: {
|
||||
monthly: { responses: 0 },
|
||||
@@ -30,17 +30,13 @@ describe("getOrganizationBillingByEnvironmentId", () => {
|
||||
|
||||
test("returns billing when organization is found", async () => {
|
||||
vi.mocked(prisma.organization.findFirst).mockResolvedValue({ billing: mockBillingData } as any);
|
||||
const result = await getOrganizationBillingByEnvironmentId(environmentId);
|
||||
const result = await getOrganizationBillingByWorkspaceId(workspaceId);
|
||||
expect(result).toEqual(mockBillingData);
|
||||
expect(prisma.organization.findFirst).toHaveBeenCalledWith({
|
||||
where: {
|
||||
workspaces: {
|
||||
some: {
|
||||
environments: {
|
||||
some: {
|
||||
id: environmentId,
|
||||
},
|
||||
},
|
||||
id: workspaceId,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -59,15 +55,15 @@ describe("getOrganizationBillingByEnvironmentId", () => {
|
||||
|
||||
test("returns null when organization is not found", async () => {
|
||||
vi.mocked(prisma.organization.findFirst).mockResolvedValueOnce(null);
|
||||
const result = await getOrganizationBillingByEnvironmentId(environmentId);
|
||||
const result = await getOrganizationBillingByWorkspaceId(workspaceId);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("logs error and returns null on exception", async () => {
|
||||
const error = new Error("db error");
|
||||
vi.mocked(prisma.organization.findFirst).mockRejectedValueOnce(error);
|
||||
const result = await getOrganizationBillingByEnvironmentId(environmentId);
|
||||
const result = await getOrganizationBillingByWorkspaceId(workspaceId);
|
||||
expect(result).toBeNull();
|
||||
expect(logger.error).toHaveBeenCalledWith(error, "Failed to get organization billing by environment ID");
|
||||
expect(logger.error).toHaveBeenCalledWith(error, "Failed to get organization billing by workspace ID");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,18 +3,14 @@ import { prisma } from "@formbricks/database";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { TOrganizationBilling } from "@formbricks/types/organizations";
|
||||
|
||||
export const getOrganizationBillingByEnvironmentId = reactCache(
|
||||
async (environmentId: string): Promise<TOrganizationBilling | null> => {
|
||||
export const getOrganizationBillingByWorkspaceId = reactCache(
|
||||
async (workspaceId: string): Promise<TOrganizationBilling | null> => {
|
||||
try {
|
||||
const organization = await prisma.organization.findFirst({
|
||||
where: {
|
||||
workspaces: {
|
||||
some: {
|
||||
environments: {
|
||||
some: {
|
||||
id: environmentId,
|
||||
},
|
||||
},
|
||||
id: workspaceId,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -43,7 +39,7 @@ export const getOrganizationBillingByEnvironmentId = reactCache(
|
||||
: { stripe: organization.billing.stripe as TOrganizationBilling["stripe"] }),
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to get organization billing by environment ID");
|
||||
logger.error(error, "Failed to get organization billing by workspace ID");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@ import { TResponseWithQuotaFull, TSurveyQuota } from "@formbricks/types/quota";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { TResponseInputV2 } from "@/app/api/v2/client/[environmentId]/responses/types/response";
|
||||
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||
import { getOrganization } from "@/lib/organization/service";
|
||||
import { calculateTtcTotal } from "@/lib/response/utils";
|
||||
import { getOrganizationIdFromWorkspaceId } from "@/lib/utils/helper";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
import { evaluateResponseQuotas } from "@/modules/ee/quotas/lib/evaluation-service";
|
||||
import { getContact } from "./contact";
|
||||
@@ -45,6 +46,7 @@ vi.mock("@/lib/constants", () => ({
|
||||
|
||||
vi.mock("@/lib/organization/service");
|
||||
vi.mock("@/lib/response/utils");
|
||||
vi.mock("@/lib/utils/helper");
|
||||
vi.mock("@/lib/utils/validate");
|
||||
vi.mock("@/modules/ee/quotas/lib/evaluation-service");
|
||||
vi.mock("@formbricks/database", () => ({
|
||||
@@ -58,6 +60,7 @@ vi.mock("@formbricks/logger");
|
||||
vi.mock("./contact");
|
||||
|
||||
const environmentId = "test-environment-id";
|
||||
const workspaceId = "test-workspace-id";
|
||||
const surveyId = "test-survey-id";
|
||||
const organizationId = "test-organization-id";
|
||||
const responseId = "test-response-id";
|
||||
@@ -79,6 +82,7 @@ const mockContact: { id: string; attributes: TContactAttributes } = {
|
||||
|
||||
const mockResponseInput: TResponseInputV2 = {
|
||||
environmentId,
|
||||
workspaceId,
|
||||
surveyId,
|
||||
contactId: null,
|
||||
displayId: null,
|
||||
@@ -151,7 +155,8 @@ describe("createResponse V2", () => {
|
||||
prisma.$transaction = vi.fn(async (cb: any) => cb(mockTx));
|
||||
|
||||
vi.mocked(validateInputs).mockImplementation((() => {}) as typeof validateInputs);
|
||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrganization as any);
|
||||
vi.mocked(getOrganizationIdFromWorkspaceId).mockResolvedValue(organizationId);
|
||||
vi.mocked(getOrganization).mockResolvedValue(mockOrganization as any);
|
||||
vi.mocked(getContact).mockResolvedValue(mockContact);
|
||||
vi.mocked(mockTx.response.create).mockResolvedValue(mockResponsePrisma as any);
|
||||
vi.mocked(calculateTtcTotal).mockImplementation((ttc) => ({
|
||||
@@ -169,7 +174,7 @@ describe("createResponse V2", () => {
|
||||
});
|
||||
|
||||
test("should throw ResourceNotFoundError if organization not found", async () => {
|
||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(null);
|
||||
vi.mocked(getOrganization).mockResolvedValue(null);
|
||||
await expect(
|
||||
createResponse(mockResponseInput, mockTx as unknown as Prisma.TransactionClient)
|
||||
).rejects.toThrow(ResourceNotFoundError);
|
||||
@@ -199,6 +204,7 @@ describe("createResponse V2", () => {
|
||||
id: "tag1",
|
||||
name: "Tag 1",
|
||||
environmentId,
|
||||
workspaceId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
@@ -223,7 +229,8 @@ describe("createResponseWithQuotaEvaluation V2", () => {
|
||||
},
|
||||
};
|
||||
prisma.$transaction = vi.fn(async (cb: any) => cb(mockTx));
|
||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrganization as any);
|
||||
vi.mocked(getOrganizationIdFromWorkspaceId).mockResolvedValue(organizationId);
|
||||
vi.mocked(getOrganization).mockResolvedValue(mockOrganization as any);
|
||||
vi.mocked(getContact).mockResolvedValue(mockContact);
|
||||
vi.mocked(mockTx.response.create).mockResolvedValue(mockResponsePrisma as any);
|
||||
vi.mocked(calculateTtcTotal).mockImplementation((ttc) => ({
|
||||
|
||||
@@ -8,8 +8,9 @@ import { TResponse, ZResponseInput } from "@formbricks/types/responses";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
import { responseSelection } from "@/app/api/v1/client/[environmentId]/responses/lib/response";
|
||||
import { TResponseInputV2 } from "@/app/api/v2/client/[environmentId]/responses/types/response";
|
||||
import { getOrganizationByEnvironmentId } from "@/lib/organization/service";
|
||||
import { getOrganization } from "@/lib/organization/service";
|
||||
import { calculateTtcTotal } from "@/lib/response/utils";
|
||||
import { getOrganizationIdFromWorkspaceId } from "@/lib/utils/helper";
|
||||
import { validateInputs } from "@/lib/utils/validate";
|
||||
import { evaluateResponseQuotas } from "@/modules/ee/quotas/lib/evaluation-service";
|
||||
import { getContact } from "./contact";
|
||||
@@ -90,14 +91,15 @@ export const createResponse = async (
|
||||
): Promise<TResponse> => {
|
||||
validateInputs([responseInput, ZResponseInput]);
|
||||
|
||||
const { environmentId, contactId, finished, ttc: initialTtc } = responseInput;
|
||||
const { workspaceId, contactId, finished, ttc: initialTtc } = responseInput;
|
||||
|
||||
try {
|
||||
let contact: { id: string; attributes: TContactAttributes } | null = null;
|
||||
|
||||
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||
const organizationId = await getOrganizationIdFromWorkspaceId(workspaceId);
|
||||
const organization = await getOrganization(organizationId);
|
||||
if (!organization) {
|
||||
throw new ResourceNotFoundError("Organization", environmentId);
|
||||
throw new ResourceNotFoundError("Organization", null);
|
||||
}
|
||||
|
||||
if (contactId) {
|
||||
|
||||
@@ -2,13 +2,13 @@ import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { TOrganizationBilling } from "@formbricks/types/organizations";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getOrganizationBillingByEnvironmentId } from "@/app/api/v2/client/[environmentId]/responses/lib/organization";
|
||||
import { getOrganizationBillingByWorkspaceId } from "@/app/api/v2/client/[environmentId]/responses/lib/organization";
|
||||
import { verifyRecaptchaToken } from "@/app/api/v2/client/[environmentId]/responses/lib/recaptcha";
|
||||
import { checkSurveyValidity } from "@/app/api/v2/client/[environmentId]/responses/lib/utils";
|
||||
import { TResponseInputV2 } from "@/app/api/v2/client/[environmentId]/responses/types/response";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { symmetricDecrypt } from "@/lib/crypto";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { getOrganizationIdFromWorkspaceId } from "@/lib/utils/helper";
|
||||
import { getIsSpamProtectionEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
|
||||
vi.mock("@/lib/i18n/utils", () => ({
|
||||
@@ -33,11 +33,11 @@ vi.mock("@/modules/ee/license-check/lib/utils", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("@/app/api/v2/client/[environmentId]/responses/lib/organization", () => ({
|
||||
getOrganizationBillingByEnvironmentId: vi.fn(),
|
||||
getOrganizationBillingByWorkspaceId: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/utils/helper", () => ({
|
||||
getOrganizationIdFromEnvironmentId: vi.fn(),
|
||||
getOrganizationIdFromWorkspaceId: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@formbricks/logger", () => ({
|
||||
@@ -97,6 +97,7 @@ const mockSurvey: TSurvey = {
|
||||
const mockResponseInput: TResponseInputV2 = {
|
||||
surveyId: "survey-1",
|
||||
environmentId: "env-1",
|
||||
workspaceId: "ws-1",
|
||||
data: {},
|
||||
finished: false,
|
||||
ttc: {},
|
||||
@@ -115,37 +116,37 @@ const mockBillingData: TOrganizationBilling = {
|
||||
describe("checkSurveyValidity", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(getOrganizationIdFromEnvironmentId).mockResolvedValue("cm8f4x9mm0001gx9h5b7d7h3q");
|
||||
vi.mocked(getOrganizationIdFromWorkspaceId).mockResolvedValue("cm8f4x9mm0001gx9h5b7d7h3q");
|
||||
});
|
||||
|
||||
test("should return badRequestResponse if survey environmentId does not match", async () => {
|
||||
const survey = { ...mockSurvey, environmentId: "env-2" };
|
||||
const result = await checkSurveyValidity(survey, "env-1", mockResponseInput);
|
||||
test("should return badRequestResponse if survey workspaceId does not match", async () => {
|
||||
const survey = { ...mockSurvey, workspaceId: "ws-2" };
|
||||
const result = await checkSurveyValidity(survey, "ws-1", mockResponseInput);
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
expect(result?.status).toBe(400);
|
||||
expect(responses.badRequestResponse).toHaveBeenCalledWith(
|
||||
"Survey is part of another environment",
|
||||
"Survey is part of another workspace",
|
||||
{
|
||||
"survey.environmentId": "env-2",
|
||||
environmentId: "env-1",
|
||||
"survey.workspaceId": "ws-2",
|
||||
workspaceId: "ws-1",
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test("should return null if recaptcha is not enabled", async () => {
|
||||
const survey = { ...mockSurvey, recaptcha: { enabled: false, threshold: 0.5 } };
|
||||
const result = await checkSurveyValidity(survey, "env-1", mockResponseInput);
|
||||
const survey = { ...mockSurvey, recaptcha: { enabled: false, threshold: 0.5 }, workspaceId: "ws-1" };
|
||||
const result = await checkSurveyValidity(survey, "ws-1", mockResponseInput);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("should return badRequestResponse if recaptcha enabled, spam protection enabled, but token is missing", async () => {
|
||||
const survey = { ...mockSurvey, recaptcha: { enabled: true, threshold: 0.5 } };
|
||||
const survey = { ...mockSurvey, recaptcha: { enabled: true, threshold: 0.5 }, workspaceId: "ws-1" };
|
||||
vi.mocked(getIsSpamProtectionEnabled).mockResolvedValue(true);
|
||||
const responseInputWithoutToken = { ...mockResponseInput };
|
||||
delete responseInputWithoutToken.recaptchaToken;
|
||||
|
||||
const result = await checkSurveyValidity(survey, "env-1", responseInputWithoutToken);
|
||||
const result = await checkSurveyValidity(survey, "ws-1", responseInputWithoutToken);
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
expect(result?.status).toBe(400);
|
||||
expect(logger.error).toHaveBeenCalledWith("Missing recaptcha token");
|
||||
@@ -157,26 +158,26 @@ describe("checkSurveyValidity", () => {
|
||||
});
|
||||
|
||||
test("should return not found response if billing data is not found", async () => {
|
||||
const survey = { ...mockSurvey, recaptcha: { enabled: true, threshold: 0.5 } };
|
||||
const survey = { ...mockSurvey, recaptcha: { enabled: true, threshold: 0.5 }, workspaceId: "ws-1" };
|
||||
vi.mocked(getIsSpamProtectionEnabled).mockResolvedValue(true);
|
||||
vi.mocked(getOrganizationBillingByEnvironmentId).mockResolvedValue(null);
|
||||
vi.mocked(getOrganizationBillingByWorkspaceId).mockResolvedValue(null);
|
||||
|
||||
const result = await checkSurveyValidity(survey, "env-1", {
|
||||
const result = await checkSurveyValidity(survey, "ws-1", {
|
||||
...mockResponseInput,
|
||||
recaptchaToken: "test-token",
|
||||
});
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
expect(result?.status).toBe(404);
|
||||
expect(responses.notFoundResponse).toHaveBeenCalledWith("Organization", null);
|
||||
expect(getOrganizationBillingByEnvironmentId).toHaveBeenCalledWith("env-1");
|
||||
expect(getOrganizationBillingByWorkspaceId).toHaveBeenCalledWith("ws-1");
|
||||
});
|
||||
|
||||
test("should return null if recaptcha is enabled but spam protection is disabled", async () => {
|
||||
const survey = { ...mockSurvey, recaptcha: { enabled: true, threshold: 0.5 } };
|
||||
const survey = { ...mockSurvey, recaptcha: { enabled: true, threshold: 0.5 }, workspaceId: "ws-1" };
|
||||
vi.mocked(getIsSpamProtectionEnabled).mockResolvedValue(false);
|
||||
vi.mocked(verifyRecaptchaToken).mockResolvedValue(true);
|
||||
vi.mocked(getOrganizationBillingByEnvironmentId).mockResolvedValue(mockBillingData);
|
||||
const result = await checkSurveyValidity(survey, "env-1", {
|
||||
vi.mocked(getOrganizationBillingByWorkspaceId).mockResolvedValue(mockBillingData);
|
||||
const result = await checkSurveyValidity(survey, "ws-1", {
|
||||
...mockResponseInput,
|
||||
recaptchaToken: "test-token",
|
||||
});
|
||||
@@ -185,13 +186,13 @@ describe("checkSurveyValidity", () => {
|
||||
});
|
||||
|
||||
test("should return badRequestResponse if recaptcha verification fails", async () => {
|
||||
const survey = { ...mockSurvey, recaptcha: { enabled: true, threshold: 0.5 } };
|
||||
const survey = { ...mockSurvey, recaptcha: { enabled: true, threshold: 0.5 }, workspaceId: "ws-1" };
|
||||
const responseInputWithToken = { ...mockResponseInput, recaptchaToken: "test-token" };
|
||||
vi.mocked(getIsSpamProtectionEnabled).mockResolvedValue(true);
|
||||
vi.mocked(verifyRecaptchaToken).mockResolvedValue(false);
|
||||
vi.mocked(getOrganizationBillingByEnvironmentId).mockResolvedValue(mockBillingData);
|
||||
vi.mocked(getOrganizationBillingByWorkspaceId).mockResolvedValue(mockBillingData);
|
||||
|
||||
const result = await checkSurveyValidity(survey, "env-1", responseInputWithToken);
|
||||
const result = await checkSurveyValidity(survey, "ws-1", responseInputWithToken);
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
expect(result?.status).toBe(400);
|
||||
expect(verifyRecaptchaToken).toHaveBeenCalledWith("test-token", 0.5);
|
||||
@@ -203,37 +204,37 @@ describe("checkSurveyValidity", () => {
|
||||
});
|
||||
|
||||
test("should return null if recaptcha verification passes", async () => {
|
||||
const survey = { ...mockSurvey, recaptcha: { enabled: true, threshold: 0.5 } };
|
||||
const survey = { ...mockSurvey, recaptcha: { enabled: true, threshold: 0.5 }, workspaceId: "ws-1" };
|
||||
const responseInputWithToken = { ...mockResponseInput, recaptchaToken: "test-token" };
|
||||
vi.mocked(getIsSpamProtectionEnabled).mockResolvedValue(true);
|
||||
vi.mocked(verifyRecaptchaToken).mockResolvedValue(true);
|
||||
vi.mocked(getOrganizationBillingByEnvironmentId).mockResolvedValue(mockBillingData);
|
||||
vi.mocked(getOrganizationBillingByWorkspaceId).mockResolvedValue(mockBillingData);
|
||||
|
||||
const result = await checkSurveyValidity(survey, "env-1", responseInputWithToken);
|
||||
const result = await checkSurveyValidity(survey, "ws-1", responseInputWithToken);
|
||||
expect(result).toBeNull();
|
||||
expect(verifyRecaptchaToken).toHaveBeenCalledWith("test-token", 0.5);
|
||||
});
|
||||
|
||||
test("should return null for a valid survey and input", async () => {
|
||||
const survey = { ...mockSurvey }; // Recaptcha disabled by default in mock
|
||||
const result = await checkSurveyValidity(survey, "env-1", mockResponseInput);
|
||||
const survey = { ...mockSurvey, workspaceId: "ws-1" }; // Recaptcha disabled by default in mock
|
||||
const result = await checkSurveyValidity(survey, "ws-1", mockResponseInput);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("should return badRequestResponse if singleUse is enabled and singleUseId is missing", async () => {
|
||||
const survey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: false } };
|
||||
const result = await checkSurveyValidity(survey, "env-1", { ...mockResponseInput });
|
||||
const survey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: false }, workspaceId: "ws-1" };
|
||||
const result = await checkSurveyValidity(survey, "ws-1", { ...mockResponseInput });
|
||||
expect(result).toBeInstanceOf(Response);
|
||||
expect(result?.status).toBe(400);
|
||||
expect(responses.badRequestResponse).toHaveBeenCalledWith("Missing single use id", {
|
||||
surveyId: survey.id,
|
||||
environmentId: "env-1",
|
||||
workspaceId: "ws-1",
|
||||
});
|
||||
});
|
||||
|
||||
test("should return badRequestResponse if singleUse is enabled and meta.url is missing", async () => {
|
||||
const survey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: false } };
|
||||
const result = await checkSurveyValidity(survey, "env-1", {
|
||||
const survey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: false }, workspaceId: "ws-1" };
|
||||
const result = await checkSurveyValidity(survey, "ws-1", {
|
||||
...mockResponseInput,
|
||||
singleUseId: "su-1",
|
||||
meta: {},
|
||||
@@ -242,13 +243,13 @@ describe("checkSurveyValidity", () => {
|
||||
expect(result?.status).toBe(400);
|
||||
expect(responses.badRequestResponse).toHaveBeenCalledWith("Missing or invalid URL in response metadata", {
|
||||
surveyId: survey.id,
|
||||
environmentId: "env-1",
|
||||
workspaceId: "ws-1",
|
||||
});
|
||||
});
|
||||
|
||||
test("should return badRequestResponse if meta.url is invalid", async () => {
|
||||
const survey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: false } };
|
||||
const result = await checkSurveyValidity(survey, "env-1", {
|
||||
const survey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: false }, workspaceId: "ws-1" };
|
||||
const result = await checkSurveyValidity(survey, "ws-1", {
|
||||
...mockResponseInput,
|
||||
singleUseId: "su-1",
|
||||
meta: { url: "not-a-url" },
|
||||
@@ -257,14 +258,14 @@ describe("checkSurveyValidity", () => {
|
||||
expect(result?.status).toBe(400);
|
||||
expect(responses.badRequestResponse).toHaveBeenCalledWith(
|
||||
"Invalid URL in response metadata",
|
||||
expect.objectContaining({ surveyId: survey.id, environmentId: "env-1" })
|
||||
expect.objectContaining({ surveyId: survey.id, workspaceId: "ws-1" })
|
||||
);
|
||||
});
|
||||
|
||||
test("should return badRequestResponse if suId is missing from url", async () => {
|
||||
const survey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: false } };
|
||||
const survey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: false }, workspaceId: "ws-1" };
|
||||
const url = "https://example.com/?foo=bar";
|
||||
const result = await checkSurveyValidity(survey, "env-1", {
|
||||
const result = await checkSurveyValidity(survey, "ws-1", {
|
||||
...mockResponseInput,
|
||||
singleUseId: "su-1",
|
||||
meta: { url },
|
||||
@@ -273,15 +274,15 @@ describe("checkSurveyValidity", () => {
|
||||
expect(result?.status).toBe(400);
|
||||
expect(responses.badRequestResponse).toHaveBeenCalledWith("Missing single use id", {
|
||||
surveyId: survey.id,
|
||||
environmentId: "env-1",
|
||||
workspaceId: "ws-1",
|
||||
});
|
||||
});
|
||||
|
||||
test("should return badRequestResponse if isEncrypted and decrypted suId does not match singleUseId", async () => {
|
||||
const survey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: true } };
|
||||
const survey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: true }, workspaceId: "ws-1" };
|
||||
const url = "https://example.com/?suId=encrypted-id";
|
||||
vi.mocked(symmetricDecrypt).mockReturnValue("decrypted-id");
|
||||
const resultEncryptedMismatch = await checkSurveyValidity(survey, "env-1", {
|
||||
const resultEncryptedMismatch = await checkSurveyValidity(survey, "ws-1", {
|
||||
...mockResponseInput,
|
||||
singleUseId: "su-1",
|
||||
meta: { url },
|
||||
@@ -291,14 +292,14 @@ describe("checkSurveyValidity", () => {
|
||||
expect(resultEncryptedMismatch?.status).toBe(400);
|
||||
expect(responses.badRequestResponse).toHaveBeenCalledWith("Invalid single use id", {
|
||||
surveyId: survey.id,
|
||||
environmentId: "env-1",
|
||||
workspaceId: "ws-1",
|
||||
});
|
||||
});
|
||||
|
||||
test("should return badRequestResponse if not encrypted and suId does not match singleUseId", async () => {
|
||||
const survey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: false } };
|
||||
const survey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: false }, workspaceId: "ws-1" };
|
||||
const url = "https://example.com/?suId=su-2";
|
||||
const result = await checkSurveyValidity(survey, "env-1", {
|
||||
const result = await checkSurveyValidity(survey, "ws-1", {
|
||||
...mockResponseInput,
|
||||
singleUseId: "su-1",
|
||||
meta: { url },
|
||||
@@ -307,14 +308,14 @@ describe("checkSurveyValidity", () => {
|
||||
expect(result?.status).toBe(400);
|
||||
expect(responses.badRequestResponse).toHaveBeenCalledWith("Invalid single use id", {
|
||||
surveyId: survey.id,
|
||||
environmentId: "env-1",
|
||||
workspaceId: "ws-1",
|
||||
});
|
||||
});
|
||||
|
||||
test("should return null if singleUse is enabled, not encrypted, and suId matches singleUseId", async () => {
|
||||
const survey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: false } };
|
||||
const survey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: false }, workspaceId: "ws-1" };
|
||||
const url = "https://example.com/?suId=su-1";
|
||||
const result = await checkSurveyValidity(survey, "env-1", {
|
||||
const result = await checkSurveyValidity(survey, "ws-1", {
|
||||
...mockResponseInput,
|
||||
singleUseId: "su-1",
|
||||
meta: { url },
|
||||
@@ -323,10 +324,10 @@ describe("checkSurveyValidity", () => {
|
||||
});
|
||||
|
||||
test("should return null if singleUse is enabled, encrypted, and decrypted suId matches singleUseId", async () => {
|
||||
const survey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: true } };
|
||||
const survey = { ...mockSurvey, singleUse: { enabled: true, isEncrypted: true }, workspaceId: "ws-1" };
|
||||
const url = "https://example.com/?suId=encrypted-id";
|
||||
vi.mocked(symmetricDecrypt).mockReturnValue("su-1");
|
||||
const _resultEncryptedMatch = await checkSurveyValidity(survey, "env-1", {
|
||||
const _resultEncryptedMatch = await checkSurveyValidity(survey, "ws-1", {
|
||||
...mockResponseInput,
|
||||
singleUseId: "su-1",
|
||||
meta: { url },
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getOrganizationBillingByEnvironmentId } from "@/app/api/v2/client/[environmentId]/responses/lib/organization";
|
||||
import { getOrganizationBillingByWorkspaceId } from "@/app/api/v2/client/[environmentId]/responses/lib/organization";
|
||||
import { verifyRecaptchaToken } from "@/app/api/v2/client/[environmentId]/responses/lib/recaptcha";
|
||||
import { TResponseInputV2 } from "@/app/api/v2/client/[environmentId]/responses/types/response";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { ENCRYPTION_KEY } from "@/lib/constants";
|
||||
import { symmetricDecrypt } from "@/lib/crypto";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { getOrganizationIdFromWorkspaceId } from "@/lib/utils/helper";
|
||||
import { getIsSpamProtectionEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
|
||||
export const RECAPTCHA_VERIFICATION_ERROR_CODE = "recaptcha_verification_failed";
|
||||
|
||||
export const checkSurveyValidity = async (
|
||||
survey: TSurvey,
|
||||
environmentId: string,
|
||||
workspaceId: string,
|
||||
responseInput: TResponseInputV2
|
||||
): Promise<Response | null> => {
|
||||
if (survey.environmentId !== environmentId) {
|
||||
if (survey.workspaceId !== workspaceId) {
|
||||
return responses.badRequestResponse(
|
||||
"Survey is part of another environment",
|
||||
"Survey is part of another workspace",
|
||||
{
|
||||
"survey.environmentId": survey.environmentId,
|
||||
environmentId,
|
||||
"survey.workspaceId": survey.workspaceId,
|
||||
workspaceId,
|
||||
},
|
||||
true
|
||||
);
|
||||
@@ -31,14 +31,14 @@ export const checkSurveyValidity = async (
|
||||
if (!responseInput.singleUseId) {
|
||||
return responses.badRequestResponse("Missing single use id", {
|
||||
surveyId: survey.id,
|
||||
environmentId,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
if (!responseInput.meta?.url) {
|
||||
return responses.badRequestResponse("Missing or invalid URL in response metadata", {
|
||||
surveyId: survey.id,
|
||||
environmentId,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export const checkSurveyValidity = async (
|
||||
} catch (error) {
|
||||
return responses.badRequestResponse("Invalid URL in response metadata", {
|
||||
surveyId: survey.id,
|
||||
environmentId,
|
||||
workspaceId,
|
||||
error: error instanceof Error ? error.message : "Unknown error occurred",
|
||||
});
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export const checkSurveyValidity = async (
|
||||
if (!suId) {
|
||||
return responses.badRequestResponse("Missing single use id", {
|
||||
surveyId: survey.id,
|
||||
environmentId,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -65,13 +65,13 @@ export const checkSurveyValidity = async (
|
||||
if (decryptedSuId !== responseInput.singleUseId) {
|
||||
return responses.badRequestResponse("Invalid single use id", {
|
||||
surveyId: survey.id,
|
||||
environmentId,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
} else if (responseInput.singleUseId !== suId) {
|
||||
return responses.badRequestResponse("Invalid single use id", {
|
||||
surveyId: survey.id,
|
||||
environmentId,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -87,13 +87,13 @@ export const checkSurveyValidity = async (
|
||||
true
|
||||
);
|
||||
}
|
||||
const billing = await getOrganizationBillingByEnvironmentId(environmentId);
|
||||
const billing = await getOrganizationBillingByWorkspaceId(workspaceId);
|
||||
|
||||
if (!billing) {
|
||||
return responses.notFoundResponse("Organization", null);
|
||||
}
|
||||
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(environmentId);
|
||||
const organizationId = await getOrganizationIdFromWorkspaceId(workspaceId);
|
||||
const isSpamProtectionEnabled = await getIsSpamProtectionEnabled(organizationId);
|
||||
|
||||
if (!isSpamProtectionEnabled) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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 +10,8 @@ import { sendToPipeline } from "@/app/lib/pipelines";
|
||||
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 { getOrganizationIdFromWorkspaceId } 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,17 +49,18 @@ export const POST = async (request: Request, context: Context): Promise<Response
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
// 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, workspaceId } = resolved;
|
||||
|
||||
const responseInputValidation = ZResponseInputV2.safeParse({
|
||||
...responseInput,
|
||||
environmentId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (!responseInputValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
@@ -81,7 +82,7 @@ export const POST = async (request: Request, context: Context): Promise<Response
|
||||
const responseInputData = responseInputValidation.data;
|
||||
|
||||
if (responseInputData.contactId) {
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(environmentId);
|
||||
const organizationId = await getOrganizationIdFromWorkspaceId(workspaceId);
|
||||
const isContactsEnabled = await getIsContactsEnabled(organizationId);
|
||||
if (!isContactsEnabled) {
|
||||
return responses.forbiddenResponse("User identification is only available for enterprise users.", true);
|
||||
@@ -93,7 +94,7 @@ export const POST = async (request: Request, context: Context): Promise<Response
|
||||
if (!survey) {
|
||||
return responses.notFoundResponse("Survey", responseInput.surveyId, true);
|
||||
}
|
||||
const surveyCheckResult = await checkSurveyValidity(survey, environmentId, responseInput);
|
||||
const surveyCheckResult = await checkSurveyValidity(survey, workspaceId, responseInput);
|
||||
if (surveyCheckResult) return surveyCheckResult;
|
||||
|
||||
// Validate response data for "other" options exceeding character limit
|
||||
@@ -168,6 +169,7 @@ export const POST = async (request: Request, context: Context): Promise<Response
|
||||
sendToPipeline({
|
||||
event: "responseCreated",
|
||||
environmentId,
|
||||
workspaceId,
|
||||
surveyId: responseData.surveyId,
|
||||
response: responseData,
|
||||
});
|
||||
@@ -176,6 +178,7 @@ export const POST = async (request: Request, context: Context): Promise<Response
|
||||
sendToPipeline({
|
||||
event: "responseFinished",
|
||||
environmentId,
|
||||
workspaceId,
|
||||
surveyId: responseData.surveyId,
|
||||
response: responseData,
|
||||
});
|
||||
|
||||
@@ -45,6 +45,7 @@ describe("pipelines", () => {
|
||||
event: PipelineTriggers.responseCreated,
|
||||
surveyId: "cm8ckvchx000008lb710n0gdn",
|
||||
environmentId: "cm8cmp9hp000008jf7l570ml2",
|
||||
workspaceId: "cm8cnq2hp000008jf7l570abc",
|
||||
response: { id: "cm8cmpnjj000108jfdr9dfqe6" } as TResponse,
|
||||
};
|
||||
|
||||
@@ -61,6 +62,7 @@ describe("pipelines", () => {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
environmentId: testData.environmentId,
|
||||
workspaceId: testData.workspaceId,
|
||||
surveyId: testData.surveyId,
|
||||
event: testData.event,
|
||||
response: testData.response,
|
||||
@@ -78,6 +80,7 @@ describe("pipelines", () => {
|
||||
event: PipelineTriggers.responseCreated,
|
||||
surveyId: "cm8ckvchx000008lb710n0gdn",
|
||||
environmentId: "cm8cmp9hp000008jf7l570ml2",
|
||||
workspaceId: "cm8cnq2hp000008jf7l570abc",
|
||||
response: { id: "cm8cmpnjj000108jfdr9dfqe6" } as TResponse,
|
||||
};
|
||||
|
||||
@@ -104,6 +107,7 @@ describe("pipelines", () => {
|
||||
event: PipelineTriggers.responseCreated,
|
||||
surveyId: "cm8ckvchx000008lb710n0gdn",
|
||||
environmentId: "cm8cmp9hp000008jf7l570ml2",
|
||||
workspaceId: "cm8cnq2hp000008jf7l570abc",
|
||||
response: { id: "cm8cmpnjj000108jfdr9dfqe6" } as TResponse,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,13 @@ import { logger } from "@formbricks/logger";
|
||||
import { TPipelineInput } from "@/app/lib/types/pipelines";
|
||||
import { CRON_SECRET, WEBAPP_URL } from "@/lib/constants";
|
||||
|
||||
export const sendToPipeline = async ({ event, surveyId, environmentId, response }: TPipelineInput) => {
|
||||
export const sendToPipeline = async ({
|
||||
event,
|
||||
surveyId,
|
||||
environmentId,
|
||||
workspaceId,
|
||||
response,
|
||||
}: TPipelineInput) => {
|
||||
if (!CRON_SECRET) {
|
||||
throw new Error("CRON_SECRET is not set");
|
||||
}
|
||||
@@ -14,8 +20,9 @@ export const sendToPipeline = async ({ event, surveyId, environmentId, response
|
||||
"x-api-key": CRON_SECRET,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
environmentId: environmentId,
|
||||
surveyId: surveyId,
|
||||
environmentId,
|
||||
workspaceId,
|
||||
surveyId,
|
||||
event,
|
||||
response,
|
||||
}),
|
||||
|
||||
@@ -5,5 +5,6 @@ export interface TPipelineInput {
|
||||
event: PipelineTriggers;
|
||||
response: TResponse;
|
||||
environmentId: string;
|
||||
workspaceId: string;
|
||||
surveyId: string;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ export const mockDisplayWithResponseId = createMockDisplay({
|
||||
|
||||
export const mockDisplayInput = {
|
||||
environmentId: mockEnvironmentId,
|
||||
workspaceId: mockId,
|
||||
surveyId: mockSurveyId,
|
||||
};
|
||||
export const mockDisplayInputWithUserId = {
|
||||
|
||||
@@ -14,6 +14,7 @@ type ResponseMock = Prisma.ResponseGetPayload<{
|
||||
}>;
|
||||
|
||||
export const mockEnvironmentId = "ars2tjk8hsi8oqk1uac00mo7";
|
||||
export const mockWorkspaceId = "wksp2tjk8hsi8oqk1uac00mo";
|
||||
export const mockContactId = "lhwy39ga2zy8by1ol1bnaiso";
|
||||
export const mockResponseId = "z32bqib0nlcw8vqymlj6m8x7";
|
||||
export const mockSingleUseId = "qj57j3opsw8b5sxgea20fgcq";
|
||||
@@ -48,6 +49,7 @@ export const mockTags = [
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: mockEnvironmentId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -117,6 +119,7 @@ const getMockTags = (tags: string[]): { tag: TTag }[] => {
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
environmentId: mockEnvironmentId,
|
||||
workspaceId: mockWorkspaceId,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
88
apps/web/lib/utils/resolve-client-id.test.ts
Normal file
88
apps/web/lib/utils/resolve-client-id.test.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
46
apps/web/lib/utils/resolve-client-id.ts
Normal file
46
apps/web/lib/utils/resolve-client-id.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import "server-only";
|
||||
import { cache as reactCache } from "react";
|
||||
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 = reactCache(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?.environments[0]) {
|
||||
return { workspaceId: workspace.id, environmentId: workspace.environments[0].id };
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { sendToPipeline } from "@/app/lib/pipelines";
|
||||
import { getWorkspaceIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { formatValidationErrorsForV2Api, validateResponseData } from "@/modules/api/lib/validation";
|
||||
import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client";
|
||||
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/element";
|
||||
@@ -224,9 +225,12 @@ export const PUT = (request: Request, props: { params: Promise<{ responseId: str
|
||||
// Fetch updated response with relations for pipeline
|
||||
const updatedResponseForPipeline = await getResponseForPipeline(params.responseId);
|
||||
if (updatedResponseForPipeline.ok) {
|
||||
const workspaceId = await getWorkspaceIdFromEnvironmentId(environmentIdResult.data);
|
||||
|
||||
sendToPipeline({
|
||||
event: "responseUpdated",
|
||||
environmentId: environmentIdResult.data,
|
||||
workspaceId,
|
||||
surveyId: existingResponse.data.surveyId,
|
||||
response: updatedResponseForPipeline.data,
|
||||
});
|
||||
@@ -235,6 +239,7 @@ export const PUT = (request: Request, props: { params: Promise<{ responseId: str
|
||||
sendToPipeline({
|
||||
event: "responseFinished",
|
||||
environmentId: environmentIdResult.data,
|
||||
workspaceId,
|
||||
surveyId: existingResponse.data.surveyId,
|
||||
response: updatedResponseForPipeline.data,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Response } from "@prisma/client";
|
||||
import { NextRequest } from "next/server";
|
||||
import { sendToPipeline } from "@/app/lib/pipelines";
|
||||
import { getWorkspaceIdFromEnvironmentId } from "@/lib/utils/helper";
|
||||
import { formatValidationErrorsForV2Api, validateResponseData } from "@/modules/api/lib/validation";
|
||||
import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client";
|
||||
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/element";
|
||||
@@ -158,9 +159,12 @@ export const POST = async (request: Request) =>
|
||||
// Fetch created response with relations for pipeline
|
||||
const createdResponseForPipeline = await getResponseForPipeline(createResponseResult.data.id);
|
||||
if (createdResponseForPipeline.ok) {
|
||||
const workspaceId = await getWorkspaceIdFromEnvironmentId(environmentId);
|
||||
|
||||
sendToPipeline({
|
||||
event: "responseCreated",
|
||||
environmentId: environmentId,
|
||||
workspaceId,
|
||||
surveyId: body.surveyId,
|
||||
response: createdResponseForPipeline.data,
|
||||
});
|
||||
@@ -169,6 +173,7 @@ export const POST = async (request: Request) =>
|
||||
sendToPipeline({
|
||||
event: "responseFinished",
|
||||
environmentId: environmentId,
|
||||
workspaceId,
|
||||
surveyId: body.surveyId,
|
||||
response: createdResponseForPipeline.data,
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ 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";
|
||||
|
||||
@@ -45,15 +46,10 @@ export const POST = withV1ApiWrapper({
|
||||
};
|
||||
}
|
||||
|
||||
const environmentId = params.environmentId.trim();
|
||||
const idParam = params.environmentId.trim();
|
||||
|
||||
// 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);
|
||||
// Validate CUID format
|
||||
const cuidValidation = ZEnvironmentId.safeParse(idParam);
|
||||
if (!cuidValidation.success) {
|
||||
logger.warn(
|
||||
{
|
||||
@@ -61,13 +57,22 @@ export const POST = withV1ApiWrapper({
|
||||
url: req.url,
|
||||
validationError: cuidValidation.error.issues[0]?.message,
|
||||
},
|
||||
"Invalid CUID v1 format detected"
|
||||
"Invalid CUID 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
|
||||
|
||||
@@ -34,7 +34,7 @@ export class ApiClient {
|
||||
}
|
||||
|
||||
async createDisplay(
|
||||
displayInput: Omit<TDisplayCreateInput, "environmentId"> & { contactId?: string }
|
||||
displayInput: Omit<TDisplayCreateInput, "environmentId" | "workspaceId"> & { contactId?: string }
|
||||
): Promise<Result<{ id: string }, ApiErrorResponse>> {
|
||||
const fromV1 = !!displayInput.userId;
|
||||
|
||||
@@ -47,7 +47,7 @@ export class ApiClient {
|
||||
}
|
||||
|
||||
async createResponse(
|
||||
responseInput: Omit<TResponseInput, "environmentId"> & {
|
||||
responseInput: Omit<TResponseInput, "environmentId" | "workspaceId"> & {
|
||||
contactId: string | null;
|
||||
recaptchaToken?: string;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export type TDisplay = z.infer<typeof ZDisplay>;
|
||||
|
||||
export const ZDisplayCreateInput = z.object({
|
||||
environmentId: z.cuid2(),
|
||||
workspaceId: z.cuid2(),
|
||||
surveyId: z.cuid2(),
|
||||
userId: z
|
||||
.string()
|
||||
|
||||
@@ -343,6 +343,7 @@ export const ZResponseInput = z.object({
|
||||
createdAt: z.coerce.date().optional(),
|
||||
updatedAt: z.coerce.date().optional(),
|
||||
environmentId: z.cuid2(),
|
||||
workspaceId: z.cuid2(),
|
||||
surveyId: z.cuid2(),
|
||||
userId: z.string().nullish(),
|
||||
displayId: z.string().nullish(),
|
||||
|
||||
@@ -109,6 +109,7 @@ export const ZUploadPrivateFileRequest = z
|
||||
allowedFileExtensions: z.array(ZAllowedFileExtension).optional(),
|
||||
surveyId: z.cuid2(),
|
||||
environmentId: z.cuid2(),
|
||||
workspaceId: z.cuid2(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
refineFileUploadInput({
|
||||
|
||||
Reference in New Issue
Block a user