Files
formbricks/packages/lib/product/service.ts
T
Piyush Gupta 1af1a92fec feat: granular team roles (#3975)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
Co-authored-by: Johannes <johannes@formbricks.com>
2024-11-08 06:03:14 +00:00

370 lines
9.8 KiB
TypeScript

import "server-only";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { z } from "zod";
import { prisma } from "@formbricks/database";
import { ZOptionalNumber, ZString } from "@formbricks/types/common";
import { ZId } from "@formbricks/types/common";
import { DatabaseError, ValidationError } from "@formbricks/types/errors";
import type { TProduct, TProductUpdateInput } from "@formbricks/types/product";
import { ZProduct, ZProductUpdateInput } from "@formbricks/types/product";
import { cache } from "../cache";
import { ITEMS_PER_PAGE, isS3Configured } from "../constants";
import { environmentCache } from "../environment/cache";
import { createEnvironment } from "../environment/service";
import { deleteLocalFilesByEnvironmentId, deleteS3FilesByEnvironmentId } from "../storage/service";
import { validateInputs } from "../utils/validate";
import { productCache } from "./cache";
const selectProduct = {
id: true,
createdAt: true,
updatedAt: true,
name: true,
organizationId: true,
languages: true,
recontactDays: true,
linkSurveyBranding: true,
inAppSurveyBranding: true,
config: true,
placement: true,
clickOutsideClose: true,
darkOverlay: true,
environments: true,
styling: true,
logo: true,
};
export const getUserProducts = reactCache(
(userId: string, organizationId: string, page?: number): Promise<TProduct[]> =>
cache(
async () => {
validateInputs([userId, ZString], [organizationId, ZId], [page, ZOptionalNumber]);
const orgMembership = await prisma.membership.findFirst({
where: {
userId,
organizationId,
},
});
if (!orgMembership) {
throw new ValidationError("User is not a member of this organization");
}
let productWhereClause: Prisma.ProductWhereInput = {};
if (orgMembership.role === "member") {
productWhereClause = {
productTeams: {
some: {
team: {
teamUsers: {
some: {
userId,
},
},
},
},
},
};
}
try {
const products = await prisma.product.findMany({
where: {
organizationId,
...productWhereClause,
},
select: selectProduct,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return products;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getUserProducts-${userId}-${organizationId}-${page}`],
{
tags: [productCache.tag.byUserId(userId), productCache.tag.byOrganizationId(organizationId)],
}
)()
);
export const getProducts = reactCache(
(organizationId: string, page?: number): Promise<TProduct[]> =>
cache(
async () => {
validateInputs([organizationId, ZId], [page, ZOptionalNumber]);
try {
const products = await prisma.product.findMany({
where: {
organizationId,
},
select: selectProduct,
take: page ? ITEMS_PER_PAGE : undefined,
skip: page ? ITEMS_PER_PAGE * (page - 1) : undefined,
});
return products;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getProducts-${organizationId}-${page}`],
{
tags: [productCache.tag.byOrganizationId(organizationId)],
}
)()
);
export const getProductByEnvironmentId = reactCache(
(environmentId: string): Promise<TProduct | null> =>
cache(
async () => {
validateInputs([environmentId, ZId]);
let productPrisma;
try {
productPrisma = await prisma.product.findFirst({
where: {
environments: {
some: {
id: environmentId,
},
},
},
select: selectProduct,
});
return productPrisma;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
console.error(error);
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getProductByEnvironmentId-${environmentId}`],
{
tags: [productCache.tag.byEnvironmentId(environmentId)],
}
)()
);
export const updateProduct = async (
productId: string,
inputProduct: TProductUpdateInput
): Promise<TProduct> => {
validateInputs([productId, ZId], [inputProduct, ZProductUpdateInput]);
const { environments, ...data } = inputProduct;
let updatedProduct;
try {
updatedProduct = await prisma.product.update({
where: {
id: productId,
},
data: {
...data,
environments: {
connect: environments?.map((environment) => ({ id: environment.id })) ?? [],
},
},
select: selectProduct,
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
try {
const product = ZProduct.parse(updatedProduct);
productCache.revalidate({
id: product.id,
organizationId: product.organizationId,
});
product.environments.forEach((environment) => {
// revalidate environment cache
productCache.revalidate({
environmentId: environment.id,
});
});
return product;
} catch (error) {
if (error instanceof z.ZodError) {
console.error(JSON.stringify(error.errors, null, 2));
}
throw new ValidationError("Data validation of product failed");
}
};
export const getProduct = reactCache(
(productId: string): Promise<TProduct | null> =>
cache(
async () => {
let productPrisma;
try {
productPrisma = await prisma.product.findUnique({
where: {
id: productId,
},
select: selectProduct,
});
return productPrisma;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getProduct-${productId}`],
{
tags: [productCache.tag.byId(productId)],
}
)()
);
export const deleteProduct = async (productId: string): Promise<TProduct> => {
try {
const product = await prisma.product.delete({
where: {
id: productId,
},
select: selectProduct,
});
if (product) {
// delete all files from storage related to this product
if (isS3Configured()) {
const s3FilesPromises = product.environments.map(async (environment) => {
return deleteS3FilesByEnvironmentId(environment.id);
});
try {
await Promise.all(s3FilesPromises);
} catch (err) {
// fail silently because we don't want to throw an error if the files are not deleted
console.error(err);
}
} else {
const localFilesPromises = product.environments.map(async (environment) => {
return deleteLocalFilesByEnvironmentId(environment.id);
});
try {
await Promise.all(localFilesPromises);
} catch (err) {
// fail silently because we don't want to throw an error if the files are not deleted
console.error(err);
}
}
productCache.revalidate({
id: product.id,
organizationId: product.organizationId,
});
environmentCache.revalidate({
productId: product.id,
});
product.environments.forEach((environment) => {
// revalidate product cache
productCache.revalidate({
environmentId: environment.id,
});
environmentCache.revalidate({
id: environment.id,
});
});
}
return product;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};
export const createProduct = async (
organizationId: string,
productInput: Partial<TProductUpdateInput>
): Promise<TProduct> => {
validateInputs([organizationId, ZString], [productInput, ZProductUpdateInput.partial()]);
if (!productInput.name) {
throw new ValidationError("Product Name is required");
}
const { environments, teamIds, ...data } = productInput;
try {
let product = await prisma.product.create({
data: {
config: {
channel: null,
industry: null,
},
...data,
name: productInput.name,
organizationId,
},
select: selectProduct,
});
if (teamIds) {
await prisma.productTeam.createMany({
data: teamIds.map((teamId) => ({
productId: product.id,
teamId,
})),
});
}
productCache.revalidate({
id: product.id,
organizationId: product.organizationId,
});
const devEnvironment = await createEnvironment(product.id, {
type: "development",
});
const prodEnvironment = await createEnvironment(product.id, {
type: "production",
});
const updatedProduct = await updateProduct(product.id, {
environments: [devEnvironment, prodEnvironment],
});
return updatedProduct;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};