Files
pwa/backend/scripts/recover-user-from-novya-test.ts
T
ar.badieiandCursor 370ac15471 Ship Novya PWA with backend for deployable production use.
Fix production TypeScript builds and add DEPLOY.md so the app can be hosted from this repo.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-03 21:01:52 +03:30

267 lines
8.9 KiB
TypeScript

/**
* One-shot recovery: copy one password user (and that user's graph)
* from novya_test into live novya. Does not copy auth_sessions.
* Does not print password hashes or API keys.
*/
import { Prisma, PrismaClient } from '@prisma/client';
const SOURCE_URL = 'mysql://novya:novya@127.0.0.1:3306/novya_test';
const DEST_URL = 'mysql://novya:novya@127.0.0.1:3306/novya';
const TARGET_EMAIL = 'ahmadreza6137@gmail.com';
function client(url: string) {
return new PrismaClient({
datasources: { db: { url } },
log: ['error'],
});
}
function jsonValue(value: Prisma.JsonValue | null | undefined): Prisma.InputJsonValue | undefined {
if (value === null || value === undefined) return undefined;
return value as Prisma.InputJsonValue;
}
async function main() {
const source = client(SOURCE_URL);
const dest = client(DEST_URL);
try {
const identity = await source.authIdentity.findFirst({
where: {
provider: 'password',
email: TARGET_EMAIL,
},
});
if (!identity) {
throw new Error(`Source password identity not found for target email`);
}
const sourceUser = await source.user.findUniqueOrThrow({
where: { id: identity.userId },
include: {
profile: true,
identities: true,
journey: true,
assessmentAttempts: {
include: { answers: true, scores: true },
},
sessionProgress: true,
chatSessions: {
include: { messages: true },
},
aiRuns: true,
},
});
const destEmailClash = await dest.authIdentity.findFirst({
where: {
provider: 'password',
email: TARGET_EMAIL,
},
});
if (destEmailClash) {
throw new Error('Target email already exists in novya — aborting to avoid overwrite');
}
const destExternalClash = await dest.user.findUnique({
where: { externalId: sourceUser.externalId },
});
if (destExternalClash) {
throw new Error('externalId already exists in novya — aborting to avoid overwrite');
}
const summary = await dest.$transaction(
async (tx) => {
const createdUser = await tx.user.create({
data: {
externalId: sourceUser.externalId,
isGuest: sourceUser.isGuest,
createdAt: sourceUser.createdAt,
updatedAt: sourceUser.updatedAt,
},
});
if (sourceUser.profile) {
await tx.userProfile.create({
data: {
userId: createdUser.id,
name: sourceUser.profile.name,
age: sourceUser.profile.age,
createdAt: sourceUser.profile.createdAt,
updatedAt: sourceUser.profile.updatedAt,
},
});
}
for (const item of sourceUser.identities) {
await tx.authIdentity.create({
data: {
userId: createdUser.id,
provider: item.provider,
providerSubject: item.providerSubject,
email: item.email,
passwordHash: item.passwordHash,
createdAt: item.createdAt,
updatedAt: item.updatedAt,
},
});
}
if (sourceUser.journey) {
await tx.learningJourney.create({
data: {
userId: createdUser.id,
currentStep: sourceUser.journey.currentStep,
preTestCompletedAt: sourceUser.journey.preTestCompletedAt,
trainingStartedAt: sourceUser.journey.trainingStartedAt,
trainingCompletedAt: sourceUser.journey.trainingCompletedAt,
postTestCompletedAt: sourceUser.journey.postTestCompletedAt,
lastActivityAt: sourceUser.journey.lastActivityAt,
createdAt: sourceUser.journey.createdAt,
updatedAt: sourceUser.journey.updatedAt,
},
});
}
for (const row of sourceUser.sessionProgress) {
await tx.sessionProgress.create({
data: {
userId: createdUser.id,
sessionOrder: row.sessionOrder,
status: row.status,
startedAt: row.startedAt,
completedAt: row.completedAt,
completionMetadata: jsonValue(row.completionMetadata),
createdAt: row.createdAt,
updatedAt: row.updatedAt,
},
});
}
const attemptIdMap = new Map<bigint, bigint>();
for (const attempt of sourceUser.assessmentAttempts) {
const createdAttempt = await tx.assessmentAttempt.create({
data: {
userId: createdUser.id,
phase: attempt.phase,
status: attempt.status,
submittedAt: attempt.submittedAt,
scoredAt: attempt.scoredAt,
createdAt: attempt.createdAt,
updatedAt: attempt.updatedAt,
answers: {
create: attempt.answers.map((answer) => ({
questionId: answer.questionId,
rawValue: answer.rawValue,
points: answer.points,
dimension: answer.dimension,
isReverseScored: answer.isReverseScored,
createdAt: answer.createdAt,
})),
},
scores: {
create: attempt.scores.map((score) => ({
metricKey: score.metricKey,
rawScore: score.rawScore,
createdAt: score.createdAt,
})),
},
},
});
attemptIdMap.set(attempt.id, createdAttempt.id);
}
const chatIdMap = new Map<bigint, bigint>();
for (const session of sourceUser.chatSessions) {
const createdSession = await tx.chatSession.create({
data: {
userId: createdUser.id,
sessionOrder: session.sessionOrder,
startedAt: session.startedAt,
endedAt: session.endedAt,
createdAt: session.createdAt,
updatedAt: session.updatedAt,
},
});
chatIdMap.set(session.id, createdSession.id);
if (session.messages.length > 0) {
await tx.chatMessage.createMany({
data: session.messages.map((message) => ({
chatSessionId: createdSession.id,
role: message.role,
content: message.content,
metadata: jsonValue(message.metadata) ?? Prisma.JsonNull,
createdAt: message.createdAt,
})),
});
}
}
for (const run of sourceUser.aiRuns) {
const mappedChatId = run.chatSessionId ? chatIdMap.get(run.chatSessionId) : undefined;
await tx.aiAnalysisRun.create({
data: {
userId: createdUser.id,
chatSessionId: mappedChatId ?? null,
provider: run.provider,
model: run.model,
status: run.status,
latencyMs: run.latencyMs,
totalTokens: run.totalTokens,
errorMessage: run.errorMessage,
createdAt: run.createdAt,
updatedAt: run.updatedAt,
},
});
}
const destIdentity = await tx.authIdentity.findFirstOrThrow({
where: {
userId: createdUser.id,
provider: 'password',
email: TARGET_EMAIL,
},
});
const hashCopied =
Boolean(identity.passwordHash) && destIdentity.passwordHash === identity.passwordHash;
const hashIsArgon2 = Boolean(destIdentity.passwordHash?.startsWith('$argon2id$'));
return {
newUserId: createdUser.id.toString(),
externalId: createdUser.externalId,
profile: Boolean(sourceUser.profile),
identities: sourceUser.identities.length,
authSessionsCopied: 0,
journeyStep: sourceUser.journey?.currentStep ?? null,
preTestAt: sourceUser.journey?.preTestCompletedAt?.toISOString() ?? null,
attempts: sourceUser.assessmentAttempts.length,
answers: sourceUser.assessmentAttempts.reduce((n, a) => n + a.answers.length, 0),
scores: sourceUser.assessmentAttempts.reduce((n, a) => n + a.scores.length, 0),
progressRows: sourceUser.sessionProgress.length,
chatSessions: sourceUser.chatSessions.length,
chatMessages: sourceUser.chatSessions.reduce((n, s) => n + s.messages.length, 0),
aiRuns: sourceUser.aiRuns.length,
hashCopied,
hashIsArgon2,
attemptIdRemaps: attemptIdMap.size,
chatIdRemaps: chatIdMap.size,
};
},
{ maxWait: 15_000, timeout: 60_000 },
);
console.log(JSON.stringify({ ok: true, email: TARGET_EMAIL, ...summary }, null, 2));
} finally {
await source.$disconnect();
await dest.$disconnect();
}
}
main().catch((err) => {
console.error(err instanceof Error ? err.message : err);
process.exit(1);
});