Merge pull request #375 from formbricks/feature/FOR-772

Add Data Service for Responses, Webhooks, Surveys & Api Keys
This commit is contained in:
Johannes
2023-06-15 08:39:02 -05:00
committed by GitHub
23 changed files with 645 additions and 331 deletions

View File

@@ -1,7 +1,8 @@
import { responses } from "@/lib/api/response";
import { transformErrorToDetails } from "@/lib/api/validator";
import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
import { getResponse, updateResponse } from "@formbricks/lib/services/response";
import { sendToPipeline } from "@/lib/pipelines";
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/errors";
import { 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";
@@ -27,55 +28,54 @@ export async function PUT(
);
}
// get current response
const currentResponse = await getResponse(responseId);
if (!currentResponse) {
return responses.notFoundResponse("Response", responseId, true);
// update response
let response;
try {
response = await updateResponse(responseId, inputValidation.data);
} catch (error) {
if (error instanceof ResourceNotFoundError) {
return responses.notFoundResponse("Response", responseId, true);
}
if (error instanceof InvalidInputError) {
return responses.badRequestResponse(error.message);
}
if (error instanceof DatabaseError) {
return responses.internalServerErrorResponse(error.message);
}
}
// 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);
let survey;
try {
survey = await getSurvey(response.surveyId);
} catch (error) {
if (error instanceof InvalidInputError) {
return responses.badRequestResponse(error.message);
}
if (error instanceof DatabaseError) {
return responses.internalServerErrorResponse(error.message);
}
}
const environmentId = survey.environmentId;
// update response
const response = await updateResponse(responseId, responseUpdate);
// send response update to pipeline
// don't await to not block the response
fetch(`${WEBAPP_URL}/api/pipeline`, {
method: "POST",
headers: {
"Content-Type": "application/json",
sendToPipeline("responseUpdated", {
environmentId: survey.environmentId,
surveyId: survey.id,
// only send the updated fields
data: {
...response,
data: inputValidation.data.data,
},
body: JSON.stringify({
internalSecret: INTERNAL_SECRET,
environmentId,
surveyId: response.surveyId,
event: "responseUpdated",
data: response,
}),
});
if (response.finished) {
// send response to pipeline
// don't await to not block the response
fetch(`${WEBAPP_URL}/api/pipeline`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
internalSecret: INTERNAL_SECRET,
environmentId,
surveyId: response.surveyId,
event: "responseFinished",
data: response,
}),
sendToPipeline("responseFinished", {
environmentId: survey.environmentId,
surveyId: survey.id,
data: response,
});
}

View File

@@ -1,13 +1,14 @@
import { responses } from "@/lib/api/response";
import { prisma } from "@formbricks/database";
import { transformErrorToDetails } from "@/lib/api/validator";
import { sendToPipeline } from "@/lib/pipelines";
import { DatabaseError, InvalidInputError } from "@formbricks/errors";
import { capturePosthogEvent } from "@formbricks/lib/posthogServer";
import { createResponse } from "@formbricks/lib/services/response";
import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
import { getSurvey } from "@formbricks/lib/services/survey";
import { captureTelemetry } from "@formbricks/lib/telemetry";
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";
import { prisma } from "@formbricks/database";
export async function OPTIONS(): Promise<NextResponse> {
return responses.successResponse({}, true);
@@ -25,25 +26,22 @@ export async function POST(request: Request): Promise<NextResponse> {
);
}
// check if survey exists
const survey = await getSurvey(responseInput.surveyId);
let survey;
if (!survey) {
return responses.badRequestResponse(
"Linked ressource not found",
{
surveyId: "Survey not found",
},
true
);
try {
survey = await getSurvey(responseInput.surveyId);
} catch (error) {
if (error instanceof InvalidInputError) {
return responses.badRequestResponse(error.message);
} else {
return responses.internalServerErrorResponse(error.message);
}
}
const environmentId = survey.environmentId;
// prisma call to get the teamId
// TODO use services
const environment = await prisma.environment.findUnique({
where: { id: environmentId },
where: { id: survey.environmentId },
include: {
product: {
select: {
@@ -63,9 +61,8 @@ export async function POST(request: Request): Promise<NextResponse> {
});
if (!environment) {
throw new Error("Environment not found");
return responses.internalServerErrorResponse("Environment not found");
}
const {
product: {
team: { id: teamId, memberships },
@@ -74,39 +71,28 @@ export async function POST(request: Request): Promise<NextResponse> {
const teamOwnerId = memberships[0]?.userId;
const response = await createResponse(responseInput);
let response;
try {
response = await createResponse(responseInput);
} catch (error) {
if (error instanceof InvalidInputError) {
return responses.badRequestResponse(error.message);
} else {
return responses.internalServerErrorResponse(error.message);
}
}
// send response to pipeline
// don't await to not block the response
fetch(`${WEBAPP_URL}/api/pipeline`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
internalSecret: INTERNAL_SECRET,
environmentId,
surveyId: response.surveyId,
event: "responseCreated",
data: response,
}),
sendToPipeline("responseCreated", {
environmentId: survey.environmentId,
surveyId: response.surveyId,
data: response,
});
if (responseInput.finished) {
// send response to pipeline
// don't await to not block the response
fetch(`${WEBAPP_URL}/api/pipeline`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
internalSecret: INTERNAL_SECRET,
environmentId,
surveyId: response.surveyId,
event: "responseFinished",
data: response,
}),
sendToPipeline("responseFinished", {
environmentId: survey.environmentId,
surveyId: response.surveyId,
data: response,
});
}

View File

@@ -1,6 +1,6 @@
import { hashApiKey } from "@/lib/api/apiHelper";
import { responses } from "@/lib/api/response";
import { prisma } from "@formbricks/database";
import { getApiKeyFromKey } from "@formbricks/lib/services/apiKey";
import { deleteWebhook, getWebhook } from "@formbricks/lib/services/webhook";
import { headers } from "next/headers";
export async function GET(_: Request, { params }: { params: { webhookId: string } }) {
@@ -8,24 +8,13 @@ export async function GET(_: Request, { params }: { params: { webhookId: string
if (!apiKey) {
return responses.notAuthenticatedResponse();
}
const apiKeyData = await prisma.apiKey.findUnique({
where: {
hashedKey: hashApiKey(apiKey),
},
select: {
environmentId: true,
},
});
const apiKeyData = await getApiKeyFromKey(apiKey);
if (!apiKeyData) {
return responses.notAuthenticatedResponse();
}
// add webhook to database
const webhook = await prisma.webhook.findUnique({
where: {
id: params.webhookId,
},
});
const webhook = await getWebhook(params.webhookId);
if (!webhook) {
return responses.notFoundResponse("Webhook", params.webhookId);
}
@@ -37,26 +26,17 @@ export async function DELETE(_: Request, { params }: { params: { webhookId: stri
if (!apiKey) {
return responses.notAuthenticatedResponse();
}
const apiKeyData = await prisma.apiKey.findUnique({
where: {
hashedKey: hashApiKey(apiKey),
},
select: {
environmentId: true,
},
});
const apiKeyData = await getApiKeyFromKey(apiKey);
if (!apiKeyData) {
return responses.notAuthenticatedResponse();
}
// add webhook to database
const webhook = await prisma.webhook.delete({
where: {
id: params.webhookId,
},
});
if (!webhook) {
try {
const webhook = await deleteWebhook(params.webhookId);
return responses.successResponse(webhook);
} catch (e) {
console.error(e.message);
return responses.notFoundResponse("Webhook", params.webhookId);
}
return responses.successResponse(webhook);
}

View File

@@ -1,33 +1,32 @@
import { headers } from "next/headers";
import { prisma } from "@formbricks/database";
import { NextResponse } from "next/server";
import { hashApiKey } from "@/lib/api/apiHelper";
import { responses } from "@/lib/api/response";
import { transformErrorToDetails } from "@/lib/api/validator";
import { DatabaseError, InvalidInputError } from "@formbricks/errors";
import { getApiKeyFromKey } from "@formbricks/lib/services/apiKey";
import { createWebhook, getWebhooks } from "@formbricks/lib/services/webhook";
import { ZWebhookInput } from "@formbricks/types/v1/webhooks";
import { headers } from "next/headers";
import { NextResponse } from "next/server";
export async function GET() {
const apiKey = headers().get("x-api-key");
if (!apiKey) {
return responses.notAuthenticatedResponse();
}
const apiKeyData = await prisma.apiKey.findUnique({
where: {
hashedKey: hashApiKey(apiKey),
},
select: {
environmentId: true,
},
});
const apiKeyData = await getApiKeyFromKey(apiKey);
if (!apiKeyData) {
return responses.notAuthenticatedResponse();
}
// add webhook to database
const webhooks = await prisma.webhook.findMany({
where: {
environmentId: apiKeyData.environmentId,
},
});
return NextResponse.json({ data: webhooks });
// get webhooks from database
try {
const webhooks = await getWebhooks(apiKeyData.environmentId);
return NextResponse.json({ data: webhooks });
} catch (error) {
if (error instanceof DatabaseError) {
return responses.badRequestResponse(error.message);
}
throw error;
}
}
export async function POST(request: Request) {
@@ -35,37 +34,32 @@ export async function POST(request: Request) {
if (!apiKey) {
return responses.notAuthenticatedResponse();
}
const apiKeyData = await prisma.apiKey.findUnique({
where: {
hashedKey: hashApiKey(apiKey),
},
select: {
environmentId: true,
},
});
const apiKeyData = await getApiKeyFromKey(apiKey);
if (!apiKeyData) {
return responses.notAuthenticatedResponse();
}
const { url, trigger } = await request.json();
if (!url) {
return responses.missingFieldResponse("url");
}
const webhookInput = await request.json();
const inputValidation = ZWebhookInput.safeParse(webhookInput);
if (!trigger) {
return responses.missingFieldResponse("trigger");
if (!inputValidation.success) {
return responses.badRequestResponse(
"Fields are missing or incorrectly formatted",
transformErrorToDetails(inputValidation.error),
true
);
}
// add webhook to database
const webhook = await prisma.webhook.create({
data: {
url,
triggers: [trigger],
environment: {
connect: {
id: apiKeyData.environmentId,
},
},
},
});
return responses.successResponse(webhook);
try {
const webhook = await createWebhook(apiKeyData.environmentId, inputValidation.data);
return responses.successResponse(webhook);
} catch (error) {
if (error instanceof InvalidInputError) {
return responses.badRequestResponse(error.message);
}
if (error instanceof DatabaseError) {
return responses.internalServerErrorResponse(error.message);
}
throw error;
}
}

View File

@@ -112,8 +112,22 @@ const successResponse = (data: Object, cors: boolean = false) =>
}
);
const internalServerErrorResponse = (message: string, cors: boolean = false) =>
NextResponse.json(
{
code: "internal_server_error",
message,
details: {},
} as ApiErrorResponse,
{
status: 500,
...(cors && { headers: corsHeaders }),
}
);
export const responses = {
badRequestResponse,
internalServerErrorResponse,
missingFieldResponse,
methodNotAllowedResponse,
notAuthenticatedResponse,

19
apps/web/lib/pipelines.ts Normal file
View File

@@ -0,0 +1,19 @@
import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
export async function sendToPipeline(event, data) {
return fetch(`${WEBAPP_URL}/api/pipeline`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
internalSecret: INTERNAL_SECRET,
environmentId: data.environmentId,
surveyId: data.surveyId,
event,
data,
}),
}).catch((error) => {
console.error(`Error sending event to pipeline: ${error}`);
});
}

View File

@@ -12,7 +12,13 @@ const nextConfig = {
experimental: {
serverActions: true,
},
transpilePackages: ["@formbricks/database", "@formbricks/ee", "@formbricks/ui", "@formbricks/lib"],
transpilePackages: [
"@formbricks/database",
"@formbricks/ee",
"@formbricks/ui",
"@formbricks/lib",
"@formbricks/errors",
],
images: {
remotePatterns: [
{

View File

@@ -11,6 +11,7 @@
},
"dependencies": {
"@formbricks/ee": "workspace:*",
"@formbricks/errors": "workspace:*",
"@formbricks/js": "workspace:*",
"@formbricks/lib": "workspace:*",
"@formbricks/ui": "workspace:*",

View File

@@ -0,0 +1,83 @@
class ResourceNotFoundError extends Error {
statusCode = 404;
constructor(resource: string, id: string) {
super(`${resource} with ID ${id} not found`);
this.name = "ResourceNotFoundError";
}
}
class InvalidInputError extends Error {
statusCode = 400;
constructor(message: string) {
super(message);
this.name = "InvalidInputError";
}
}
class ValidationError extends Error {
statusCode = 400;
constructor(message: string) {
super(message);
this.name = "ValidationError";
}
}
class DatabaseError extends Error {
statusCode = 500;
constructor(message: string) {
super(message);
this.name = "DatabaseError";
}
}
class UniqueConstraintError extends Error {
statusCode = 409;
constructor(message: string) {
super(message);
this.name = "UniqueConstraintError";
}
}
class ForeignKeyConstraintError extends Error {
statusCode = 409;
constructor(message: string) {
super(message);
this.name = "ForeignKeyConstraintError";
}
}
class OperationNotAllowedError extends Error {
statusCode = 403;
constructor(message: string) {
super(message);
this.name = "OperationNotAllowedError";
}
}
class AuthenticationError extends Error {
statusCode = 401;
constructor(message: string) {
super(message);
this.name = "AuthenticationError";
}
}
class AuthorizationError extends Error {
statusCode = 403;
constructor(message: string) {
super(message);
this.name = "AuthorizationError";
}
}
export {
ResourceNotFoundError,
InvalidInputError,
ValidationError,
DatabaseError,
UniqueConstraintError,
ForeignKeyConstraintError,
OperationNotAllowedError,
AuthenticationError,
AuthorizationError,
};

View File

@@ -1,2 +1,3 @@
export * from "./functions";
export * from "./result";
export * from "./errors";

3
packages/lib/crypto.ts Normal file
View File

@@ -0,0 +1,3 @@
import { createHash } from "crypto";
export const getHash = (key: string): string => createHash("sha256").update(key).digest("hex");

View File

@@ -12,11 +12,12 @@
"lint:report": "eslint . --format json --output-file ../../lint-results/app-store.json"
},
"dependencies": {
"@formbricks/database": "*"
"@formbricks/database": "*",
"@formbricks/types": "*",
"@formbricks/errors": "*"
},
"devDependencies": {
"@formbricks/tsconfig": "*",
"@formbricks/types": "*",
"eslint": "^8.41.0",
"eslint-config-formbricks": "workspace:*",
"typescript": "^5.0.4"

View File

@@ -0,0 +1,57 @@
import { prisma } from "@formbricks/database";
import { TApiKey } from "@formbricks/types/v1/apiKeys";
import { Prisma } from "@prisma/client";
import { getHash } from "../crypto";
import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/errors";
export const getApiKey = async (apiKey: string): Promise<TApiKey | null> => {
if (!apiKey) {
throw new InvalidInputError("API key cannot be null or undefined.");
}
try {
const apiKeyData = await prisma.apiKey.findUnique({
where: {
hashedKey: getHash(apiKey),
},
});
if (!apiKeyData) {
throw new ResourceNotFoundError("API Key", apiKey);
}
return apiKeyData;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
}
};
export const getApiKeyFromKey = async (apiKey: string): Promise<TApiKey | null> => {
if (!apiKey) {
throw new InvalidInputError("API key cannot be null or undefined.");
}
try {
const apiKeyData = await prisma.apiKey.findUnique({
where: {
hashedKey: getHash(apiKey),
},
});
if (!apiKeyData) {
throw new ResourceNotFoundError("API Key", apiKey);
}
return apiKeyData;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
}
};

View File

@@ -1,5 +1,7 @@
import { TPerson } from "@formbricks/types/v1/people";
import { prisma } from "@formbricks/database";
import { TPerson } from "@formbricks/types/v1/people";
import { Prisma } from "@prisma/client";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/errors";
type TransformPersonInput = {
id: string;
@@ -33,24 +35,36 @@ export const transformPrismaPerson = (person: TransformPersonInput | null): Tran
};
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,
try {
const personPrisma = await prisma.person.findUnique({
where: {
id: personId,
},
include: {
attributes: {
include: {
attributeClass: {
select: {
name: true,
},
},
},
},
},
},
});
});
const person = transformPrismaPerson(personPrisma);
if (!personPrisma) {
throw new ResourceNotFoundError("Person", personId);
}
return person;
const person = transformPrismaPerson(personPrisma);
return person;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
}
};

View File

@@ -1,159 +1,179 @@
import { prisma } from "@formbricks/database";
import { TResponse, TResponseInput, TResponseUpdateInput } from "@formbricks/types/v1/responses";
import { Prisma } from "@prisma/client";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/errors";
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: {
try {
const responsePrisma = await prisma.response.create({
data: {
survey: {
connect: {
id: responseInput.personId,
id: responseInput.surveyId,
},
},
}),
},
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,
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),
};
const response: TResponse = {
...responsePrisma,
person: transformPrismaPerson(responsePrisma.person),
};
return response;
return response;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
}
};
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,
try {
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;
if (!responsePrisma) {
throw new ResourceNotFoundError("Response", responseId);
}
const response: TResponse = {
...responsePrisma,
person: transformPrismaPerson(responsePrisma.person),
};
return response;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
}
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);
try {
const currentResponse = await getResponse(responseId);
if (!currentResponse) {
throw new Error("Response not found");
}
if (!currentResponse) {
throw new ResourceNotFoundError("Response", responseId);
}
// merge data object
const data = {
...currentResponse.data,
...responseInput.data,
};
// 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 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),
};
const response: TResponse = {
...responsePrisma,
person: transformPrismaPerson(responsePrisma.person),
};
return response;
return response;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
}
};

View File

@@ -1,38 +1,50 @@
import { prisma } from "@formbricks/database";
import { ValidationError } from "@formbricks/errors";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/errors";
import { TSurvey, ZSurvey } from "@formbricks/types/v1/surveys";
import { Prisma } from "@prisma/client";
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,
let surveyPrisma;
try {
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,
attributeFilters: {
select: {
id: true,
attributeClassId: true,
condition: true,
value: true,
},
},
},
},
});
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
}
if (!surveyPrisma) {
return null;
throw new ResourceNotFoundError("Survey", surveyId);
}
const numDisplays = await prisma.display.count({
@@ -53,8 +65,6 @@ export const getSurvey = async (surveyId: string): Promise<TSurvey | null> => {
const transformedSurvey = {
...surveyPrisma,
createdAt: surveyPrisma.createdAt.toISOString(),
updatedAt: surveyPrisma.updatedAt.toISOString(),
triggers: surveyPrisma.triggers.map((trigger) => trigger.eventClass),
analytics: {
numDisplays,
@@ -62,7 +72,10 @@ export const getSurvey = async (surveyId: string): Promise<TSurvey | null> => {
},
};
const survey = ZSurvey.parse(transformedSurvey);
return survey;
try {
const survey = ZSurvey.parse(transformedSurvey);
return survey;
} catch (error) {
throw new ValidationError("Data validation of survey failed");
}
};

View File

@@ -0,0 +1,77 @@
import { TWebhook, TWebhookInput } from "@formbricks/types/v1/webhooks";
import { prisma } from "@formbricks/database";
import { Prisma } from "@prisma/client";
import { ResourceNotFoundError, DatabaseError, InvalidInputError } from "@formbricks/errors";
export const getWebhooks = async (environmentId: string): Promise<TWebhook[]> => {
try {
return await prisma.webhook.findMany({
where: {
environmentId: environmentId,
},
});
} catch (error) {
throw new DatabaseError(`Database error when fetching webhooks for environment ${environmentId}`);
}
};
export const getWebhook = async (id: string): Promise<TWebhook | null> => {
try {
const webhook = await prisma.webhook.findUnique({
where: {
id,
},
});
if (!webhook) {
throw new ResourceNotFoundError("Webhook", id);
}
return webhook;
} catch (error) {
if (!(error instanceof ResourceNotFoundError)) {
throw new DatabaseError(`Database error when fetching webhook with ID ${id}`);
}
throw error;
}
};
export const createWebhook = async (
environmentId: string,
webhookInput: TWebhookInput
): Promise<TWebhook> => {
try {
if (!webhookInput.url || !webhookInput.trigger) {
throw new InvalidInputError("Missing URL or trigger in webhook input");
}
return await prisma.webhook.create({
data: {
url: webhookInput.url,
triggers: [webhookInput.trigger],
environment: {
connect: {
id: environmentId,
},
},
},
});
} catch (error) {
if (!(error instanceof InvalidInputError)) {
throw new DatabaseError(`Database error when creating webhook for environment ${environmentId}`);
}
throw error;
}
};
export const deleteWebhook = async (id: string): Promise<TWebhook> => {
try {
return await prisma.webhook.delete({
where: {
id,
},
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2025") {
throw new ResourceNotFoundError("Webhook", id);
}
throw new DatabaseError(`Database error when deleting webhook with ID ${id}`);
}
};

View File

@@ -1,5 +1,11 @@
{
"extends": "@formbricks/tsconfig/js-library.json",
"include": ["."],
"exclude": ["dist", "build", "node_modules"]
"exclude": ["dist", "build", "node_modules"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@prisma/client/*": ["@formbricks/database/client/*"]
}
}
}

View File

@@ -0,0 +1,12 @@
import { z } from "zod";
export const ZApiKey = z.object({
id: z.string().cuid2(),
createdAt: z.date(),
lastUsedAt: z.date().nullable(),
label: z.string().nullable(),
hashedKey: z.string(),
environmentId: z.string().cuid2(),
});
export type TApiKey = z.infer<typeof ZApiKey>;

View File

@@ -6,8 +6,8 @@ export type TResponseData = z.infer<typeof ZResponseData>;
const ZResponse = z.object({
id: z.string().cuid2(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
createdAt: z.date(),
updatedAt: z.date(),
surveyId: z.string().cuid2(),
person: z
.object({

View File

@@ -178,9 +178,9 @@ export const ZSurveyAttributeFilter = z.object({
});
export const ZSurvey = z.object({
id: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
id: z.string().cuid2(),
createdAt: z.date(),
updatedAt: z.date(),
name: z.string(),
type: z.union([z.literal("web"), z.literal("email"), z.literal("link"), z.literal("mobile")]),
environmentId: z.string(),

View File

@@ -0,0 +1,21 @@
import { z } from "zod";
export const ZWebhookTrigger = z.enum(["responseFinished", "responseCreated", "responseUpdated"]);
export const ZWebhook = z.object({
id: z.string().cuid2(),
createdAt: z.date(),
updatedAt: z.date(),
url: z.string().url(),
environmentId: z.string().cuid2(),
triggers: z.array(ZWebhookTrigger),
});
export type TWebhook = z.infer<typeof ZWebhook>;
export const ZWebhookInput = z.object({
url: z.string().url(),
trigger: ZWebhookTrigger,
});
export type TWebhookInput = z.infer<typeof ZWebhookInput>;

12
pnpm-lock.yaml generated
View File

@@ -195,6 +195,9 @@ importers:
'@formbricks/ee':
specifier: workspace:*
version: link:../../packages/ee
'@formbricks/errors':
specifier: workspace:*
version: link:../../packages/errors
'@formbricks/js':
specifier: workspace:*
version: link:../../packages/js
@@ -575,13 +578,16 @@ importers:
'@formbricks/database':
specifier: '*'
version: link:../database
'@formbricks/errors':
specifier: '*'
version: link:../errors
'@formbricks/types':
specifier: '*'
version: link:../types
devDependencies:
'@formbricks/tsconfig':
specifier: '*'
version: link:../tsconfig
'@formbricks/types':
specifier: '*'
version: link:../types
eslint:
specifier: ^8.41.0
version: 8.41.0