Files
formbricks/packages/lib/organization/service.ts
T

378 lines
10 KiB
TypeScript

import "server-only";
import { Prisma } from "@prisma/client";
import { prisma } from "@formbricks/database";
import { ZOptionalNumber, ZString } from "@formbricks/types/common";
import { ZId } from "@formbricks/types/environment";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import {
TOrganization,
TOrganizationCreateInput,
TOrganizationUpdateInput,
ZOrganizationCreateInput,
} from "@formbricks/types/organizations";
import { TUserNotificationSettings } from "@formbricks/types/user";
import { cache } from "../cache";
import { BILLING_LIMITS, ITEMS_PER_PAGE, PRODUCT_FEATURE_KEYS } from "../constants";
import { environmentCache } from "../environment/cache";
import { getProducts } from "../product/service";
import { updateUser } from "../user/service";
import { validateInputs } from "../utils/validate";
import { organizationCache } from "./cache";
export const select = {
id: true,
createdAt: true,
updatedAt: true,
name: true,
billing: true,
};
export const getOrganizationsTag = (organizationId: string) => `organizations-${organizationId}`;
export const getOrganizationsByUserIdCacheTag = (userId: string) => `users-${userId}-organizations`;
export const getOrganizationByEnvironmentIdCacheTag = (environmentId: string) =>
`environments-${environmentId}-organization`;
export const getOrganizationsByUserId = (userId: string, page?: number): Promise<TOrganization[]> =>
cache(
async () => {
validateInputs([userId, ZString], [page, ZOptionalNumber]);
try {
const organizations = await prisma.organization.findMany({
where: {
memberships: {
some: {
userId,
},
},
},
select,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
if (!organizations) {
throw new ResourceNotFoundError("Organizations by UserId", userId);
}
return organizations;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getOrganizationsByUserId-${userId}-${page}`],
{
tags: [organizationCache.tag.byUserId(userId)],
}
)();
export const getOrganizationByEnvironmentId = (environmentId: string): Promise<TOrganization | null> =>
cache(
async () => {
validateInputs([environmentId, ZId]);
try {
const organization = await prisma.organization.findFirst({
where: {
products: {
some: {
environments: {
some: {
id: environmentId,
},
},
},
},
},
select: { ...select, memberships: true }, // include memberships
});
return organization;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getOrganizationByEnvironmentId-${environmentId}`],
{
tags: [organizationCache.tag.byEnvironmentId(environmentId)],
}
)();
export const getOrganization = (organizationId: string): Promise<TOrganization | null> =>
cache(
async () => {
validateInputs([organizationId, ZString]);
try {
const organization = await prisma.organization.findUnique({
where: {
id: organizationId,
},
select,
});
return organization;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getOrganization-${organizationId}`],
{
tags: [organizationCache.tag.byId(organizationId)],
}
)();
export const createOrganization = async (
organizationInput: TOrganizationCreateInput
): Promise<TOrganization> => {
try {
validateInputs([organizationInput, ZOrganizationCreateInput]);
const organization = await prisma.organization.create({
data: {
...organizationInput,
billing: {
plan: PRODUCT_FEATURE_KEYS.FREE,
limits: {
monthly: {
responses: BILLING_LIMITS.FREE.RESPONSES,
miu: BILLING_LIMITS.FREE.MIU,
},
},
stripeCustomerId: null,
periodStart: new Date(),
period: "monthly",
},
},
select,
});
organizationCache.revalidate({
id: organization.id,
count: true,
});
return organization;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};
export const updateOrganization = async (
organizationId: string,
data: Partial<TOrganizationUpdateInput>
): Promise<TOrganization> => {
try {
const updatedOrganization = await prisma.organization.update({
where: {
id: organizationId,
},
data,
select: { ...select, memberships: true, products: { select: { environments: true } } }, // include memberships & environments
});
// revalidate cache for members
updatedOrganization?.memberships.forEach((membership) => {
organizationCache.revalidate({
userId: membership.userId,
});
});
// revalidate cache for environments
updatedOrganization?.products.forEach((product) => {
product.environments.forEach(async (environment) => {
organizationCache.revalidate({
environmentId: environment.id,
});
});
});
const organization = {
...updatedOrganization,
memberships: undefined,
products: undefined,
};
organizationCache.revalidate({
id: organization.id,
});
return organization;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2016") {
throw new ResourceNotFoundError("Organization", organizationId);
}
throw error; // Re-throw any other errors
}
};
export const deleteOrganization = async (organizationId: string): Promise<TOrganization> => {
validateInputs([organizationId, ZId]);
try {
const deletedOrganization = await prisma.organization.delete({
where: {
id: organizationId,
},
select: { ...select, memberships: true, products: { select: { environments: true } } }, // include memberships & environments
});
// revalidate cache for members
deletedOrganization?.memberships.forEach((membership) => {
organizationCache.revalidate({
userId: membership.userId,
});
});
// revalidate cache for environments
deletedOrganization?.products.forEach((product) => {
product.environments.forEach((environment) => {
environmentCache.revalidate({
id: environment.id,
});
organizationCache.revalidate({
environmentId: environment.id,
});
});
});
const organization = {
...deletedOrganization,
memberships: undefined,
products: undefined,
};
organizationCache.revalidate({
id: organization.id,
count: true,
});
return organization;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};
export const getMonthlyActiveOrganizationPeopleCount = (organizationId: string): Promise<number> =>
cache(
async () => {
validateInputs([organizationId, ZId]);
try {
// temporary solution until we have a better way to track active users
return 0;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getMonthlyActiveOrganizationPeopleCount-${organizationId}`],
{
revalidate: 60 * 60 * 2, // 2 hours
}
)();
export const getMonthlyOrganizationResponseCount = (organizationId: string): Promise<number> =>
cache(
async () => {
validateInputs([organizationId, ZId]);
try {
// Define the start of the month
// const now = new Date();
// const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const organization = await getOrganization(organizationId);
if (!organization) {
throw new ResourceNotFoundError("Organization", organizationId);
}
if (!organization.billing.periodStart) {
throw new Error("Organization billing period start is not set");
}
// Get all environment IDs for the organization
const products = await getProducts(organizationId);
const environmentIds = products.flatMap((product) => product.environments.map((env) => env.id));
// 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 } } },
{ createdAt: { gte: organization.billing.periodStart } },
],
},
});
// The result is an aggregation of the total count
return responseAggregations._count.id;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getMonthlyOrganizationResponseCount-${organizationId}`],
{
revalidate: 60 * 60 * 2, // 2 hours
}
)();
export const subscribeOrganizationMembersToSurveyResponses = async (
surveyId: string,
createdBy: string
): Promise<void> => {
try {
const surveyCreator = await prisma.user.findUnique({
where: {
id: createdBy,
},
});
if (!surveyCreator) {
throw new ResourceNotFoundError("User", createdBy);
}
const defaultSettings = { alert: {}, weeklySummary: {} };
const updatedNotificationSettings: TUserNotificationSettings = {
...defaultSettings,
...surveyCreator.notificationSettings,
};
updatedNotificationSettings.alert[surveyId] = true;
await updateUser(surveyCreator.id, {
notificationSettings: updatedNotificationSettings,
});
} catch (error) {
throw error;
}
};