mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 18:30:32 -06:00
Merge pull request #375 from formbricks/feature/FOR-772
Add Data Service for Responses, Webhooks, Surveys & Api Keys
This commit is contained in:
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
19
apps/web/lib/pipelines.ts
Normal 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}`);
|
||||
});
|
||||
}
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@formbricks/ee": "workspace:*",
|
||||
"@formbricks/errors": "workspace:*",
|
||||
"@formbricks/js": "workspace:*",
|
||||
"@formbricks/lib": "workspace:*",
|
||||
"@formbricks/ui": "workspace:*",
|
||||
|
||||
83
packages/errors/src/errors.ts
Normal file
83
packages/errors/src/errors.ts
Normal 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,
|
||||
};
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./functions";
|
||||
export * from "./result";
|
||||
export * from "./errors";
|
||||
|
||||
3
packages/lib/crypto.ts
Normal file
3
packages/lib/crypto.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { createHash } from "crypto";
|
||||
|
||||
export const getHash = (key: string): string => createHash("sha256").update(key).digest("hex");
|
||||
@@ -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"
|
||||
|
||||
57
packages/lib/services/apiKey.ts
Normal file
57
packages/lib/services/apiKey.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
};
|
||||
|
||||
77
packages/lib/services/webhook.ts
Normal file
77
packages/lib/services/webhook.ts
Normal 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}`);
|
||||
}
|
||||
};
|
||||
@@ -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/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
packages/types/v1/apiKeys.ts
Normal file
12
packages/types/v1/apiKeys.ts
Normal 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>;
|
||||
@@ -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({
|
||||
|
||||
@@ -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(),
|
||||
|
||||
21
packages/types/v1/webhooks.ts
Normal file
21
packages/types/v1/webhooks.ts
Normal 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
12
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user