fix: data migration and cleanup for userId attribute (#2400)

Co-authored-by: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com>
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
This commit is contained in:
Anshuman Pandey
2024-04-15 18:22:12 +05:30
committed by GitHub
parent 529144fe36
commit 5add263e6f
13 changed files with 80 additions and 366 deletions

View File

@@ -38,9 +38,7 @@ export default async function AttributesSection({ personId }: { personId: string
<div>
<dt className="text-sm font-medium text-slate-500">User Id</dt>
<dd className="ph-no-capture mt-1 text-sm text-slate-900">
{person.attributes.userId ? (
<span>{person.attributes.userId}</span>
) : person.userId ? (
{person.userId ? (
<span>{person.userId}</span>
) : (
<span className="text-slate-300">Not provided</span>
@@ -53,7 +51,7 @@ export default async function AttributesSection({ personId }: { personId: string
</div>
{Object.entries(person.attributes)
.filter(([key, _]) => key !== "email" && key !== "userId" && key !== "language")
.filter(([key, _]) => key !== "email" && key !== "language")
.map(([key, value]) => (
<div key={key}>
<dt className="text-sm font-medium text-slate-500">{capitalizeFirstLetter(key.toString())}</dt>
@@ -62,10 +60,6 @@ export default async function AttributesSection({ personId }: { personId: string
))}
<hr />
<div>
{/* <dt className="text-sm font-medium text-slate-500">Sessions</dt> */}
{/* <dd className="mt-1 text-sm text-slate-900">{numberOfSessions}</dd> */}
</div>
<div>
<dt className="text-sm font-medium text-slate-500">Responses</dt>
<dd className="mt-1 text-sm text-slate-900">{numberOfResponses}</dd>

View File

@@ -4,7 +4,6 @@ import Link from "next/link";
import { ITEMS_PER_PAGE } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getPeople, getPeopleCount } from "@formbricks/lib/person/service";
import { truncateMiddle } from "@formbricks/lib/strings";
import { TPerson } from "@formbricks/types/people";
import { PersonAvatar } from "@formbricks/ui/Avatars";
import EmptySpaceFiller from "@formbricks/ui/EmptySpaceFiller";
@@ -83,9 +82,7 @@ export default async function PeoplePage({
</div>
</div>
<div className="col-span-2 my-auto hidden whitespace-nowrap text-center text-sm text-slate-500 sm:block">
<div className="ph-no-capture text-slate-900">
{truncateMiddle(getAttributeValue(person, "userId"), 24) || person.userId}
</div>
<div className="ph-no-capture text-slate-900">{person.userId}</div>
</div>
<div className="col-span-2 my-auto hidden whitespace-nowrap text-center text-sm text-slate-500 sm:block">
<div className="ph-no-capture text-slate-900">{getAttributeValue(person, "email")}</div>

View File

@@ -0,0 +1,69 @@
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function main() {
await prisma.$transaction(
async (tx) => {
// get all the persons that have an attribute class with the name "userId"
const personsWithUserIdAttribute = await tx.person.findMany({
where: {
attributes: {
some: {
attributeClass: {
name: "userId",
},
},
},
},
include: {
attributes: {
include: { attributeClass: true },
},
},
});
for (let person of personsWithUserIdAttribute) {
// If the person already has a userId, skip it
if (person.userId) {
continue;
}
const userIdAttributeValue = person.attributes.find((attribute) => {
if (attribute.attributeClass.name === "userId") {
return attribute;
}
});
if (!userIdAttributeValue) {
continue;
}
await tx.person.update({
where: {
id: person.id,
},
data: {
userId: userIdAttributeValue.value,
},
});
}
// Delete all attributeClasses with the name "userId"
await tx.attributeClass.deleteMany({
where: {
name: "userId",
},
});
},
{
timeout: 60000 * 3, // 3 minutes
}
);
}
main()
.catch(async (e) => {
console.error(e);
process.exit(1);
})
.finally(async () => await prisma.$disconnect());

View File

@@ -1,293 +0,0 @@
"use strict";
var __awaiter =
(this && this.__awaiter) ||
function (thisArg, _arguments, P, generator) {
function adopt(value) {
return value instanceof P
? value
: new P(function (resolve) {
resolve(value);
});
}
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
}
function rejected(value) {
try {
step(generator["throw"](value));
} catch (e) {
reject(e);
}
}
function step(result) {
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
}
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator =
(this && this.__generator) ||
function (thisArg, body) {
var _ = {
label: 0,
sent: function () {
if (t[0] & 1) throw t[1];
return t[1];
},
trys: [],
ops: [],
},
f,
y,
t,
g;
return (
(g = { next: verb(0), throw: verb(1), return: verb(2) }),
typeof Symbol === "function" &&
(g[Symbol.iterator] = function () {
return this;
}),
g
);
function verb(n) {
return function (v) {
return step([n, v]);
};
}
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while ((g && ((g = 0), op[0] && (_ = 0)), _))
try {
if (
((f = 1),
y &&
(t =
op[0] & 2
? y["return"]
: op[0]
? y["throw"] || ((t = y["return"]) && t.call(y), 0)
: y.next) &&
!(t = t.call(y, op[1])).done)
)
return t;
if (((y = 0), t)) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0:
case 1:
t = op;
break;
case 4:
_.label++;
return { value: op[1], done: false };
case 5:
_.label++;
y = op[1];
op = [0];
continue;
case 7:
op = _.ops.pop();
_.trys.pop();
continue;
default:
if (!((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && (op[0] === 6 || op[0] === 2)) {
_ = 0;
continue;
}
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) {
_.label = op[1];
break;
}
if (op[0] === 6 && _.label < t[1]) {
_.label = t[1];
t = op;
break;
}
if (t && _.label < t[2]) {
_.label = t[2];
_.ops.push(op);
break;
}
if (t[2]) _.ops.pop();
_.trys.pop();
continue;
}
op = body.call(thisArg, _);
} catch (e) {
op = [6, e];
y = 0;
} finally {
f = t = 0;
}
if (op[0] & 5) throw op[1];
return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
var cuid2_1 = require("@paralleldrive/cuid2");
var client_1 = require("@prisma/client");
var prisma = new client_1.PrismaClient();
function main() {
return __awaiter(this, void 0, void 0, function () {
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
return [
4 /*yield*/,
prisma.$transaction(function (tx) {
return __awaiter(_this, void 0, void 0, function () {
var allSurveysWithAttributeFilters;
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
return [
4 /*yield*/,
prisma.survey.findMany({
where: {
attributeFilters: {
some: {},
},
},
include: {
attributeFilters: { include: { attributeClass: true } },
},
}),
];
case 1:
allSurveysWithAttributeFilters = _a.sent();
if (
!(allSurveysWithAttributeFilters === null || allSurveysWithAttributeFilters === void 0
? void 0
: allSurveysWithAttributeFilters.length)
) {
// stop the migration if there are no surveys with attribute filters
return [2 /*return*/];
}
allSurveysWithAttributeFilters.forEach(function (survey) {
return __awaiter(_this, void 0, void 0, function () {
var attributeFilters, filters;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
attributeFilters = survey.attributeFilters;
// if there are no attribute filters, we can skip this survey
if (
!(attributeFilters === null || attributeFilters === void 0
? void 0
: attributeFilters.length)
) {
return [2 /*return*/];
}
filters = attributeFilters.map(function (filter, idx) {
var attributeClass = filter.attributeClass;
var resource;
// if the attribute class is userId, we need to create a user segment with the person filter
if (
attributeClass.name === "userId" &&
attributeClass.type === "automatic"
) {
resource = {
id: (0, cuid2_1.createId)(),
root: {
type: "person",
personIdentifier: "userId",
},
qualifier: {
operator: filter.condition,
},
value: filter.value,
};
} else {
resource = {
id: (0, cuid2_1.createId)(),
root: {
type: "attribute",
attributeClassName: attributeClass.name,
},
qualifier: {
operator: filter.condition,
},
value: filter.value,
};
}
var attributeSegment = {
id: filter.id,
connector: idx === 0 ? null : "and",
resource: resource,
};
return attributeSegment;
});
return [
4 /*yield*/,
tx.segment.create({
data: {
title: "".concat(survey.id),
description: "",
isPrivate: true,
filters: filters,
surveys: {
connect: {
id: survey.id,
},
},
environment: {
connect: {
id: survey.environmentId,
},
},
},
}),
];
case 1:
_a.sent();
return [2 /*return*/];
}
});
});
});
// delete all attribute filters
return [4 /*yield*/, tx.surveyAttributeFilter.deleteMany({})];
case 2:
// delete all attribute filters
_a.sent();
return [2 /*return*/];
}
});
});
}),
];
case 1:
_a.sent();
return [2 /*return*/];
}
});
});
}
main()
.catch(function (e) {
return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
console.error(e);
process.exit(1);
return [2 /*return*/];
});
});
})
.finally(function () {
return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
return [4 /*yield*/, prisma.$disconnect()];
case 1:
return [2 /*return*/, _a.sent()];
}
});
});
});

View File

@@ -23,12 +23,13 @@
"lint": "eslint ./src --fix",
"post-install": "pnpm generate",
"predev": "pnpm generate",
"data-migration:v1.6": "ts-node ./migrations/20240207041922_advanced_targeting/data-migration.ts",
"data-migration:styling": "ts-node ./migrations/20240320090315_add_form_styling/data-migration.ts",
"data-migration:v1.6": "ts-node ./data-migrations/20240207041922_advanced_targeting/data-migration.ts",
"data-migration:styling": "ts-node ./data-migrations/20240320090315_add_form_styling/data-migration.ts",
"data-migration:v1.7": "pnpm data-migration:mls && pnpm data-migration:styling",
"data-migration:mls": "ts-node ./migrations/20240318050527_add_languages_and_survey_languages/data-migration.ts",
"data-migration:mls-fix": "ts-node ./migrations/20240318050527_add_languages_and_survey_languages/data-migration-fix.ts",
"data-migration:mls-range-fix": "ts-node ./migrations/20240318050527_add_languages_and_survey_languages/data-migration-range-fix.ts"
"data-migration:mls": "ts-node ./data-migrations/20240318050527_add_languages_and_survey_languages/data-migration.ts",
"data-migration:mls-fix": "ts-node ./data-migrations/20240318050527_add_languages_and_survey_languages/data-migration-fix.ts",
"data-migration:mls-range-fix": "ts-node ./data-migrations/20240318050527_add_languages_and_survey_languages/data-migration-range-fix.ts",
"data-migration:userId": "ts-node ./data-migrations/20240408123456_userid_migration/data-migration.ts"
},
"dependencies": {
"@prisma/client": "^5.12.1",

View File

@@ -325,50 +325,7 @@ export const getPersonByUserId = async (environmentId: string, userId: string):
return transformPrismaPerson(personWithUserId);
}
// Check if a person with the userId attribute exists
let personWithUserIdAttribute = await prisma.person.findFirst({
where: {
environmentId,
attributes: {
some: {
attributeClass: {
name: "userId",
},
value: userId,
},
},
},
select: selectPerson,
});
const userIdAttributeClassId = personWithUserIdAttribute?.attributes.find(
(attr) => attr.attributeClass.name === "userId" && attr.value === userId
)?.attributeClass.id;
if (!personWithUserIdAttribute) {
return null;
}
personWithUserIdAttribute = await prisma.person.update({
where: {
id: personWithUserIdAttribute.id,
},
data: {
userId,
attributes: {
deleteMany: { attributeClassId: userIdAttributeClassId },
},
},
select: selectPerson,
});
personCache.revalidate({
id: personWithUserIdAttribute.id,
environmentId,
userId,
});
return transformPrismaPerson(personWithUserIdAttribute);
return null;
},
[`getPersonByUserId-${environmentId}-${userId}`],
{
@@ -380,7 +337,7 @@ export const getPersonByUserId = async (environmentId: string, userId: string):
};
/**
* @deprecated This function is deprecated and only used in legacy endpoints. Use updatePerson instead.
* @deprecated This function is deprecated and only used in legacy endpoints. Use `updatePerson` instead.
*/
export const updatePersonAttribute = async (
personId: string,

View File

@@ -14,17 +14,6 @@ export const truncate = (str: string, length: number) => {
return str;
};
// write a function that takes a string and truncates the middle of it so that the beginning and ending are always visible
export const truncateMiddle = (str: string, length: number) => {
if (!str) return "";
if (str.length > length) {
const start = str.substring(0, length / 2);
const end = str.substring(str.length - length / 2, str.length);
return start + " ... " + end;
}
return str;
};
// write a function that takes a string and removes all characters that could cause issues with the url and truncates it to the specified length
export const sanitizeString = (str: string, delimiter: string = "_", length: number = 255) => {
return str.replace(/[^0-9a-zA-Z\-._]+/g, delimiter).substring(0, length);