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:
Matti Nannt
2023-06-12 19:51:13 +02:00
committed by GitHub
parent 85c3069155
commit 97263a66cc
55 changed files with 905 additions and 247 deletions

3
.gitignore vendored
View File

@@ -34,6 +34,9 @@ yarn-error.log*
.env.production.local
!packages/database/.env
# Prisma generated files
packages/database/zod
# turbo
.turbo

View File

@@ -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:

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -138,6 +138,7 @@ export default function SurveyMenuBar({
<Button
disabled={
localSurvey.type === "web" &&
localSurvey.triggers &&
(localSurvey.triggers[0] === "" || localSurvey.triggers.length === 0)
}
variant="darkCTA"

View File

@@ -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}`
);
}

View 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;
};

View File

@@ -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/>

View File

@@ -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:*",

View File

@@ -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"
}

View File

@@ -1 +0,0 @@
../../.env

View 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;
}
}

View File

@@ -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:*",

View File

@@ -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("{}")
}

View File

@@ -1,5 +1,5 @@
import { PrismaClient } from "@prisma/client";
import "../types/jsonTypes";
import "../jsonTypes";
declare global {
var prisma: PrismaClient | undefined;

View File

@@ -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;
}
}

View 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";

View File

@@ -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");
}
}

View File

@@ -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);
};

View File

@@ -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"]
}

View 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;
};

View File

@@ -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();
};

View File

@@ -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": "*",

View 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;
};

View 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;
};

View 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;
};

View File

@@ -9,5 +9,8 @@
"devDependencies": {
"@formbricks/tsconfig": "workspace:*",
"rimraf": "^5.0.1"
},
"dependencies": {
"zod": "^3.21.4"
}
}

View 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>;

View 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>;

View 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>;

View 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>;

View 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
View File

@@ -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