mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-25 09:58:59 -06:00
Add new client endpoints & webhook functionality (#355)
* add new zod schema for responses * add new client endpoints for responses * add services for responses and surveys * add new responses model to webhooks & email --------- Co-authored-by: Johannes <johannes@formbricks.com>
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -34,6 +34,9 @@ yarn-error.log*
|
||||
.env.production.local
|
||||
!packages/database/.env
|
||||
|
||||
# Prisma generated files
|
||||
packages/database/zod
|
||||
|
||||
# turbo
|
||||
.turbo
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ To get the project running locally on your machine you need to have the followin
|
||||
1. Make sure your Docker containers are running. Then let prisma set up the database for you:
|
||||
|
||||
```bash
|
||||
pnpm db:migrate:dev
|
||||
pnpm prisma migrate dev
|
||||
```
|
||||
|
||||
1. Start the development server of the app:
|
||||
|
||||
@@ -1,54 +1,49 @@
|
||||
import { responses } from "@/lib/api/response";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { transformErrorToDetails } from "@/lib/api/validator";
|
||||
import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getResponse, updateResponse } from "@formbricks/lib/services/response";
|
||||
import { getSurvey } from "@formbricks/lib/services/survey";
|
||||
import { ZResponseUpdateInput } from "@formbricks/types/v1/responses";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return responses.successResponse({}, true);
|
||||
}
|
||||
|
||||
export async function PUT(request: Request, params: { responseId: string }): Promise<NextResponse> {
|
||||
export async function PUT(
|
||||
request: Request,
|
||||
{ params }: { params: { responseId: string } }
|
||||
): Promise<NextResponse> {
|
||||
const { responseId } = params;
|
||||
const { response } = await request.json();
|
||||
const responseUpdate = await request.json();
|
||||
|
||||
if (!response) {
|
||||
return responses.missingFieldResponse("response", true);
|
||||
const inputValidation = ZResponseUpdateInput.safeParse(responseUpdate);
|
||||
|
||||
if (!inputValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
"Fields are missing or incorrectly formatted",
|
||||
transformErrorToDetails(inputValidation.error),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
const currentResponse = await prisma.response.findUnique({
|
||||
where: {
|
||||
id: responseId,
|
||||
},
|
||||
select: {
|
||||
data: true,
|
||||
survey: {
|
||||
select: {
|
||||
environmentId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
// get current response
|
||||
const currentResponse = await getResponse(responseId);
|
||||
|
||||
if (!currentResponse) {
|
||||
return responses.notFoundResponse("Response", responseId, true);
|
||||
}
|
||||
|
||||
const environmentId = currentResponse.survey.environmentId;
|
||||
|
||||
const newResponseData = {
|
||||
...JSON.parse(JSON.stringify(currentResponse?.data)),
|
||||
...response.data,
|
||||
};
|
||||
// get survey to get environmentId
|
||||
const survey = await getSurvey(currentResponse.surveyId);
|
||||
if (!survey) {
|
||||
// shouldn't happen as survey relation is required
|
||||
return responses.notFoundResponse("Survey", currentResponse.surveyId, true);
|
||||
}
|
||||
const environmentId = survey.environmentId;
|
||||
|
||||
// update response
|
||||
const responseData = await prisma.response.update({
|
||||
where: {
|
||||
id: responseId,
|
||||
},
|
||||
data: {
|
||||
...{ ...response, data: newResponseData },
|
||||
},
|
||||
});
|
||||
const response = await updateResponse(responseId, responseUpdate);
|
||||
|
||||
// send response update to pipeline
|
||||
// don't await to not block the response
|
||||
@@ -60,8 +55,9 @@ export async function PUT(request: Request, params: { responseId: string }): Pro
|
||||
body: JSON.stringify({
|
||||
internalSecret: INTERNAL_SECRET,
|
||||
environmentId,
|
||||
surveyId: response.surveyId,
|
||||
event: "responseUpdated",
|
||||
data: { id: responseId, ...response },
|
||||
data: response,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -76,11 +72,12 @@ export async function PUT(request: Request, params: { responseId: string }): Pro
|
||||
body: JSON.stringify({
|
||||
internalSecret: INTERNAL_SECRET,
|
||||
environmentId,
|
||||
surveyId: response.surveyId,
|
||||
event: "responseFinished",
|
||||
data: responseData,
|
||||
data: response,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return responses.successResponse({ ...responseData }, true);
|
||||
return responses.successResponse(response, true);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
/*
|
||||
THIS FILE IS WORK IN PROGRESS
|
||||
PLEASE DO NOT USE IT YET
|
||||
*/
|
||||
|
||||
import { responses } from "@/lib/api/response";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { transformErrorToDetails } from "@/lib/api/validator";
|
||||
import { createResponse } from "@formbricks/lib/services/response";
|
||||
import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { captureTelemetry } from "@formbricks/lib/telemetry";
|
||||
import { getSurvey } from "@formbricks/lib/services/survey";
|
||||
import { TResponseInput, ZResponseInput } from "@formbricks/types/v1/responses";
|
||||
import { NextResponse } from "next/server";
|
||||
import { captureTelemetry } from "@formbricks/lib/telemetry";
|
||||
import { capturePosthogEvent } from "@formbricks/lib/posthogServer";
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
@@ -15,80 +14,67 @@ export async function OPTIONS(): Promise<NextResponse> {
|
||||
}
|
||||
|
||||
export async function POST(request: Request): Promise<NextResponse> {
|
||||
const { surveyId, userCuid, response } = await request.json();
|
||||
const responseInput: TResponseInput = await request.json();
|
||||
const inputValidation = ZResponseInput.safeParse(responseInput);
|
||||
|
||||
if (!surveyId) {
|
||||
return responses.missingFieldResponse("surveyId", true);
|
||||
if (!inputValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
"Fields are missing or incorrectly formatted",
|
||||
transformErrorToDetails(inputValidation.error),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return responses.missingFieldResponse("response", true);
|
||||
}
|
||||
|
||||
// userCuid can be null, e.g. for link surveys
|
||||
|
||||
// check if survey exists
|
||||
const survey = await prisma.survey.findUnique({
|
||||
where: {
|
||||
id: surveyId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
environment: {
|
||||
const survey = await getSurvey(responseInput.surveyId);
|
||||
|
||||
if (!survey) {
|
||||
return responses.badRequestResponse(
|
||||
"Linked ressource not found",
|
||||
{
|
||||
surveyId: "Survey not found",
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
const environmentId = survey.environmentId;
|
||||
|
||||
// prisma call to get the teamId
|
||||
// TODO use services
|
||||
const environment = await prisma.environment.findUnique({
|
||||
where: { id: environmentId },
|
||||
include: {
|
||||
product: {
|
||||
select: {
|
||||
id: true,
|
||||
product: {
|
||||
team: {
|
||||
select: {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
memberships: {
|
||||
select: {
|
||||
userId: true,
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
memberships: {
|
||||
where: { role: "owner" },
|
||||
select: { userId: true },
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
type: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!survey) {
|
||||
return responses.notFoundResponse("Survey", surveyId, true);
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
|
||||
const environmentId = survey.environment.id;
|
||||
|
||||
const teamId = survey.environment.product.team.id;
|
||||
// find team owner
|
||||
const teamOwnerId = survey.environment.product.team.memberships.find((m) => m.role === "owner")?.userId;
|
||||
|
||||
const createBody = {
|
||||
data: {
|
||||
survey: {
|
||||
connect: {
|
||||
id: surveyId,
|
||||
},
|
||||
},
|
||||
...response,
|
||||
const {
|
||||
product: {
|
||||
team: { id: teamId, memberships },
|
||||
},
|
||||
};
|
||||
} = environment;
|
||||
|
||||
if (userCuid) {
|
||||
createBody.data.person = {
|
||||
connect: {
|
||||
id: userCuid,
|
||||
},
|
||||
};
|
||||
}
|
||||
const teamOwnerId = memberships[0]?.userId;
|
||||
|
||||
// create new response
|
||||
const responseData = await prisma.response.create(createBody);
|
||||
const response = await createResponse(responseInput);
|
||||
|
||||
// send response to pipeline
|
||||
// don't await to not block the response
|
||||
@@ -100,12 +86,13 @@ export async function POST(request: Request): Promise<NextResponse> {
|
||||
body: JSON.stringify({
|
||||
internalSecret: INTERNAL_SECRET,
|
||||
environmentId,
|
||||
surveyId: response.surveyId,
|
||||
event: "responseCreated",
|
||||
data: responseData,
|
||||
data: response,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.finished) {
|
||||
if (responseInput.finished) {
|
||||
// send response to pipeline
|
||||
// don't await to not block the response
|
||||
fetch(`${WEBAPP_URL}/api/pipeline`, {
|
||||
@@ -116,8 +103,9 @@ export async function POST(request: Request): Promise<NextResponse> {
|
||||
body: JSON.stringify({
|
||||
internalSecret: INTERNAL_SECRET,
|
||||
environmentId,
|
||||
surveyId: response.surveyId,
|
||||
event: "responseFinished",
|
||||
data: responseData,
|
||||
data: response,
|
||||
}),
|
||||
});
|
||||
}
|
||||
@@ -125,12 +113,12 @@ export async function POST(request: Request): Promise<NextResponse> {
|
||||
captureTelemetry("response created");
|
||||
if (teamOwnerId) {
|
||||
await capturePosthogEvent(teamOwnerId, "response created", teamId, {
|
||||
surveyId,
|
||||
surveyId: response.surveyId,
|
||||
surveyType: survey.type,
|
||||
});
|
||||
} else {
|
||||
console.warn("Posthog capture not possible. No team owner found");
|
||||
}
|
||||
|
||||
return responses.successResponse({ id: responseData.id }, true);
|
||||
return responses.successResponse(response, true);
|
||||
}
|
||||
|
||||
@@ -138,6 +138,7 @@ export default function SurveyMenuBar({
|
||||
<Button
|
||||
disabled={
|
||||
localSurvey.type === "web" &&
|
||||
localSurvey.triggers &&
|
||||
(localSurvey.triggers[0] === "" || localSurvey.triggers.length === 0)
|
||||
}
|
||||
variant="darkCTA"
|
||||
|
||||
@@ -6,11 +6,12 @@ import QuestionConditional from "@/components/preview/QuestionConditional";
|
||||
import ThankYouCard from "@/components/preview/ThankYouCard";
|
||||
import ContentWrapper from "@/components/shared/ContentWrapper";
|
||||
import LoadingSpinner from "@/components/shared/LoadingSpinner";
|
||||
import { createDisplay, markDisplayResponded } from "@formbricks/lib/clientDisplay/display";
|
||||
import { createResponse, updateResponse } from "@formbricks/lib/clientResponse/response";
|
||||
import { createDisplay, markDisplayResponded } from "@formbricks/lib/client/display";
|
||||
import { createResponse, updateResponse } from "@formbricks/lib/client/response";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import type { Logic, Question } from "@formbricks/types/questions";
|
||||
import type { Survey } from "@formbricks/types/surveys";
|
||||
import { TResponseInput } from "@formbricks/types/v1/responses";
|
||||
import { Confetti } from "@formbricks/ui";
|
||||
import { ArrowPathIcon } from "@heroicons/react/24/solid";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -144,15 +145,16 @@ export default function LinkSurvey({ survey }: LinkSurveyProps) {
|
||||
|
||||
const finished = nextQuestionId === "end";
|
||||
// build response
|
||||
const responseRequest = {
|
||||
const responseRequest: TResponseInput = {
|
||||
surveyId: survey.id,
|
||||
response: { finished, data },
|
||||
personId: null,
|
||||
finished,
|
||||
data,
|
||||
};
|
||||
if (!responseId && !isPreview) {
|
||||
const response = await createResponse(
|
||||
responseRequest,
|
||||
`${window.location.protocol}//${window.location.host}`,
|
||||
survey.environmentId
|
||||
`${window.location.protocol}//${window.location.host}`
|
||||
);
|
||||
if (displayId) {
|
||||
markDisplayResponded(
|
||||
@@ -166,8 +168,7 @@ export default function LinkSurvey({ survey }: LinkSurveyProps) {
|
||||
await updateResponse(
|
||||
responseRequest,
|
||||
responseId,
|
||||
`${window.location.protocol}//${window.location.host}`,
|
||||
survey.environmentId
|
||||
`${window.location.protocol}//${window.location.host}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
9
apps/web/lib/api/validator.ts
Normal file
9
apps/web/lib/api/validator.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ZodError } from "zod";
|
||||
|
||||
export const transformErrorToDetails = (error: ZodError<any>): { [key: string]: string } => {
|
||||
const details: { [key: string]: string } = {};
|
||||
for (const issue of error.issues) {
|
||||
details[issue.path.join(".")] = issue.message;
|
||||
}
|
||||
return details;
|
||||
};
|
||||
@@ -134,7 +134,10 @@ export const sendResponseFinishedEmail = async (
|
||||
<hr/>
|
||||
|
||||
${getQuestionResponseMapping(survey, response)
|
||||
.map((question) => `<p><strong>${question.question}</strong></p><p>${question.answer}</p>`)
|
||||
.map(
|
||||
(question) =>
|
||||
question.answer && `<p><strong>${question.question}</strong></p><p>${question.answer}</p>`
|
||||
)
|
||||
.join("")}
|
||||
|
||||
<hr/>
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"react-use": "^17.4.0",
|
||||
"stripe": "^12.6.0",
|
||||
"swr": "^2.1.5",
|
||||
"typescript": "5.0.4"
|
||||
"typescript": "5.0.4",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formbricks/database": "workspace:*",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"packages/*"
|
||||
],
|
||||
"prisma": {
|
||||
"schema": "packages/database/prisma/schema.prisma"
|
||||
"schema": "packages/database/schema.prisma"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "turbo run clean && rimraf node_modules",
|
||||
@@ -35,6 +35,5 @@
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"dependencies": {},
|
||||
"packageManager": "pnpm@8.1.1"
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../.env
|
||||
16
packages/database/jsonTypes.ts
Normal file
16
packages/database/jsonTypes.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { TEventClassNoCodeConfig } from "@formbricks/types/v1/eventClasses";
|
||||
import { TResponseData } from "@formbricks/types/v1/responses";
|
||||
import { TSurveyQuestions, TSurveyThankYouCard } from "@formbricks/types/v1/surveys";
|
||||
import { TUserNotificationSettings } from "@formbricks/types/v1/users";
|
||||
|
||||
declare global {
|
||||
namespace PrismaJson {
|
||||
export type EventProperties = { [key: string]: string };
|
||||
export type EventClassNoCodeConfig = TEventClassNoCodeConfig;
|
||||
export type ResponseData = TResponseData;
|
||||
export type ResponseMeta = { [key: string]: string };
|
||||
export type SurveyQuestions = TSurveyQuestions;
|
||||
export type SurveyThankYouCard = TSurveyThankYouCard;
|
||||
export type UserNotificationSettings = TUserNotificationSettings;
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,9 @@
|
||||
"dependencies": {
|
||||
"@formbricks/types": "workspace:*",
|
||||
"@prisma/client": "^4.15.0",
|
||||
"prisma-json-types-generator": "^2.4.0"
|
||||
"prisma-json-types-generator": "^2.4.0",
|
||||
"zod": "^3.21.4",
|
||||
"zod-prisma": "^0.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formbricks/tsconfig": "workspace:*",
|
||||
|
||||
@@ -12,14 +12,15 @@ generator client {
|
||||
//provider = "prisma-dbml-generator"
|
||||
}
|
||||
|
||||
generator zod {
|
||||
provider = "zod-prisma"
|
||||
output = "./zod"
|
||||
imports = "./zod-utils"
|
||||
relationModel = "default"
|
||||
}
|
||||
|
||||
generator json {
|
||||
provider = "prisma-json-types-generator"
|
||||
// namespace = "PrismaJson"
|
||||
// clientOutput = "<finds it automatically>"
|
||||
// (./ -> relative to schema, or an importable path to require() it)
|
||||
// useType = "MyType"
|
||||
// In case you need to use a type, export it inside the namespace and we will add a index signature to it
|
||||
// (e.g. export namespace PrismaJson { export type MyType = {a: 1, b: 2} }; will generate namespace.MyType["TYPE HERE"])
|
||||
}
|
||||
|
||||
enum PipelineTriggers {
|
||||
@@ -93,8 +94,10 @@ model Response {
|
||||
surveyId String
|
||||
person Person? @relation(fields: [personId], references: [id], onDelete: Cascade)
|
||||
personId String?
|
||||
/// @zod.custom(imports.ZResponseData)
|
||||
/// [ResponseData]
|
||||
data Json @default("{}")
|
||||
/// @zod.custom(imports.ZResponseMeta)
|
||||
/// [ResponseMeta]
|
||||
meta Json @default("{}")
|
||||
}
|
||||
@@ -176,8 +179,10 @@ model Survey {
|
||||
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
|
||||
environmentId String
|
||||
status SurveyStatus @default(draft)
|
||||
/// @zod.custom(imports.ZSurveyQuestions)
|
||||
/// [SurveyQuestions]
|
||||
questions Json @default("[]")
|
||||
/// @zod.custom(imports.ZSurveyThankYouCard)
|
||||
/// [SurveyThankYouCard]
|
||||
thankYouCard Json @default("{\"enabled\": false}")
|
||||
responses Response[]
|
||||
@@ -197,6 +202,7 @@ model Event {
|
||||
eventClassId String?
|
||||
session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
||||
sessionId String
|
||||
/// @zod.custom(imports.ZEventProperties)
|
||||
/// [EventProperties]
|
||||
properties Json @default("{}")
|
||||
}
|
||||
@@ -215,7 +221,7 @@ model EventClass {
|
||||
description String?
|
||||
type EventType
|
||||
events Event[]
|
||||
/// [EventClassNoCodeConfig]
|
||||
/// @zod.custom(imports.ZEventClassNoCodeConfig)
|
||||
noCodeConfig Json?
|
||||
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
|
||||
environmentId String
|
||||
@@ -400,6 +406,7 @@ model User {
|
||||
invitesAccepted Invite[] @relation("inviteAcceptedBy")
|
||||
role Role?
|
||||
objective Objective?
|
||||
/// @zod.custom(imports.ZUserNotificationSettings)
|
||||
/// [UserNotificationSettings]
|
||||
notificationSettings Json @default("{}")
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import "../types/jsonTypes";
|
||||
import "../jsonTypes";
|
||||
|
||||
declare global {
|
||||
var prisma: PrismaClient | undefined;
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { NoCodeConfig } from "@formbricks/types/events";
|
||||
import { Question } from "@formbricks/types/questions";
|
||||
import { ThankYouCard } from "@formbricks/types/surveys";
|
||||
import { NotificationSettings } from "@formbricks/types/users";
|
||||
|
||||
declare global {
|
||||
namespace PrismaJson {
|
||||
export type EventProperties = { [key: string]: string };
|
||||
export type EventClassNoCodeConfig = NoCodeConfig;
|
||||
export type ResponseData = { [questionId: string]: string };
|
||||
export type ResponseMeta = { [key: string]: string };
|
||||
export type SurveyQuestions = Question[];
|
||||
export type SurveyThankYouCard = ThankYouCard;
|
||||
export type UserNotificationSettings = NotificationSettings;
|
||||
}
|
||||
}
|
||||
11
packages/database/zod-utils.ts
Normal file
11
packages/database/zod-utils.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import z from "zod";
|
||||
|
||||
export const ZEventProperties = z.record(z.string());
|
||||
export { ZEventClassNoCodeConfig } from "@formbricks/types/v1/eventClasses";
|
||||
|
||||
export { ZResponseData } from "@formbricks/types/v1/responses";
|
||||
export const ZResponseMeta = z.record(z.union([z.string(), z.number()]));
|
||||
|
||||
export { ZSurveyQuestions, ZSurveyThankYouCard } from "@formbricks/types/v1/surveys";
|
||||
|
||||
export { ZUserNotificationSettings } from "@formbricks/types/v1/users";
|
||||
@@ -11,6 +11,7 @@ import Progress from "./Progress";
|
||||
import QuestionConditional from "./QuestionConditional";
|
||||
import ThankYouCard from "./ThankYouCard";
|
||||
import FormbricksSignature from "./FormbricksSignature";
|
||||
import type { TResponseInput } from "../../../types/v1/responses";
|
||||
|
||||
interface SurveyViewProps {
|
||||
config: JsConfig;
|
||||
@@ -168,10 +169,11 @@ export default function SurveyView({ config, survey, close, errorHandler }: Surv
|
||||
|
||||
const finished = nextQuestionId === "end";
|
||||
// build response
|
||||
const responseRequest = {
|
||||
const responseRequest: TResponseInput = {
|
||||
surveyId: survey.id,
|
||||
personId: config.person.id,
|
||||
response: { finished, data },
|
||||
finished,
|
||||
data,
|
||||
};
|
||||
if (!responseId) {
|
||||
const [response, _] = await Promise.all([
|
||||
@@ -185,7 +187,7 @@ export default function SurveyView({ config, survey, close, errorHandler }: Surv
|
||||
|
||||
if (result.ok !== true) {
|
||||
errorHandler(result.error);
|
||||
} else if (responseRequest.response.finished) {
|
||||
} else if (responseRequest.finished) {
|
||||
Logger.getInstance().debug("Submitted response");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import type { JsConfig, Response, ResponseCreateRequest, ResponseUpdateRequest } from "../../../types/js";
|
||||
import type { JsConfig, Response } from "../../../types/js";
|
||||
import type { TResponse, TResponseInput } from "../../../types/v1/responses";
|
||||
import { NetworkError, Result, err, ok } from "./errors";
|
||||
|
||||
export const createResponse = async (
|
||||
responseRequest: ResponseCreateRequest,
|
||||
responseInput: TResponseInput,
|
||||
config
|
||||
): Promise<Result<Response, NetworkError>> => {
|
||||
const url = `${config.apiHost}/api/v1/client/environments/${config.environmentId}/responses`;
|
||||
): Promise<Result<TResponse, NetworkError>> => {
|
||||
const url = `${config.apiHost}/api/v1/client/responses`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(responseRequest),
|
||||
body: JSON.stringify(responseInput),
|
||||
});
|
||||
|
||||
const jsonRes = await res.json();
|
||||
@@ -25,20 +26,20 @@ export const createResponse = async (
|
||||
});
|
||||
}
|
||||
|
||||
return ok(jsonRes as Response);
|
||||
return ok(jsonRes.data as TResponse);
|
||||
};
|
||||
|
||||
export const updateResponse = async (
|
||||
responseRequest: ResponseUpdateRequest,
|
||||
responseInput: TResponseInput,
|
||||
responseId: string,
|
||||
config: JsConfig
|
||||
): Promise<Result<Response, NetworkError>> => {
|
||||
const url = `${config.apiHost}/api/v1/client/environments/${config.environmentId}/responses/${responseId}`;
|
||||
): Promise<Result<TResponse, NetworkError>> => {
|
||||
const url = `${config.apiHost}/api/v1/client/responses/${responseId}`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(responseRequest),
|
||||
body: JSON.stringify(responseInput),
|
||||
});
|
||||
|
||||
const resJson = await res.json();
|
||||
@@ -53,5 +54,5 @@ export const updateResponse = async (
|
||||
});
|
||||
}
|
||||
|
||||
return ok(resJson as Response);
|
||||
return ok(resJson.data as TResponse);
|
||||
};
|
||||
|
||||
@@ -58,5 +58,5 @@
|
||||
"references": [
|
||||
{ "path": "../../../types/tsconfig.json" } // Add this line, adjust the path to the actual location
|
||||
],
|
||||
"include": ["src", "../types"]
|
||||
"include": ["src", "../types", "../lib/client"]
|
||||
}
|
||||
|
||||
32
packages/lib/client/response.ts
Normal file
32
packages/lib/client/response.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { TResponse, TResponseInput, TResponseUpdateInput } from "@formbricks/types/v1/responses";
|
||||
|
||||
export const createResponse = async (responseInput: TResponseInput, apiHost: string): Promise<TResponse> => {
|
||||
const res = await fetch(`${apiHost}/api/v1/client/responses`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(responseInput),
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error(res.text);
|
||||
throw new Error("Could not create response");
|
||||
}
|
||||
const resJson = await res.json();
|
||||
return resJson.data;
|
||||
};
|
||||
|
||||
export const updateResponse = async (
|
||||
responseInput: TResponseUpdateInput,
|
||||
responseId: string,
|
||||
apiHost: string
|
||||
): Promise<TResponse> => {
|
||||
const res = await fetch(`${apiHost}/api/v1/client/responses/${responseId}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(responseInput),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error("Could not update response");
|
||||
}
|
||||
const resJson = await res.json();
|
||||
return resJson.data;
|
||||
};
|
||||
@@ -1,65 +0,0 @@
|
||||
export interface ResponseCreateRequest {
|
||||
surveyId: string;
|
||||
personId?: string;
|
||||
response: {
|
||||
finished?: boolean;
|
||||
data: {
|
||||
[name: string]: string | number | string[] | number[] | undefined;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface ResponseUpdateRequest {
|
||||
response: {
|
||||
finished?: boolean;
|
||||
data: {
|
||||
[name: string]: string | number | string[] | number[] | undefined;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
organisationId: string;
|
||||
formId: string;
|
||||
customerId: string;
|
||||
data: {
|
||||
[name: string]: string | number | string[] | number[] | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export const createResponse = async (
|
||||
responseRequest: ResponseCreateRequest,
|
||||
apiHost: string,
|
||||
environmentId: string
|
||||
): Promise<Response> => {
|
||||
const res = await fetch(`${apiHost}/api/v1/client/environments/${environmentId}/responses`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(responseRequest),
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error(res.text);
|
||||
throw new Error("Could not create response");
|
||||
}
|
||||
return await res.json();
|
||||
};
|
||||
|
||||
export const updateResponse = async (
|
||||
responseRequest: ResponseUpdateRequest,
|
||||
responseId: string,
|
||||
apiHost: string,
|
||||
environmentId: string
|
||||
): Promise<Response> => {
|
||||
const res = await fetch(`${apiHost}/api/v1/client/environments/${environmentId}/responses/${responseId}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(responseRequest),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error("Could not update response");
|
||||
}
|
||||
return await res.json();
|
||||
};
|
||||
@@ -11,7 +11,9 @@
|
||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||
"lint:report": "eslint . --format json --output-file ../../lint-results/app-store.json"
|
||||
},
|
||||
"dependencies": {},
|
||||
"dependencies": {
|
||||
"@formbricks/database": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formbricks/tsconfig": "*",
|
||||
"@formbricks/types": "*",
|
||||
|
||||
56
packages/lib/services/person.ts
Normal file
56
packages/lib/services/person.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { TPerson } from "@formbricks/types/v1/people";
|
||||
import { prisma } from "@formbricks/database";
|
||||
|
||||
type TransformPersonInput = {
|
||||
id: string;
|
||||
attributes: {
|
||||
value: string;
|
||||
attributeClass: {
|
||||
name: string;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
|
||||
type TransformPersonOutput = {
|
||||
id: string;
|
||||
attributes: Record<string, string | number>;
|
||||
};
|
||||
|
||||
export const transformPrismaPerson = (person: TransformPersonInput | null): TransformPersonOutput | null => {
|
||||
if (person === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const attributes = person.attributes.reduce((acc, attr) => {
|
||||
acc[attr.attributeClass.name] = attr.value;
|
||||
return acc;
|
||||
}, {} as Record<string, string | number>);
|
||||
|
||||
return {
|
||||
id: person.id,
|
||||
attributes: attributes,
|
||||
};
|
||||
};
|
||||
|
||||
export const getPerson = async (personId: string): Promise<TPerson | null> => {
|
||||
const personPrisma = await prisma.person.findUnique({
|
||||
where: {
|
||||
id: personId,
|
||||
},
|
||||
include: {
|
||||
attributes: {
|
||||
include: {
|
||||
attributeClass: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const person = transformPrismaPerson(personPrisma);
|
||||
|
||||
return person;
|
||||
};
|
||||
159
packages/lib/services/response.ts
Normal file
159
packages/lib/services/response.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { TResponse, TResponseInput, TResponseUpdateInput } from "@formbricks/types/v1/responses";
|
||||
import { transformPrismaPerson } from "./person";
|
||||
|
||||
export const createResponse = async (responseInput: TResponseInput): Promise<TResponse> => {
|
||||
const responsePrisma = await prisma.response.create({
|
||||
data: {
|
||||
survey: {
|
||||
connect: {
|
||||
id: responseInput.surveyId,
|
||||
},
|
||||
},
|
||||
finished: responseInput.finished,
|
||||
data: responseInput.data,
|
||||
...(responseInput.personId && {
|
||||
person: {
|
||||
connect: {
|
||||
id: responseInput.personId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
surveyId: true,
|
||||
finished: true,
|
||||
data: true,
|
||||
person: {
|
||||
select: {
|
||||
id: true,
|
||||
attributes: {
|
||||
select: {
|
||||
value: true,
|
||||
attributeClass: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const response: TResponse = {
|
||||
...responsePrisma,
|
||||
createdAt: responsePrisma.createdAt.toISOString(),
|
||||
updatedAt: responsePrisma.updatedAt.toISOString(),
|
||||
person: transformPrismaPerson(responsePrisma.person),
|
||||
};
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
export const getResponse = async (responseId: string): Promise<TResponse | null> => {
|
||||
const responsePrisma = await prisma.response.findUnique({
|
||||
where: {
|
||||
id: responseId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
surveyId: true,
|
||||
finished: true,
|
||||
data: true,
|
||||
person: {
|
||||
select: {
|
||||
id: true,
|
||||
attributes: {
|
||||
select: {
|
||||
value: true,
|
||||
attributeClass: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!responsePrisma) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const response: TResponse = {
|
||||
...responsePrisma,
|
||||
createdAt: responsePrisma.createdAt.toISOString(),
|
||||
updatedAt: responsePrisma.updatedAt.toISOString(),
|
||||
person: transformPrismaPerson(responsePrisma.person),
|
||||
};
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
export const updateResponse = async (
|
||||
responseId: string,
|
||||
responseInput: TResponseUpdateInput
|
||||
): Promise<TResponse> => {
|
||||
const currentResponse = await getResponse(responseId);
|
||||
|
||||
if (!currentResponse) {
|
||||
throw new Error("Response not found");
|
||||
}
|
||||
|
||||
// merge data object
|
||||
const data = {
|
||||
...currentResponse.data,
|
||||
...responseInput.data,
|
||||
};
|
||||
|
||||
const responsePrisma = await prisma.response.update({
|
||||
where: {
|
||||
id: responseId,
|
||||
},
|
||||
data: {
|
||||
finished: responseInput.finished,
|
||||
data,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
surveyId: true,
|
||||
finished: true,
|
||||
data: true,
|
||||
person: {
|
||||
select: {
|
||||
id: true,
|
||||
attributes: {
|
||||
select: {
|
||||
value: true,
|
||||
attributeClass: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const response: TResponse = {
|
||||
...responsePrisma,
|
||||
createdAt: responsePrisma.createdAt.toISOString(),
|
||||
updatedAt: responsePrisma.updatedAt.toISOString(),
|
||||
person: transformPrismaPerson(responsePrisma.person),
|
||||
};
|
||||
|
||||
return response;
|
||||
};
|
||||
68
packages/lib/services/survey.ts
Normal file
68
packages/lib/services/survey.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { TSurvey, ZSurvey } from "@formbricks/types/v1/surveys";
|
||||
|
||||
export const getSurvey = async (surveyId: string): Promise<TSurvey | null> => {
|
||||
const surveyPrisma = await prisma.survey.findUnique({
|
||||
where: {
|
||||
id: surveyId,
|
||||
},
|
||||
include: {
|
||||
triggers: {
|
||||
select: {
|
||||
eventClass: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
description: true,
|
||||
type: true,
|
||||
noCodeConfig: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
attributeFilters: {
|
||||
select: {
|
||||
id: true,
|
||||
attributeClassId: true,
|
||||
condition: true,
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!surveyPrisma) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const numDisplays = await prisma.display.count({
|
||||
where: {
|
||||
surveyId,
|
||||
},
|
||||
});
|
||||
|
||||
const numDisplaysResponded = await prisma.display.count({
|
||||
where: {
|
||||
surveyId,
|
||||
status: "responded",
|
||||
},
|
||||
});
|
||||
|
||||
// responseRate, rounded to 2 decimal places
|
||||
const responseRate = Math.round((numDisplaysResponded / numDisplays) * 100) / 100;
|
||||
|
||||
const transformedSurvey = {
|
||||
...surveyPrisma,
|
||||
createdAt: surveyPrisma.createdAt.toISOString(),
|
||||
updatedAt: surveyPrisma.updatedAt.toISOString(),
|
||||
triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass),
|
||||
analytics: {
|
||||
numDisplays,
|
||||
responseRate,
|
||||
},
|
||||
};
|
||||
|
||||
const survey = ZSurvey.parse(transformedSurvey);
|
||||
|
||||
return survey;
|
||||
};
|
||||
@@ -9,5 +9,8 @@
|
||||
"devDependencies": {
|
||||
"@formbricks/tsconfig": "workspace:*",
|
||||
"rimraf": "^5.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.21.4"
|
||||
}
|
||||
}
|
||||
|
||||
32
packages/types/v1/eventClasses.ts
Normal file
32
packages/types/v1/eventClasses.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ZEventClassNoCodeConfig = z.object({
|
||||
type: z.union([z.literal("innerHtml"), z.literal("pageUrl"), z.literal("cssSelector")]),
|
||||
pageUrl: z.optional(
|
||||
z.object({
|
||||
value: z.string(),
|
||||
rule: z.union([
|
||||
z.literal("exactMatch"),
|
||||
z.literal("contains"),
|
||||
z.literal("startsWith"),
|
||||
z.literal("endsWith"),
|
||||
z.literal("notMatch"),
|
||||
z.literal("notContains"),
|
||||
]),
|
||||
})
|
||||
),
|
||||
innerHtml: z.optional(z.object({ value: z.string() })),
|
||||
cssSelector: z.optional(z.object({ value: z.string() })),
|
||||
});
|
||||
|
||||
export type TEventClassNoCodeConfig = z.infer<typeof ZEventClassNoCodeConfig>;
|
||||
|
||||
export const ZEventClass = z.object({
|
||||
id: z.string().cuid2(),
|
||||
name: z.string(),
|
||||
description: z.union([z.string(), z.null()]),
|
||||
noCodeConfig: z.union([ZEventClassNoCodeConfig, z.null()]),
|
||||
type: z.enum(["code", "noCode", "automatic"]),
|
||||
});
|
||||
|
||||
export type TEventClass = z.infer<typeof ZEventClass>;
|
||||
8
packages/types/v1/people.ts
Normal file
8
packages/types/v1/people.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import z from "zod";
|
||||
|
||||
export const ZPerson = z.object({
|
||||
id: z.string().cuid2(),
|
||||
attributes: z.record(z.union([z.string(), z.number()])),
|
||||
});
|
||||
|
||||
export type TPerson = z.infer<typeof ZPerson>;
|
||||
38
packages/types/v1/responses.ts
Normal file
38
packages/types/v1/responses.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ZResponseData = z.record(z.union([z.string(), z.number()]));
|
||||
|
||||
export type TResponseData = z.infer<typeof ZResponseData>;
|
||||
|
||||
const ZResponse = z.object({
|
||||
id: z.string().cuid2(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
surveyId: z.string().cuid2(),
|
||||
person: z
|
||||
.object({
|
||||
id: z.string().cuid2(),
|
||||
attributes: z.record(z.union([z.string(), z.number()])),
|
||||
})
|
||||
.nullable(),
|
||||
finished: z.boolean(),
|
||||
data: ZResponseData,
|
||||
});
|
||||
|
||||
export type TResponse = z.infer<typeof ZResponse>;
|
||||
|
||||
export const ZResponseInput = z.object({
|
||||
surveyId: z.string().cuid2(),
|
||||
personId: z.string().cuid2().nullable(),
|
||||
finished: z.boolean(),
|
||||
data: ZResponseData,
|
||||
});
|
||||
|
||||
export type TResponseInput = z.infer<typeof ZResponseInput>;
|
||||
|
||||
export const ZResponseUpdateInput = z.object({
|
||||
finished: z.boolean(),
|
||||
data: ZResponseData,
|
||||
});
|
||||
|
||||
export type TResponseUpdateInput = z.infer<typeof ZResponseUpdateInput>;
|
||||
211
packages/types/v1/surveys.ts
Normal file
211
packages/types/v1/surveys.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { z } from "zod";
|
||||
import { ZEventClass } from "./eventClasses";
|
||||
|
||||
export const ZSurveyThankYouCard = z.object({
|
||||
enabled: z.boolean(),
|
||||
headline: z.optional(z.string()),
|
||||
subheader: z.optional(z.string()),
|
||||
});
|
||||
|
||||
export type TSurveyThankYouCard = z.infer<typeof ZSurveyThankYouCard>;
|
||||
|
||||
export const ZSurveyChoice = z.object({
|
||||
id: z.string(),
|
||||
label: z.string(),
|
||||
});
|
||||
|
||||
export const ZSurveyLogicCondition = z.union([
|
||||
z.literal("submitted"),
|
||||
z.literal("skipped"),
|
||||
z.literal("equals"),
|
||||
z.literal("notEquals"),
|
||||
z.literal("lessThan"),
|
||||
z.literal("lessEqual"),
|
||||
z.literal("greaterThan"),
|
||||
z.literal("greaterEqual"),
|
||||
z.literal("includesAll"),
|
||||
z.literal("includesOne"),
|
||||
]);
|
||||
|
||||
export const ZSurveyLogicBase = z.object({
|
||||
condition: ZSurveyLogicCondition.optional(),
|
||||
value: z.union([z.number(), z.string(), z.array(z.string())]).optional(),
|
||||
destination: z.union([z.string(), z.literal("end")]).optional(),
|
||||
});
|
||||
|
||||
export const ZSurveyOpenTextLogic = ZSurveyLogicBase.extend({
|
||||
condition: z.union([z.literal("submitted"), z.literal("skipped")]).optional(),
|
||||
value: z.undefined(),
|
||||
});
|
||||
|
||||
export const ZSurveyConsentLogic = ZSurveyLogicBase.extend({
|
||||
condition: z.union([z.literal("submitted"), z.literal("skipped"), z.literal("accepted")]).optional(),
|
||||
value: z.undefined(),
|
||||
});
|
||||
|
||||
export const ZSurveyMultipleChoiceSingleLogic = ZSurveyLogicBase.extend({
|
||||
condition: z
|
||||
.union([z.literal("submitted"), z.literal("skipped"), z.literal("equals"), z.literal("notEquals")])
|
||||
.optional(),
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
export const ZSurveyMultipleChoiceMultiLogic = ZSurveyLogicBase.extend({
|
||||
condition: z
|
||||
.union([z.literal("submitted"), z.literal("skipped"), z.literal("includesAll"), z.literal("includesOne")])
|
||||
.optional(),
|
||||
value: z.array(z.string()),
|
||||
});
|
||||
|
||||
export const ZSurveyNPSLogic = ZSurveyLogicBase.extend({
|
||||
condition: z
|
||||
.union([
|
||||
z.literal("submitted"),
|
||||
z.literal("skipped"),
|
||||
z.literal("lessThan"),
|
||||
z.literal("lessEqual"),
|
||||
z.literal("greaterThan"),
|
||||
z.literal("greaterEqual"),
|
||||
z.literal("equals"),
|
||||
z.literal("notEquals"),
|
||||
])
|
||||
.optional(),
|
||||
value: z.number(),
|
||||
});
|
||||
|
||||
const ZSurveyCTALogic = ZSurveyLogicBase.extend({
|
||||
condition: z.union([z.literal("submitted"), z.literal("skipped")]).optional(),
|
||||
value: z.undefined(),
|
||||
});
|
||||
|
||||
const ZSurveyRatingLogic = ZSurveyLogicBase.extend({
|
||||
condition: z
|
||||
.union([
|
||||
z.literal("submitted"),
|
||||
z.literal("skipped"),
|
||||
z.literal("lessThan"),
|
||||
z.literal("lessEqual"),
|
||||
z.literal("greaterThan"),
|
||||
z.literal("greaterEqual"),
|
||||
z.literal("equals"),
|
||||
z.literal("notEquals"),
|
||||
])
|
||||
.optional(),
|
||||
value: z.number(),
|
||||
});
|
||||
|
||||
export const ZSurveyLogic = z.union([
|
||||
ZSurveyOpenTextLogic,
|
||||
ZSurveyConsentLogic,
|
||||
ZSurveyMultipleChoiceSingleLogic,
|
||||
ZSurveyMultipleChoiceMultiLogic,
|
||||
ZSurveyNPSLogic,
|
||||
ZSurveyCTALogic,
|
||||
ZSurveyRatingLogic,
|
||||
]);
|
||||
|
||||
const ZSurveyQuestionBase = z.object({
|
||||
id: z.string(),
|
||||
type: z.string(),
|
||||
headline: z.string(),
|
||||
subheader: z.string().optional(),
|
||||
required: z.boolean(),
|
||||
buttonLabel: z.string().optional(),
|
||||
logic: z.array(ZSurveyLogic).optional(),
|
||||
});
|
||||
|
||||
export const ZSurveyOpenTextQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal("openText"),
|
||||
placeholder: z.string().optional(),
|
||||
});
|
||||
|
||||
export const ZSurveyConsentQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal("consent"),
|
||||
placeholder: z.string().optional(),
|
||||
});
|
||||
|
||||
export const ZSurveyMultipleChoiceSingleQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal("multipleChoiceSingle"),
|
||||
choices: z.array(ZSurveyChoice),
|
||||
});
|
||||
|
||||
export const ZSurveyMultipleChoiceMultiQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal("multipleChoiceMulti"),
|
||||
choices: z.array(ZSurveyChoice),
|
||||
});
|
||||
|
||||
export const ZSurveyNPSQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal("nps"),
|
||||
lowerLabel: z.string(),
|
||||
upperLabel: z.string(),
|
||||
});
|
||||
|
||||
export const ZSurveyCTAQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal("cta"),
|
||||
html: z.string().optional(),
|
||||
buttonUrl: z.string().optional(),
|
||||
buttonExternal: z.boolean(),
|
||||
dismissButtonLabel: z.string().optional(),
|
||||
});
|
||||
|
||||
export const ZSurveyRatingQuestion = ZSurveyQuestionBase.extend({
|
||||
type: z.literal("rating"),
|
||||
scale: z.union([z.literal("number"), z.literal("smiley"), z.literal("star")]),
|
||||
range: z.union([z.literal(5), z.literal(3), z.literal(4), z.literal(7), z.literal(10)]),
|
||||
lowerLabel: z.string(),
|
||||
upperLabel: z.string(),
|
||||
});
|
||||
|
||||
export const ZSurveyQuestion = z.union([
|
||||
ZSurveyOpenTextQuestion,
|
||||
ZSurveyConsentQuestion,
|
||||
ZSurveyMultipleChoiceSingleQuestion,
|
||||
ZSurveyMultipleChoiceMultiQuestion,
|
||||
ZSurveyNPSQuestion,
|
||||
ZSurveyCTAQuestion,
|
||||
ZSurveyRatingQuestion,
|
||||
]);
|
||||
|
||||
export const ZSurveyQuestions = z.array(ZSurveyQuestion);
|
||||
|
||||
export type TSurveyQuestions = z.infer<typeof ZSurveyQuestions>;
|
||||
|
||||
export const ZSurveyAttributeFilter = z.object({
|
||||
id: z.string().cuid2(),
|
||||
attributeClassId: z.string(),
|
||||
condition: z.enum(["equals", "notEquals"]),
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
export const ZSurvey = z.object({
|
||||
id: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
name: z.string(),
|
||||
type: z.union([z.literal("web"), z.literal("email"), z.literal("link"), z.literal("mobile")]),
|
||||
environmentId: z.string(),
|
||||
status: z.union([
|
||||
z.literal("draft"),
|
||||
z.literal("inProgress"),
|
||||
z.literal("archived"),
|
||||
z.literal("paused"),
|
||||
z.literal("completed"),
|
||||
]),
|
||||
attributeFilters: z.array(ZSurveyAttributeFilter),
|
||||
displayOption: z.union([
|
||||
z.literal("displayOnce"),
|
||||
z.literal("displayMultiple"),
|
||||
z.literal("respondMultiple"),
|
||||
]),
|
||||
autoClose: z.union([z.number(), z.null()]),
|
||||
triggers: z.array(ZEventClass),
|
||||
recontactDays: z.union([z.number(), z.null()]),
|
||||
questions: ZSurveyQuestions,
|
||||
thankYouCard: ZSurveyThankYouCard,
|
||||
analytics: z.object({
|
||||
numDisplays: z.number(),
|
||||
responseRate: z.number(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type TSurvey = z.infer<typeof ZSurvey>;
|
||||
10
packages/types/v1/users.ts
Normal file
10
packages/types/v1/users.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ZUserNotificationSettings = z.record(
|
||||
z.object({
|
||||
responseFinished: z.boolean(),
|
||||
weeklySummary: z.boolean(),
|
||||
})
|
||||
);
|
||||
|
||||
export type TUserNotificationSettings = z.infer<typeof ZUserNotificationSettings>;
|
||||
132
pnpm-lock.yaml
generated
132
pnpm-lock.yaml
generated
@@ -18,7 +18,7 @@ importers:
|
||||
version: 3.12.6
|
||||
turbo:
|
||||
specifier: latest
|
||||
version: 1.8.8
|
||||
version: 1.10.3
|
||||
|
||||
apps/demo:
|
||||
dependencies:
|
||||
@@ -306,6 +306,9 @@ importers:
|
||||
typescript:
|
||||
specifier: 5.0.4
|
||||
version: 5.0.4
|
||||
zod:
|
||||
specifier: ^3.21.4
|
||||
version: 3.21.4
|
||||
devDependencies:
|
||||
'@formbricks/database':
|
||||
specifier: workspace:*
|
||||
@@ -386,6 +389,12 @@ importers:
|
||||
prisma-json-types-generator:
|
||||
specifier: ^2.4.0
|
||||
version: 2.4.0
|
||||
zod:
|
||||
specifier: ^3.21.4
|
||||
version: 3.21.4
|
||||
zod-prisma:
|
||||
specifier: ^0.5.4
|
||||
version: 0.5.4(prisma@4.15.0)(zod@3.21.4)
|
||||
devDependencies:
|
||||
'@formbricks/tsconfig':
|
||||
specifier: workspace:*
|
||||
@@ -556,6 +565,10 @@ importers:
|
||||
version: 5.1.3
|
||||
|
||||
packages/lib:
|
||||
dependencies:
|
||||
'@formbricks/database':
|
||||
specifier: '*'
|
||||
version: link:../database
|
||||
devDependencies:
|
||||
'@formbricks/tsconfig':
|
||||
specifier: '*'
|
||||
@@ -590,6 +603,10 @@ importers:
|
||||
packages/tsconfig: {}
|
||||
|
||||
packages/types:
|
||||
dependencies:
|
||||
zod:
|
||||
specifier: ^3.21.4
|
||||
version: 3.21.4
|
||||
devDependencies:
|
||||
'@formbricks/tsconfig':
|
||||
specifier: workspace:*
|
||||
@@ -4092,6 +4109,14 @@ packages:
|
||||
prisma: 4.15.0
|
||||
dev: false
|
||||
|
||||
/@prisma/debug@3.8.1:
|
||||
resolution: {integrity: sha512-ft4VPTYME1UBJ7trfrBuF2w9jX1ipDy786T9fAEskNGb+y26gPDqz5fiEWc2kgHNeVdz/qTI/V3wXILRyEcgxQ==}
|
||||
dependencies:
|
||||
'@types/debug': 4.1.7
|
||||
ms: 2.1.3
|
||||
strip-ansi: 6.0.1
|
||||
dev: false
|
||||
|
||||
/@prisma/debug@4.15.0:
|
||||
resolution: {integrity: sha512-dkbPz+gOVlWDBAaOEseSpAUz9NppT38UlwdryPyrwct6OClLirNC7wH+TpAQk5OZp9x59hNnfDz+T7XvL1v0/Q==}
|
||||
dependencies:
|
||||
@@ -4171,6 +4196,15 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@prisma/generator-helper@3.8.1:
|
||||
resolution: {integrity: sha512-3zSy+XTEjmjLj6NO+/YPN1Cu7or3xA11TOoOnLRJ9G4pTT67RJXjK0L9Xy5n+3I0Xlb7xrWCgo8MvQQLMWzxPA==}
|
||||
dependencies:
|
||||
'@prisma/debug': 3.8.1
|
||||
'@types/cross-spawn': 6.0.2
|
||||
chalk: 4.1.2
|
||||
cross-spawn: 7.0.3
|
||||
dev: false
|
||||
|
||||
/@prisma/generator-helper@4.15.0:
|
||||
resolution: {integrity: sha512-JVHNgXr0LrcqXqmFrs+BzxfyRL6cFD5GLTMVWfCLU7kqSJdWuZxfoZW995tg6mOXnBgPTf6Ocv3RY4RLQq8k4g==}
|
||||
dependencies:
|
||||
@@ -5491,7 +5525,7 @@ packages:
|
||||
/@swc/helpers@0.4.14:
|
||||
resolution: {integrity: sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==}
|
||||
dependencies:
|
||||
tslib: 2.4.1
|
||||
tslib: 2.5.2
|
||||
dev: false
|
||||
|
||||
/@swc/helpers@0.5.1:
|
||||
@@ -5558,6 +5592,15 @@ packages:
|
||||
engines: {node: '>=10.13.0'}
|
||||
dev: true
|
||||
|
||||
/@ts-morph/common@0.12.3:
|
||||
resolution: {integrity: sha512-4tUmeLyXJnJWvTFOKtcNJ1yh0a3SsTLi2MUoyj8iUNznFRN1ZquaNe7Oukqrnki2FzZkm0J9adCNLDZxUzvj+w==}
|
||||
dependencies:
|
||||
fast-glob: 3.2.12
|
||||
minimatch: 3.1.2
|
||||
mkdirp: 1.0.4
|
||||
path-browserify: 1.0.1
|
||||
dev: false
|
||||
|
||||
/@types/acorn@4.0.6:
|
||||
resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==}
|
||||
dependencies:
|
||||
@@ -6900,7 +6943,7 @@ packages:
|
||||
resolution: {integrity: sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
tslib: 2.4.1
|
||||
tslib: 2.5.2
|
||||
dev: false
|
||||
|
||||
/aria-query@4.2.2:
|
||||
@@ -8265,6 +8308,10 @@ packages:
|
||||
q: 1.5.1
|
||||
dev: true
|
||||
|
||||
/code-block-writer@11.0.3:
|
||||
resolution: {integrity: sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==}
|
||||
dev: false
|
||||
|
||||
/collect-v8-coverage@1.0.1:
|
||||
resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==}
|
||||
dev: true
|
||||
@@ -14526,7 +14573,7 @@ packages:
|
||||
/micromark@3.1.0:
|
||||
resolution: {integrity: sha512-6Mj0yHLdUZjHnOPgr5xfWIMqMWS12zDN6iws9SLuSz76W8jTtAv24MN4/CL7gJrl5vtxGInkkqDv/JIoRsQOvA==}
|
||||
dependencies:
|
||||
'@types/debug': 4.1.7
|
||||
'@types/debug': 4.1.8
|
||||
debug: 4.3.4
|
||||
decode-named-character-reference: 1.0.2
|
||||
micromark-core-commonmark: 1.0.6
|
||||
@@ -14784,7 +14831,6 @@ packages:
|
||||
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/moo@0.5.2:
|
||||
resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==}
|
||||
@@ -15743,6 +15789,10 @@ packages:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
|
||||
/parenthesis@3.1.8:
|
||||
resolution: {integrity: sha512-KF/U8tk54BgQewkJPvB4s/US3VQY68BRDpH638+7O/n58TpnwiwnOtGIOsT2/i+M78s61BBpeC83STB88d8sqw==}
|
||||
dev: false
|
||||
|
||||
/parse-asn1@5.1.6:
|
||||
resolution: {integrity: sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==}
|
||||
dependencies:
|
||||
@@ -15851,6 +15901,10 @@ packages:
|
||||
resolution: {integrity: sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==}
|
||||
dev: true
|
||||
|
||||
/path-browserify@1.0.1:
|
||||
resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==}
|
||||
dev: false
|
||||
|
||||
/path-dirname@1.0.2:
|
||||
resolution: {integrity: sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==}
|
||||
dev: true
|
||||
@@ -19404,7 +19458,7 @@ packages:
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
dependencies:
|
||||
'@pkgr/utils': 2.3.1
|
||||
tslib: 2.4.1
|
||||
tslib: 2.5.2
|
||||
dev: false
|
||||
|
||||
/tailwind-merge@1.12.0:
|
||||
@@ -19912,6 +19966,13 @@ packages:
|
||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||
dev: true
|
||||
|
||||
/ts-morph@13.0.3:
|
||||
resolution: {integrity: sha512-pSOfUMx8Ld/WUreoSzvMFQG5i9uEiWIsBYjpU9+TTASOeUa89j5HykomeqVULm1oqWtBdleI3KEFRLrlA3zGIw==}
|
||||
dependencies:
|
||||
'@ts-morph/common': 0.12.3
|
||||
code-block-writer: 11.0.3
|
||||
dev: false
|
||||
|
||||
/ts-pattern@4.0.6:
|
||||
resolution: {integrity: sha512-sFHQYD4KoysBi7e7a2mzDPvRBeqA4w+vEyRE+P5MU9VLq8eEYxgKCgD9RNEAT+itGRWUTYN+hry94GDPLb1/Yw==}
|
||||
dev: true
|
||||
@@ -20047,65 +20108,65 @@ packages:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
/turbo-darwin-64@1.8.8:
|
||||
resolution: {integrity: sha512-18cSeIm7aeEvIxGyq7PVoFyEnPpWDM/0CpZvXKHpQ6qMTkfNt517qVqUTAwsIYqNS8xazcKAqkNbvU1V49n65Q==}
|
||||
/turbo-darwin-64@1.10.3:
|
||||
resolution: {integrity: sha512-IIB9IomJGyD3EdpSscm7Ip1xVWtYb7D0x7oH3vad3gjFcjHJzDz9xZ/iw/qItFEW+wGFcLSRPd+1BNnuLM8AsA==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/turbo-darwin-arm64@1.8.8:
|
||||
resolution: {integrity: sha512-ruGRI9nHxojIGLQv1TPgN7ud4HO4V8mFBwSgO6oDoZTNuk5ybWybItGR+yu6fni5vJoyMHXOYA2srnxvOc7hjQ==}
|
||||
/turbo-darwin-arm64@1.10.3:
|
||||
resolution: {integrity: sha512-SBNmOZU9YEB0eyNIxeeQ+Wi0Ufd+nprEVp41rgUSRXEIpXjsDjyBnKnF+sQQj3+FLb4yyi/yZQckB+55qXWEsw==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/turbo-linux-64@1.8.8:
|
||||
resolution: {integrity: sha512-N/GkHTHeIQogXB1/6ZWfxHx+ubYeb8Jlq3b/3jnU4zLucpZzTQ8XkXIAfJG/TL3Q7ON7xQ8yGOyGLhHL7MpFRg==}
|
||||
/turbo-linux-64@1.10.3:
|
||||
resolution: {integrity: sha512-kvAisGKE7xHJdyMxZLvg53zvHxjqPK1UVj4757PQqtx9dnjYHSc8epmivE6niPgDHon5YqImzArCjVZJYpIGHQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/turbo-linux-arm64@1.8.8:
|
||||
resolution: {integrity: sha512-hKqLbBHgUkYf2Ww8uBL9UYdBFQ5677a7QXdsFhONXoACbDUPvpK4BKlz3NN7G4NZ+g9dGju+OJJjQP0VXRHb5w==}
|
||||
/turbo-linux-arm64@1.10.3:
|
||||
resolution: {integrity: sha512-Qgaqln0IYRgyL0SowJOi+PNxejv1I2xhzXOI+D+z4YHbgSx87ox1IsALYBlK8VRVYY8VCXl+PN12r1ioV09j7A==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/turbo-windows-64@1.8.8:
|
||||
resolution: {integrity: sha512-2ndjDJyzkNslXxLt+PQuU21AHJWc8f6MnLypXy3KsN4EyX/uKKGZS0QJWz27PeHg0JS75PVvhfFV+L9t9i+Yyg==}
|
||||
/turbo-windows-64@1.10.3:
|
||||
resolution: {integrity: sha512-rbH9wManURNN8mBnN/ZdkpUuTvyVVEMiUwFUX4GVE5qmV15iHtZfDLUSGGCP2UFBazHcpNHG1OJzgc55GFFrUw==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/turbo-windows-arm64@1.8.8:
|
||||
resolution: {integrity: sha512-xCA3oxgmW9OMqpI34AAmKfOVsfDljhD5YBwgs0ZDsn5h3kCHhC4x9W5dDk1oyQ4F5EXSH3xVym5/xl1J6WRpUg==}
|
||||
/turbo-windows-arm64@1.10.3:
|
||||
resolution: {integrity: sha512-ThlkqxhcGZX39CaTjsHqJnqVe+WImjX13pmjnpChz6q5HHbeRxaJSFzgrHIOt0sUUVx90W/WrNRyoIt/aafniw==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/turbo@1.8.8:
|
||||
resolution: {integrity: sha512-qYJ5NjoTX+591/x09KgsDOPVDUJfU9GoS+6jszQQlLp1AHrf1wRFA3Yps8U+/HTG03q0M4qouOfOLtRQP4QypA==}
|
||||
/turbo@1.10.3:
|
||||
resolution: {integrity: sha512-U4gKCWcKgLcCjQd4Pl8KJdfEKumpyWbzRu75A6FCj6Ctea1PIm58W6Ltw1QXKqHrl2pF9e1raAskf/h6dlrPCA==}
|
||||
hasBin: true
|
||||
requiresBuild: true
|
||||
optionalDependencies:
|
||||
turbo-darwin-64: 1.8.8
|
||||
turbo-darwin-arm64: 1.8.8
|
||||
turbo-linux-64: 1.8.8
|
||||
turbo-linux-arm64: 1.8.8
|
||||
turbo-windows-64: 1.8.8
|
||||
turbo-windows-arm64: 1.8.8
|
||||
turbo-darwin-64: 1.10.3
|
||||
turbo-darwin-arm64: 1.10.3
|
||||
turbo-linux-64: 1.10.3
|
||||
turbo-linux-arm64: 1.10.3
|
||||
turbo-windows-64: 1.10.3
|
||||
turbo-windows-arm64: 1.10.3
|
||||
dev: true
|
||||
|
||||
/tween-functions@1.2.0:
|
||||
@@ -21550,6 +21611,25 @@ packages:
|
||||
readable-stream: 3.6.0
|
||||
dev: true
|
||||
|
||||
/zod-prisma@0.5.4(prisma@4.15.0)(zod@3.21.4):
|
||||
resolution: {integrity: sha512-5Ca4Qd1a1jy1T/NqCEpbr0c+EsbjJfJ/7euEHob3zDvtUK2rTuD1Rc/vfzH8q8PtaR2TZbysD88NHmrLwpv3Xg==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
decimal.js: ^10.0.0
|
||||
prisma: ^3.0.0
|
||||
zod: ^3.0.0
|
||||
peerDependenciesMeta:
|
||||
decimal.js:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@prisma/generator-helper': 3.8.1
|
||||
parenthesis: 3.1.8
|
||||
prisma: 4.15.0
|
||||
ts-morph: 13.0.3
|
||||
zod: 3.21.4
|
||||
dev: false
|
||||
|
||||
/zod@3.21.4:
|
||||
resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==}
|
||||
dev: false
|
||||
|
||||
Reference in New Issue
Block a user