mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-18 11:31:12 -05:00
chore: Improve database performance by adding indexes (#1593)
This commit is contained in:
@@ -7,7 +7,7 @@ import type { AttributeClass } from "@prisma/client";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { ArchiveBoxArrowDownIcon, ArchiveBoxXMarkIcon } from "@heroicons/react/24/solid";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { updatetAttributeClass } from "@formbricks/lib/attributeClass/service";
|
||||
import { updateAttributeClass } from "@formbricks/lib/attributeClass/service";
|
||||
import { useState } from "react";
|
||||
|
||||
interface AttributeSettingsTabProps {
|
||||
@@ -25,7 +25,7 @@ export default function AttributeSettingsTab({ attributeClass, setOpen }: Attrib
|
||||
const onSubmit = async (data) => {
|
||||
setisAttributeBeingSubmitted(true);
|
||||
setOpen(false);
|
||||
await updatetAttributeClass(attributeClass.id, data);
|
||||
await updateAttributeClass(attributeClass.id, data);
|
||||
router.refresh();
|
||||
setisAttributeBeingSubmitted(false);
|
||||
};
|
||||
@@ -33,7 +33,7 @@ export default function AttributeSettingsTab({ attributeClass, setOpen }: Attrib
|
||||
const handleArchiveToggle = async () => {
|
||||
setisAttributeBeingSubmitted(true);
|
||||
const data = { archived: !attributeClass.archived };
|
||||
await updatetAttributeClass(attributeClass.id, data);
|
||||
await updateAttributeClass(attributeClass.id, data);
|
||||
setisAttributeBeingSubmitted(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { NextResponse } from "next/server";
|
||||
import {
|
||||
deleteAttributeClass,
|
||||
getAttributeClass,
|
||||
updatetAttributeClass,
|
||||
updateAttributeClass,
|
||||
} from "@formbricks/lib/attributeClass/service";
|
||||
import { TAttributeClass, ZAttributeClassUpdateInput } from "@formbricks/types/attributeClasses";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
@@ -82,7 +82,7 @@ export async function PUT(
|
||||
transformErrorToDetails(inputValidation.error)
|
||||
);
|
||||
}
|
||||
const updatedAttributeClass = await updatetAttributeClass(params.attributeClassId, inputValidation.data);
|
||||
const updatedAttributeClass = await updateAttributeClass(params.attributeClassId, inputValidation.data);
|
||||
if (updatedAttributeClass) {
|
||||
return responses.successResponse(updatedAttributeClass);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Account_userId_idx" ON "Account"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ApiKey_environmentId_idx" ON "ApiKey"("environmentId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AttributeClass_environmentId_idx" ON "AttributeClass"("environmentId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Display_surveyId_idx" ON "Display"("surveyId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Display_personId_idx" ON "Display"("personId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Environment_productId_idx" ON "Environment"("productId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Integration_environmentId_idx" ON "Integration"("environmentId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Invite_teamId_idx" ON "Invite"("teamId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Membership_userId_idx" ON "Membership"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Membership_teamId_idx" ON "Membership"("teamId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Person_environmentId_idx" ON "Person"("environmentId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Product_teamId_idx" ON "Product"("teamId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Response_surveyId_created_at_idx" ON "Response"("surveyId", "created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Response_surveyId_idx" ON "Response"("surveyId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ResponseNote_responseId_idx" ON "ResponseNote"("responseId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Survey_environmentId_idx" ON "Survey"("environmentId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "SurveyAttributeFilter_surveyId_idx" ON "SurveyAttributeFilter"("surveyId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "SurveyAttributeFilter_attributeClassId_idx" ON "SurveyAttributeFilter"("attributeClassId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "SurveyTrigger_surveyId_idx" ON "SurveyTrigger"("surveyId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Tag_environmentId_idx" ON "Tag"("environmentId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "TagsOnResponses_responseId_idx" ON "TagsOnResponses"("responseId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "User_email_idx" ON "User"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Webhook_environmentId_idx" ON "Webhook"("environmentId");
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "email_teamId_unique" RENAME TO "Invite_email_teamId_idx";
|
||||
@@ -47,6 +47,8 @@ model Webhook {
|
||||
environmentId String
|
||||
triggers PipelineTriggers[]
|
||||
surveyIds String[]
|
||||
|
||||
@@index([environmentId])
|
||||
}
|
||||
|
||||
model Attribute {
|
||||
@@ -82,6 +84,7 @@ model AttributeClass {
|
||||
attributeFilters SurveyAttributeFilter[]
|
||||
|
||||
@@unique([name, environmentId])
|
||||
@@index([environmentId])
|
||||
}
|
||||
|
||||
model Person {
|
||||
@@ -94,6 +97,8 @@ model Person {
|
||||
sessions Session[]
|
||||
attributes Attribute[]
|
||||
displays Display[]
|
||||
|
||||
@@index([environmentId])
|
||||
}
|
||||
|
||||
model Response {
|
||||
@@ -120,6 +125,8 @@ model Response {
|
||||
singleUseId String?
|
||||
|
||||
@@unique([surveyId, singleUseId])
|
||||
@@index([surveyId, createdAt]) // to determine monthly response count
|
||||
@@index([surveyId])
|
||||
}
|
||||
|
||||
model ResponseNote {
|
||||
@@ -133,6 +140,8 @@ model ResponseNote {
|
||||
text String
|
||||
isResolved Boolean @default(false)
|
||||
isEdited Boolean @default(false)
|
||||
|
||||
@@index([responseId])
|
||||
}
|
||||
|
||||
model Tag {
|
||||
@@ -145,6 +154,7 @@ model Tag {
|
||||
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([environmentId, name])
|
||||
@@index([environmentId])
|
||||
}
|
||||
|
||||
model TagsOnResponses {
|
||||
@@ -154,6 +164,7 @@ model TagsOnResponses {
|
||||
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([responseId, tagId])
|
||||
@@index([responseId])
|
||||
}
|
||||
|
||||
enum SurveyStatus {
|
||||
@@ -178,6 +189,9 @@ model Display {
|
||||
personId String?
|
||||
responseId String? @unique
|
||||
status DisplayStatus?
|
||||
|
||||
@@index([surveyId])
|
||||
@@index([personId])
|
||||
}
|
||||
|
||||
model SurveyTrigger {
|
||||
@@ -190,6 +204,7 @@ model SurveyTrigger {
|
||||
eventClassId String
|
||||
|
||||
@@unique([surveyId, eventClassId])
|
||||
@@index([surveyId])
|
||||
}
|
||||
|
||||
enum SurveyAttributeFilterCondition {
|
||||
@@ -209,6 +224,8 @@ model SurveyAttributeFilter {
|
||||
value String
|
||||
|
||||
@@unique([surveyId, attributeClassId])
|
||||
@@index([surveyId])
|
||||
@@index([attributeClassId])
|
||||
}
|
||||
|
||||
enum SurveyType {
|
||||
@@ -272,6 +289,8 @@ model Survey {
|
||||
/// [SurveyVerifyEmail]
|
||||
verifyEmail Json?
|
||||
pin String?
|
||||
|
||||
@@index([environmentId])
|
||||
}
|
||||
|
||||
model Event {
|
||||
@@ -341,6 +360,7 @@ model Integration {
|
||||
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([type, environmentId])
|
||||
@@index([environmentId])
|
||||
}
|
||||
|
||||
model Environment {
|
||||
@@ -359,6 +379,8 @@ model Environment {
|
||||
webhooks Webhook[]
|
||||
tags Tag[]
|
||||
integration Integration[]
|
||||
|
||||
@@index([productId])
|
||||
}
|
||||
|
||||
enum WidgetPlacement {
|
||||
@@ -386,6 +408,7 @@ model Product {
|
||||
darkOverlay Boolean @default(false)
|
||||
|
||||
@@unique([teamId, name])
|
||||
@@index([teamId])
|
||||
}
|
||||
|
||||
model Team {
|
||||
@@ -398,8 +421,7 @@ model Team {
|
||||
/// @zod.custom(imports.ZTeamBilling)
|
||||
/// [TeamBilling]
|
||||
billing Json @default("{\"stripeCustomerId\": null, \"features\": {\"inAppSurvey\": {\"status\": \"inactive\", \"unlimited\": false}, \"linkSurvey\": {\"status\": \"inactive\", \"unlimited\": false}, \"userTargeting\": {\"status\": \"inactive\", \"unlimited\": false}}}")
|
||||
|
||||
invites Invite[]
|
||||
invites Invite[]
|
||||
}
|
||||
|
||||
enum MembershipRole {
|
||||
@@ -419,6 +441,8 @@ model Membership {
|
||||
role MembershipRole
|
||||
|
||||
@@id([userId, teamId])
|
||||
@@index([userId])
|
||||
@@index([teamId])
|
||||
}
|
||||
|
||||
model Invite {
|
||||
@@ -436,7 +460,8 @@ model Invite {
|
||||
expiresAt DateTime
|
||||
role MembershipRole @default(admin)
|
||||
|
||||
@@index([email, teamId], name: "email_teamId_unique")
|
||||
@@index([email, teamId])
|
||||
@@index([teamId])
|
||||
}
|
||||
|
||||
model ApiKey {
|
||||
@@ -447,6 +472,8 @@ model ApiKey {
|
||||
hashedKey String @unique()
|
||||
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
|
||||
environmentId String
|
||||
|
||||
@@index([environmentId])
|
||||
}
|
||||
|
||||
enum IdentityProvider {
|
||||
@@ -475,6 +502,7 @@ model Account {
|
||||
session_state String?
|
||||
|
||||
@@unique([provider, providerAccountId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
enum Role {
|
||||
@@ -529,6 +557,8 @@ model User {
|
||||
/// @zod.custom(imports.ZUserNotificationSettings)
|
||||
/// [UserNotificationSettings]
|
||||
notificationSettings Json @default("{}")
|
||||
|
||||
@@index([email])
|
||||
}
|
||||
|
||||
model ShortUrl {
|
||||
|
||||
@@ -84,7 +84,7 @@ export const getAttributeClasses = async (
|
||||
return attributeClasses.map(formatAttributeClassDateFields);
|
||||
};
|
||||
|
||||
export const updatetAttributeClass = async (
|
||||
export const updateAttributeClass = async (
|
||||
attributeClassId: string,
|
||||
data: Partial<TAttributeClassUpdateInput>
|
||||
): Promise<TAttributeClass | null> => {
|
||||
|
||||
@@ -319,39 +319,6 @@ export const getOrCreatePersonByUserId = async (userId: string, environmentId: s
|
||||
return transformPrismaPerson(personPrisma);
|
||||
};
|
||||
|
||||
export const getMonthlyActivePeopleCount = async (environmentId: string): Promise<number> =>
|
||||
await unstable_cache(
|
||||
async () => {
|
||||
validateInputs([environmentId, ZId]);
|
||||
|
||||
const now = new Date();
|
||||
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
|
||||
const personAggregations = await prisma.person.aggregate({
|
||||
_count: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
environmentId,
|
||||
sessions: {
|
||||
some: {
|
||||
createdAt: {
|
||||
gte: firstDayOfMonth,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return personAggregations._count.id;
|
||||
},
|
||||
[`getMonthlyActivePeopleCount-${environmentId}`],
|
||||
{
|
||||
tags: [personCache.tag.byEnvironmentId(environmentId)],
|
||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||
}
|
||||
)();
|
||||
|
||||
export const updatePersonAttribute = async (
|
||||
personId: string,
|
||||
attributeClassId: string,
|
||||
|
||||
@@ -533,35 +533,3 @@ export const getResponseCountBySurveyId = async (surveyId: string): Promise<numb
|
||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||
}
|
||||
)();
|
||||
|
||||
export const getMonthlyResponseCount = async (environmentId: string): Promise<number> =>
|
||||
await unstable_cache(
|
||||
async () => {
|
||||
validateInputs([environmentId, ZId]);
|
||||
|
||||
const now = new Date();
|
||||
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
|
||||
const responseAggregations = await prisma.response.aggregate({
|
||||
_count: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
survey: {
|
||||
environmentId,
|
||||
type: "web",
|
||||
},
|
||||
createdAt: {
|
||||
gte: firstDayOfMonth,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return responseAggregations._count.id;
|
||||
},
|
||||
[`getMonthlyResponseCount-${environmentId}`],
|
||||
{
|
||||
tags: [responseCache.tag.byEnvironmentId(environmentId)],
|
||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||
}
|
||||
)();
|
||||
|
||||
@@ -279,7 +279,7 @@ export const getSurveys = async (environmentId: string, page?: number): Promise<
|
||||
}));
|
||||
};
|
||||
|
||||
export async function updateSurvey(updatedSurvey: TSurvey): Promise<TSurvey> {
|
||||
export const updateSurvey = async (updatedSurvey: TSurvey): Promise<TSurvey> => {
|
||||
validateInputs([updatedSurvey, ZSurvey]);
|
||||
|
||||
const surveyId = updatedSurvey.id;
|
||||
@@ -449,7 +449,7 @@ export async function updateSurvey(updatedSurvey: TSurvey): Promise<TSurvey> {
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export async function deleteSurvey(surveyId: string) {
|
||||
validateInputs([surveyId, ZId]);
|
||||
@@ -486,7 +486,7 @@ export async function deleteSurvey(surveyId: string) {
|
||||
return deletedSurvey;
|
||||
}
|
||||
|
||||
export async function createSurvey(environmentId: string, surveyBody: TSurveyInput): Promise<TSurvey> {
|
||||
export const createSurvey = async (environmentId: string, surveyBody: TSurveyInput): Promise<TSurvey> => {
|
||||
validateInputs([environmentId, ZId]);
|
||||
|
||||
if (surveyBody.attributeFilters) {
|
||||
@@ -530,9 +530,9 @@ export async function createSurvey(environmentId: string, surveyBody: TSurveyInp
|
||||
});
|
||||
|
||||
return transformedSurvey;
|
||||
}
|
||||
};
|
||||
|
||||
export async function duplicateSurvey(environmentId: string, surveyId: string) {
|
||||
export const duplicateSurvey = async (environmentId: string, surveyId: string) => {
|
||||
const existingSurvey = await getSurvey(surveyId);
|
||||
|
||||
if (!existingSurvey) {
|
||||
@@ -596,4 +596,4 @@ export async function duplicateSurvey(environmentId: string, surveyId: string) {
|
||||
revalidateSurveyByAttributeClassId(newAttributeFilters);
|
||||
|
||||
return newSurvey;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,11 +7,9 @@ import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { TTeam, TTeamUpdateInput, ZTeamUpdateInput } from "@formbricks/types/teams";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import { getMonthlyActivePeopleCount } from "../person/service";
|
||||
import { getProducts } from "../product/service";
|
||||
import { getMonthlyResponseCount } from "../response/service";
|
||||
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
|
||||
import { environmentCache } from "../environment/cache";
|
||||
import { getProducts } from "../product/service";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { teamCache } from "./cache";
|
||||
|
||||
@@ -291,19 +289,34 @@ export const getMonthlyActiveTeamPeopleCount = async (teamId: string): Promise<n
|
||||
async () => {
|
||||
validateInputs([teamId, ZId]);
|
||||
|
||||
// Define the start of the month
|
||||
const now = new Date();
|
||||
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
|
||||
// Get all environment IDs for the team
|
||||
const products = await getProducts(teamId);
|
||||
const environmentIds = products.flatMap((product) => product.environments.map((env) => env.id));
|
||||
|
||||
let peopleCount = 0;
|
||||
// Aggregate the count of active people across all environments
|
||||
const peopleAggregations = await prisma.person.aggregate({
|
||||
_count: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
AND: [
|
||||
{ environmentId: { in: environmentIds } },
|
||||
{
|
||||
sessions: {
|
||||
some: {
|
||||
createdAt: { gte: firstDayOfMonth },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
for (const product of products) {
|
||||
for (const environment of product.environments) {
|
||||
const peopleInThisEnvironment = await getMonthlyActivePeopleCount(environment.id);
|
||||
|
||||
peopleCount += peopleInThisEnvironment;
|
||||
}
|
||||
}
|
||||
|
||||
return peopleCount;
|
||||
return peopleAggregations._count.id;
|
||||
},
|
||||
[`getMonthlyActiveTeamPeopleCount-${teamId}`],
|
||||
{
|
||||
@@ -317,19 +330,30 @@ export const getMonthlyTeamResponseCount = async (teamId: string): Promise<numbe
|
||||
async () => {
|
||||
validateInputs([teamId, ZId]);
|
||||
|
||||
// Define the start of the month
|
||||
const now = new Date();
|
||||
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
|
||||
// Get all environment IDs for the team
|
||||
const products = await getProducts(teamId);
|
||||
const environmentIds = products.flatMap((product) => product.environments.map((env) => env.id));
|
||||
|
||||
let responseCount = 0;
|
||||
// Use Prisma's aggregate to count responses for all environments
|
||||
const responseAggregations = await prisma.response.aggregate({
|
||||
_count: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
AND: [
|
||||
{ survey: { environmentId: { in: environmentIds } } },
|
||||
{ survey: { type: "web" } },
|
||||
{ createdAt: { gte: firstDayOfMonth } },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
for (const product of products) {
|
||||
for (const environment of product.environments) {
|
||||
const responsesInEnvironment = await getMonthlyResponseCount(environment.id);
|
||||
|
||||
responseCount += responsesInEnvironment;
|
||||
}
|
||||
}
|
||||
|
||||
return responseCount;
|
||||
// The result is an aggregation of the total count
|
||||
return responseAggregations._count.id;
|
||||
},
|
||||
[`getMonthlyTeamResponseCount-${teamId}`],
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user