Files
formbricks/packages/lib/attribute/service.ts
Dhruwang Jariwala ab80bc1bf2 feat: language switch (#2692)
Co-authored-by: Johannes <johannes@formbricks.com>
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
2024-06-12 14:10:22 +00:00

256 lines
7.0 KiB
TypeScript

import "server-only";
import { Prisma } from "@prisma/client";
import { prisma } from "@formbricks/database";
import { TAttributes, ZAttributes } from "@formbricks/types/attributes";
import { ZString } from "@formbricks/types/common";
import { ZId } from "@formbricks/types/environment";
import { DatabaseError, OperationNotAllowedError } from "@formbricks/types/errors";
import { attributeCache } from "../attribute/cache";
import { attributeClassCache } from "../attributeClass/cache";
import {
getAttributeClassByName,
getAttributeClasses,
getAttributeClassesCount,
} from "../attributeClass/service";
import { cache } from "../cache";
import { MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT } from "../constants";
import { getPerson, getPersonByUserId } from "../person/service";
import { validateInputs } from "../utils/validate";
export const selectAttribute: Prisma.AttributeSelect = {
value: true,
attributeClass: {
select: {
name: true,
id: true,
},
},
};
// convert prisma attributes to a key-value object
const convertPrismaAttributes = (prismaAttributes: any): TAttributes => {
return prismaAttributes.reduce(
(acc, attr) => {
acc[attr.attributeClass.name] = attr.value;
return acc;
},
{} as Record<string, string | number>
);
};
export const getAttributes = (personId: string): Promise<TAttributes> =>
cache(
async () => {
validateInputs([personId, ZId]);
try {
const prismaAttributes = await prisma.attribute.findMany({
where: {
personId,
},
select: selectAttribute,
});
return convertPrismaAttributes(prismaAttributes);
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getAttributes-${personId}`],
{
tags: [attributeCache.tag.byPersonId(personId)],
}
)();
export const getAttributesByUserId = (environmentId: string, userId: string): Promise<TAttributes> =>
cache(
async () => {
validateInputs([environmentId, ZId], [userId, ZString]);
const person = await getPersonByUserId(environmentId, userId);
if (!person) {
throw new Error("Person not found");
}
try {
const prismaAttributes = await prisma.attribute.findMany({
where: {
personId: person.id,
},
select: selectAttribute,
});
return convertPrismaAttributes(prismaAttributes);
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getAttributesByUserId-${environmentId}-${userId}`],
{
tags: [attributeCache.tag.byEnvironmentIdAndUserId(environmentId, userId)],
}
)();
export const getAttribute = (name: string, personId: string): Promise<string | undefined> =>
cache(
async () => {
validateInputs([name, ZString], [personId, ZId]);
const person = await getPerson(personId);
if (!person) {
throw new Error("Person not found");
}
const attributeClass = await getAttributeClassByName(person?.environmentId, name);
if (!attributeClass) {
return undefined;
}
try {
const prismaAttributes = await prisma.attribute.findFirst({
where: {
attributeClassId: attributeClass.id,
personId,
},
select: { value: true },
});
return prismaAttributes?.value;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getAttribute-${name}-${personId}`],
{
tags: [attributeCache.tag.byNameAndPersonId(name, personId)],
}
)();
export const updateAttributes = async (personId: string, attributes: TAttributes): Promise<boolean> => {
validateInputs([personId, ZId], [attributes, ZAttributes]);
const person = await getPerson(personId);
if (!person) {
throw new Error("Person not found");
}
const environmentId = person.environmentId;
const userId = person.userId;
const attributeClasses = await getAttributeClasses(environmentId);
const attributeClassMap = new Map(attributeClasses.map((ac) => [ac.name, ac.id]));
const upsertOperations: Promise<any>[] = [];
const createOperations: Promise<any>[] = [];
const newAttributes: { name: string; value: string }[] = [];
for (const [name, value] of Object.entries(attributes)) {
const attributeClassId = attributeClassMap.get(name);
if (attributeClassId) {
// Class exists, perform an upsert operation
upsertOperations.push(
prisma.attribute
.upsert({
select: {
id: true,
},
where: {
personId_attributeClassId: {
personId,
attributeClassId,
},
},
update: {
value,
},
create: {
personId,
attributeClassId,
value,
},
})
.then(() => {
attributeCache.revalidate({ environmentId, personId, userId, name });
})
);
} else {
// Collect new attributes to be created later
newAttributes.push({ name, value });
}
}
// Execute all upsert operations concurrently
await Promise.all(upsertOperations);
if (newAttributes.length === 0) {
// short-circuit if no new attributes to create
return true;
}
// Check if new attribute classes will exceed the limit
const attributeClassCount = await getAttributeClassesCount(environmentId);
const totalAttributeClassesLength = attributeClassCount + newAttributes.length;
if (totalAttributeClassesLength > MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT) {
throw new OperationNotAllowedError(
`Updating these attributes would exceed the maximum number of attribute classes (${MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT}) for environment ${environmentId}. Existing attributes have been updated.`
);
}
for (const { name, value } of newAttributes) {
createOperations.push(
prisma.attributeClass
.create({
select: { id: true },
data: {
name,
type: "code",
environment: {
connect: {
id: environmentId,
},
},
attributes: {
create: {
personId,
value,
},
},
},
})
.then(({ id }) => {
attributeClassCache.revalidate({ id, environmentId, name });
attributeCache.revalidate({ environmentId, personId, userId, name });
})
);
}
// Execute all create operations for new attribute classes
await Promise.all(createOperations);
// Revalidate the count cache
attributeClassCache.revalidate({
environmentId,
});
return true;
};