mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-21 19:39:28 -05:00
378 lines
10 KiB
TypeScript
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;
|
|
}
|
|
};
|