feat: 레거시 재이관과 계정 복구 기반을 추가

중앙 이전 기록 스키마와 버전 정규화기를 도입하고, 재실행 시 현행 계정 상태를 보존한다. 카카오 인증 뒤 이관 비밀번호를 1회 설정하는 흐름과 비카카오 계정용 안전한 CLI 복구 경로를 추가한다.
This commit is contained in:
2026-08-17 15:55:32 +00:00
parent 50e7d894e4
commit fc7de05017
33 changed files with 1720 additions and 150 deletions
+13 -3
View File
@@ -5,7 +5,7 @@ import path from 'node:path';
import process from 'node:process';
import { createMariaPool, createPostgresPool } from './db.js';
import { migrateGame } from './game.js';
import { isLegacyArchiveProfile, LEGACY_ARCHIVE_PROFILES, migrateGame } from './game.js';
import { migrateGateway } from './gateway.js';
import { hashPasswordForReset } from './password.js';
import { migrateCurrentSeasonFixture } from './currentSeason.js';
@@ -122,7 +122,10 @@ const resetPassword = async (options: CliOptions): Promise<Record<string, unknow
const hashed = await hashPasswordForReset(password);
await pool.query(
`UPDATE "app_user"
SET "password_hash" = $1, "password_salt" = $2, "updated_at" = CURRENT_TIMESTAMP
SET "password_hash" = $1,
"password_salt" = $2,
"password_reset_required" = FALSE,
"updated_at" = CURRENT_TIMESTAMP
WHERE "id" = $3`,
[hashed.hash, hashed.salt, existing.rows[0]!.id]
);
@@ -142,7 +145,11 @@ const run = async (): Promise<void> => {
const migratedAt = new Date();
if (options.command === 'gateway') {
const source = createMariaPool(requireEnvironment('LEGACY_ROOT_DATABASE_URL'));
const target = options.apply ? createPostgresPool(requireEnvironment('GATEWAY_DATABASE_URL')) : null;
const targetUrl = process.env.GATEWAY_DATABASE_URL?.trim();
if (options.apply && !targetUrl) {
throw new Error('GATEWAY_DATABASE_URL is required with --apply');
}
const target = targetUrl ? createPostgresPool(targetUrl) : null;
try {
const summary = await migrateGateway(source, target, options.apply, migratedAt);
console.log(JSON.stringify(summary, null, 2));
@@ -156,6 +163,9 @@ const run = async (): Promise<void> => {
if (!options.profile || !/^[a-z][a-z0-9_-]{1,31}$/.test(options.profile)) {
throw new Error(`${options.command} requires a safe --profile value\n\n${usage}`);
}
if (options.command === 'game' && !isLegacyArchiveProfile(options.profile)) {
throw new Error(`game requires --profile ${LEGACY_ARCHIVE_PROFILES.join('|')}\n\n${usage}`);
}
const source = createMariaPool(requireEnvironment('LEGACY_GAME_DATABASE_URL'));
const target =
options.apply || options.command === 'current-season-fixture'
+13 -3
View File
@@ -24,6 +24,14 @@ const quoteIdentifier = (value: string): string => {
return `"${value}"`;
};
export const quoteQualifiedIdentifier = (value: string): string => {
const parts = value.split('.');
if (parts.length < 1 || parts.length > 2 || parts.some((part) => !IDENTIFIER.test(part))) {
throw new Error(`Unsafe SQL identifier: ${value}`);
}
return parts.map((part) => `"${part}"`).join('.');
};
export const createMariaPool = (uri: string): MariaPool => mariadb.createPool(uri);
export const createPostgresPool = (connectionString: string): pg.Pool => {
@@ -94,7 +102,8 @@ export const upsertRows = async (
client: PoolClient,
table: string,
rows: readonly TargetRow[],
conflictColumns: readonly string[]
conflictColumns: readonly string[],
options: { preserveOnConflict?: readonly string[] } = {}
): Promise<void> => {
if (rows.length === 0) {
return;
@@ -116,12 +125,13 @@ export const upsertRows = async (
});
return `(${placeholders.join(', ')})`;
});
const preserved = new Set(options.preserveOnConflict ?? []);
const updates = columns
.filter((column) => !conflictColumns.includes(column))
.filter((column) => !conflictColumns.includes(column) && !preserved.has(column))
.map((column) => `${quoteIdentifier(column)} = EXCLUDED.${quoteIdentifier(column)}`);
const conflictAction = updates.length ? `DO UPDATE SET ${updates.join(', ')}` : 'DO NOTHING';
await client.query(
`INSERT INTO ${quoteIdentifier(table)} (${columns.map(quoteIdentifier).join(', ')})
`INSERT INTO ${quoteQualifiedIdentifier(table)} (${columns.map(quoteIdentifier).join(', ')})
VALUES ${tuples.join(', ')}
ON CONFLICT (${conflictColumns.map(quoteIdentifier).join(', ')}) ${conflictAction}`,
values
+165 -57
View File
@@ -3,6 +3,16 @@ import { createHash } from 'node:crypto';
import type { Pool as MariaPool } from 'mariadb';
import type { Pool as PgPool, PoolClient } from 'pg';
import {
isLegacyArchiveProfile,
normalizeArchivedGeneral,
type ArchivedGeneralSourceFormat,
type ArchivedJsonValue,
type LegacyArchiveProfile,
} from '@sammo-ts/common';
export { isLegacyArchiveProfile, LEGACY_ARCHIVE_PROFILES, type LegacyArchiveProfile } from '@sammo-ts/common';
import {
paginateSource,
jsonParameter,
@@ -29,6 +39,12 @@ import {
const batchSize = 250;
interface ArchiveMigrationContext {
profile: LegacyArchiveProfile;
importRunId: string;
sourceFormats: Record<ArchivedGeneralSourceFormat, number>;
}
const parseNullableJson = (value: unknown, fallback: JsonValue, context: string): JsonValue =>
value === null || value === undefined ? fallback : parseJson(value, context);
@@ -43,17 +59,17 @@ const ownerId = (value: unknown): string | null => {
return memberNo > 0 ? legacyUserId(memberNo) : null;
};
const hashYearbook = (row: TargetRow): string =>
createHash('sha256')
.update(
JSON.stringify({
map: row.map,
nations: row.nations,
globalHistory: row.global_history,
globalAction: row.global_action,
})
)
.digest('hex');
const hashYearbook = (map: JsonValue, nations: JsonValue, globalHistory: JsonValue, globalAction: JsonValue): string =>
createHash('sha256').update(JSON.stringify({ map, nations, globalHistory, globalAction })).digest('hex');
const asJsonRecord = (value: JsonValue): Record<string, JsonValue> =>
value !== null && !Array.isArray(value) && typeof value === 'object' ? value : {};
export const resolveLegacyGameOpenedAt = (env: JsonValue, legacyDate: Date, context: string): Date => {
const record = asJsonRecord(env);
const candidate = record.opentime ?? record.starttime;
return candidate === null || candidate === undefined || candidate === '' ? legacyDate : toDate(candidate, context);
};
const migrateSimpleTable = async (
source: MariaPool,
@@ -75,17 +91,24 @@ const migrateSimpleTable = async (
}
};
const migrateHall = (source: MariaPool, target: PoolClient | null, counts: Record<string, number>): Promise<void> =>
const migrateHall = (
source: MariaPool,
target: PoolClient | null,
counts: Record<string, number>,
archive: ArchiveMigrationContext
): Promise<void> =>
migrateSimpleTable(
source,
target,
'hall',
'id',
'hall',
['server_id', 'type', 'general_no'],
'legacy_archive.hall',
['source_profile', 'server_id', 'type', 'general_no'],
(row) => {
const sourceId = toNumber(row.id, 'hall.id');
return {
source_profile: archive.profile,
legacy_id: sourceId,
server_id: toStringValue(row.server_id, `hall.${sourceId}.server_id`),
season: toNumber(row.season, `hall.${sourceId}.season`),
scenario: toNumber(row.scenario, `hall.${sourceId}.scenario`),
@@ -94,24 +117,36 @@ const migrateHall = (source: MariaPool, target: PoolClient | null, counts: Recor
value: toFloat(row.value, `hall.${sourceId}.value`),
owner: ownerId(row.owner),
aux: parseJson(row.aux, `hall.${sourceId}.aux`),
import_run_id: archive.importRunId,
};
},
counts
);
const migrateGames = (source: MariaPool, target: PoolClient | null, counts: Record<string, number>): Promise<void> =>
const migrateGames = (
source: MariaPool,
target: PoolClient | null,
counts: Record<string, number>,
archive: ArchiveMigrationContext
): Promise<void> =>
migrateSimpleTable(
source,
target,
'ng_games',
'id',
'ng_games',
['server_id'],
'legacy_archive.game_history',
['source_profile', 'server_id'],
(row) => {
const sourceId = toNumber(row.id, 'ng_games.id');
const legacyDate = toDate(row.date, `ng_games.${sourceId}.date`);
const env = parseJson(row.env, `ng_games.${sourceId}.env`);
return {
source_profile: archive.profile,
server_id: toStringValue(row.server_id, `ng_games.${sourceId}.server_id`),
date: toDate(row.date, `ng_games.${sourceId}.date`),
legacy_id: sourceId,
opened_at: resolveLegacyGameOpenedAt(env, legacyDate, `ng_games.${sourceId}.opened_at`),
completed_at: null,
legacy_date: legacyDate,
winner_nation:
row.winner_nation === null
? null
@@ -120,7 +155,8 @@ const migrateGames = (source: MariaPool, target: PoolClient | null, counts: Reco
season: toNumber(row.season, `ng_games.${sourceId}.season`),
scenario: toNumber(row.scenario, `ng_games.${sourceId}.scenario`),
scenario_name: toStringValue(row.scenario_name, `ng_games.${sourceId}.scenario_name`),
env: parseJson(row.env, `ng_games.${sourceId}.env`),
raw_env: jsonParameter(env),
import_run_id: archive.importRunId,
};
},
counts
@@ -129,25 +165,36 @@ const migrateGames = (source: MariaPool, target: PoolClient | null, counts: Reco
const migrateOldGenerals = (
source: MariaPool,
target: PoolClient | null,
counts: Record<string, number>
counts: Record<string, number>,
archive: ArchiveMigrationContext
): Promise<void> =>
migrateSimpleTable(
source,
target,
'ng_old_generals',
'id',
'ng_old_generals',
['server_id', 'general_no'],
'legacy_archive.general',
['source_profile', 'server_id', 'general_no'],
(row) => {
const sourceId = toNumber(row.id, 'ng_old_generals.id');
const name = toStringValue(row.name, `ng_old_generals.${sourceId}.name`);
const rawData = parseJson(row.data, `ng_old_generals.${sourceId}.data`);
const normalized = normalizeArchivedGeneral(rawData as ArchivedJsonValue, name);
archive.sourceFormats[normalized.sourceFormat] += 1;
return {
source_profile: archive.profile,
server_id: toStringValue(row.server_id, `ng_old_generals.${sourceId}.server_id`),
general_no: toNumber(row.general_no, `ng_old_generals.${sourceId}.general_no`),
legacy_id: sourceId,
owner: ownerId(row.owner),
name: toStringValue(row.name, `ng_old_generals.${sourceId}.name`),
name,
last_yearmonth: toNumber(row.last_yearmonth, `ng_old_generals.${sourceId}.last_yearmonth`),
turntime: toDate(row.turntime, `ng_old_generals.${sourceId}.turntime`),
data: parseJson(row.data, `ng_old_generals.${sourceId}.data`),
schema_version: normalized.snapshot.schemaVersion,
source_format: normalized.sourceFormat,
data: jsonParameter(normalized.snapshot),
raw_data: jsonParameter(rawData),
import_run_id: archive.importRunId,
};
},
counts
@@ -156,41 +203,47 @@ const migrateOldGenerals = (
const migrateOldNations = (
source: MariaPool,
target: PoolClient | null,
counts: Record<string, number>
counts: Record<string, number>,
archive: ArchiveMigrationContext
): Promise<void> =>
migrateSimpleTable(
source,
target,
'ng_old_nations',
'id',
'ng_old_nations',
['server_id', 'nation', 'source_id'],
'legacy_archive.nation',
['source_profile', 'legacy_id'],
(row) => {
const sourceId = toNumber(row.id, 'ng_old_nations.id');
return {
source_profile: archive.profile,
legacy_id: sourceId,
server_id: toStringValue(row.server_id, `ng_old_nations.${sourceId}.server_id`),
nation: toNumber(row.nation, `ng_old_nations.${sourceId}.nation`),
source_id: sourceId,
data: parseJson(row.data, `ng_old_nations.${sourceId}.data`),
date: toDate(row.date, `ng_old_nations.${sourceId}.date`),
data: jsonParameter(parseJson(row.data, `ng_old_nations.${sourceId}.data`)),
archived_at: toDate(row.date, `ng_old_nations.${sourceId}.date`),
import_run_id: archive.importRunId,
};
},
counts
);
const migrateEmperors = (source: MariaPool, target: PoolClient | null, counts: Record<string, number>): Promise<void> =>
const migrateEmperors = (
source: MariaPool,
target: PoolClient | null,
counts: Record<string, number>,
archive: ArchiveMigrationContext
): Promise<void> =>
migrateSimpleTable(
source,
target,
'emperior',
'no',
'emperior',
['legacy_id'],
'legacy_archive.emperor',
['source_profile', 'legacy_id'],
(row) => {
const id = toNumber(row.no, 'emperior.no');
return {
legacy_id: id,
server_id: toNullableString(row.server_id),
const data = {
phase: toNullableString(row.phase),
nation_count: toNullableString(row.nation_count),
nation_name: toNullableString(row.nation_name),
@@ -232,6 +285,13 @@ const migrateEmperors = (source: MariaPool, target: PoolClient | null, counts: R
history: parseNullableJson(row.history, [], `emperior.${id}.history`),
aux: parseNullableJson(row.aux, {}, `emperior.${id}.aux`),
};
return {
source_profile: archive.profile,
legacy_id: id,
server_id: toNullableString(row.server_id),
data: jsonParameter(data),
import_run_id: archive.importRunId,
};
},
counts
);
@@ -295,30 +355,35 @@ const migrateUserRecords = (
const migrateYearbook = async (
source: MariaPool,
target: PoolClient | null,
counts: Record<string, number>
counts: Record<string, number>,
archive: ArchiveMigrationContext
): Promise<void> => {
await migrateSimpleTable(
source,
target,
'ng_history',
'no',
'yearbook_history',
['profile_name', 'year', 'month', 'source_id'],
'legacy_archive.yearbook',
['source_profile', 'legacy_id'],
(row) => {
const id = toNumber(row.no, 'ng_history.no');
const map = parseNullableJson(row.map, {}, `ng_history.${id}.map`);
const nations = parseNullableJson(row.nations, [], `ng_history.${id}.nations`);
const globalHistory = parseNullableJson(row.global_history, [], `ng_history.${id}.global_history`);
const globalAction = parseNullableJson(row.global_action, [], `ng_history.${id}.global_action`);
const mapped: TargetRow = {
source_profile: archive.profile,
legacy_id: id,
profile_name: toStringValue(row.server_id, `ng_history.${id}.server_id`),
source_id: id,
year: toNumber(row.year, `ng_history.${id}.year`),
month: toNumber(row.month, `ng_history.${id}.month`),
map: parseNullableJson(row.map, {}, `ng_history.${id}.map`),
nations: parseNullableJson(row.nations, [], `ng_history.${id}.nations`),
global_history: parseNullableJson(row.global_history, [], `ng_history.${id}.global_history`),
global_action: parseNullableJson(row.global_action, [], `ng_history.${id}.global_action`),
hash: '',
created_at: new Date(0),
map: jsonParameter(map),
nations: jsonParameter(nations),
global_history: jsonParameter(globalHistory),
global_action: jsonParameter(globalAction),
content_hash: hashYearbook(map, nations, globalHistory, globalAction),
import_run_id: archive.importRunId,
};
mapped.hash = hashYearbook(mapped);
return mapped;
},
counts,
@@ -388,7 +453,16 @@ export const migrateGame = async (
apply: boolean,
profile: string
): Promise<MigrationSummary> => {
if (!isLegacyArchiveProfile(profile)) {
throw new Error(`Unsupported legacy archive profile: ${profile}`);
}
const counts: Record<string, number> = {};
const sourceFormats: Record<ArchivedGeneralSourceFormat, number> = {
'legacy-flat-v0': 0,
'ref-flat-v1': 0,
'core-snapshot-v1': 0,
unknown: 0,
};
const excluded = {
general: 'Current-season actor state is intentionally not transferred.',
city: 'Current-season world state is intentionally not transferred.',
@@ -421,25 +495,59 @@ export const migrateGame = async (
'storage:season-state': 'Only inheritance_* and user_* long-lived namespaces are archived or projected.',
};
const client = apply && targetPool ? await targetPool.connect() : null;
let importRunId: string | null = null;
try {
const run = async (): Promise<void> => {
await migrateGames(source, client, counts);
await migrateHall(source, client, counts);
await migrateOldGenerals(source, client, counts);
await migrateOldNations(source, client, counts);
await migrateEmperors(source, client, counts);
const run = async (archive: ArchiveMigrationContext): Promise<void> => {
await migrateGames(source, client, counts, archive);
await migrateHall(source, client, counts, archive);
await migrateOldGenerals(source, client, counts, archive);
await migrateOldNations(source, client, counts, archive);
await migrateEmperors(source, client, counts, archive);
await migrateInheritanceResults(source, client, counts);
await migrateUserRecords(source, client, counts);
await migrateStorage(source, client, counts);
await migrateYearbook(source, client, counts);
await migrateYearbook(source, client, counts, archive);
};
if (client) {
await withMigrationLock(client, `sammo-legacy-game-v1:${profile}`, run);
await withMigrationLock(client, `sammo-legacy-archive-v2:${profile}`, async () => {
const created = await client.query<{ id: string }>(
`INSERT INTO "legacy_archive"."import_run" ("source_profile", "status")
VALUES ($1, 'RUNNING') RETURNING "id"`,
[profile]
);
importRunId = created.rows[0]?.id ?? null;
if (!importRunId) throw new Error('Failed to create legacy archive import run');
const archive: ArchiveMigrationContext = { profile, importRunId, sourceFormats };
await client.query('BEGIN');
try {
await run(archive);
await client.query(
`UPDATE "legacy_archive"."import_run"
SET "status" = 'COMPLETED', "finished_at" = CURRENT_TIMESTAMP,
"counts" = $2::jsonb, "source_format_summary" = $3::jsonb
WHERE "id" = $1`,
[importRunId, JSON.stringify(counts), JSON.stringify(sourceFormats)]
);
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
const message =
error instanceof Error ? error.message.slice(0, 2000) : String(error).slice(0, 2000);
await client.query(
`UPDATE "legacy_archive"."import_run"
SET "status" = 'FAILED', "finished_at" = CURRENT_TIMESTAMP,
"counts" = $2::jsonb, "source_format_summary" = $3::jsonb, "error" = $4
WHERE "id" = $1`,
[importRunId, JSON.stringify(counts), JSON.stringify(sourceFormats), message]
);
throw error;
}
});
} else {
await run();
await run({ profile, importRunId: '0', sourceFormats });
}
} finally {
client?.release();
}
return { command: 'game', apply, counts, excluded };
return { command: 'game', apply, counts, excluded, importRunId, sourceFormatSummary: sourceFormats };
};
+94 -14
View File
@@ -24,17 +24,83 @@ export interface MigrationSummary {
apply: boolean;
counts: Record<string, number>;
excluded: Record<string, string>;
importRunId?: string | null;
sourceFormatSummary?: Record<string, number>;
}
const batchSize = 500;
const mapMember = (row: SourceRow, migratedAt: Date, lastLoginAt: Date | null): TargetRow => {
export const MEMBER_PRESERVED_COLUMNS = [
'login_id',
'display_name',
'password_hash',
'password_salt',
'password_reset_required',
'roles',
'sanctions',
'oauth_type',
'oauth_id',
'email',
'oauth_info',
'picture',
'image_server',
'icon_updated_at',
'third_party_use',
'terms_accepted_at',
'privacy_accepted_at',
'kakao_verified_at',
'kakao_talk_verified_until',
'kakao_grace_started_at',
'delete_after',
'updated_at',
'last_login_at',
'created_at',
] as const;
export const preflightMemberConflicts = async (target: PoolClient, rows: readonly TargetRow[]): Promise<void> => {
if (rows.length === 0) return;
const ids = rows.map((row) => String(row.id));
const loginIds = rows.map((row) => String(row.login_id));
const displayNames = rows.map((row) => String(row.display_name));
const emails = rows.map((row) => row.email).filter((value): value is string => typeof value === 'string');
const existing = await target.query<{
id: string;
login_id: string;
display_name: string;
email: string | null;
}>(
`SELECT "id", "login_id", "display_name", "email"
FROM "app_user"
WHERE "id" = ANY($1::text[])
OR "login_id" = ANY($2::text[])
OR "display_name" = ANY($3::text[])
OR "email" = ANY($4::text[])`,
[ids, loginIds, displayNames, emails]
);
for (const row of rows) {
const id = String(row.id);
const collision = existing.rows.find(
(candidate) =>
candidate.id !== id &&
(candidate.login_id === row.login_id ||
candidate.display_name === row.display_name ||
(row.email !== null && candidate.email === row.email))
);
if (collision) {
throw new Error('Target account identity collision in legacy member batch');
}
}
};
export const mapMember = (row: SourceRow, migratedAt: Date, lastLoginAt: Date | null): TargetRow => {
const memberNo = toNumber(row.NO, 'member.NO');
const grade = toNumber(row.GRADE, `member.${memberNo}.GRADE`);
const acl = parseJson(row.acl, `member.${memberNo}.acl`);
const penalty = parseJson(row.penalty, `member.${memberNo}.penalty`);
const oauthInfo = parseJson(row.oauth_info, `member.${memberNo}.oauth_info`);
const oauthType = row.oauth_type === 'KAKAO' ? 'KAKAO' : 'NONE';
const oauthId = toNullableString(row.oauth_id)?.trim() || null;
const passwordHash = toStringValue(row.PW, `member.${memberNo}.PW`);
const legacyData: JsonValue = {
memberNo,
grade,
@@ -49,12 +115,13 @@ const mapMember = (row: SourceRow, migratedAt: Date, lastLoginAt: Date | null):
id: legacyUserId(memberNo),
login_id: toStringValue(row.ID, `member.${memberNo}.ID`).toLowerCase(),
display_name: toStringValue(row.NAME, `member.${memberNo}.NAME`),
password_hash: toStringValue(row.PW, `member.${memberNo}.PW`),
password_hash: passwordHash,
password_salt: toStringValue(row.salt, `member.${memberNo}.salt`),
password_reset_required: /^[a-f0-9]{128}$/i.test(passwordHash),
roles: jsonParameter(mapLegacyRoles(grade, acl)),
sanctions: jsonParameter(mapLegacySanctions(grade, penalty)),
oauth_type: oauthType,
oauth_id: toNullableString(row.oauth_id),
oauth_id: oauthId,
email: toNullableString(row.EMAIL)?.toLowerCase() ?? null,
oauth_info: jsonParameter(oauthInfo),
picture: toNullableString(row.PICTURE) ?? 'default.jpg',
@@ -63,9 +130,9 @@ const mapMember = (row: SourceRow, migratedAt: Date, lastLoginAt: Date | null):
third_party_use: toNumber(row.third_use ?? 0, `member.${memberNo}.third_use`) !== 0,
terms_accepted_at: null,
privacy_accepted_at: null,
kakao_verified_at: oauthType === 'KAKAO' ? migratedAt : null,
kakao_verified_at: oauthType === 'KAKAO' && oauthId ? migratedAt : null,
kakao_talk_verified_until:
oauthType === 'KAKAO'
oauthType === 'KAKAO' && oauthId
? toNullableDate(row.token_valid_until, `member.${memberNo}.token_valid_until`)
: null,
kakao_grace_started_at: migratedAt,
@@ -93,6 +160,7 @@ const loadLastLogins = async (source: MariaPool): Promise<Map<number, Date>> =>
const processMembers = async (
source: MariaPool,
target: PoolClient | null,
apply: boolean,
migratedAt: Date,
counts: Record<string, number>
): Promise<void> => {
@@ -103,7 +171,10 @@ const processMembers = async (
return mapMember(row, migratedAt, lastLogins.get(memberNo) ?? null);
});
if (target) {
await upsertRows(target, 'app_user', mapped, ['id']);
await preflightMemberConflicts(target, mapped);
}
if (target && apply) {
await upsertRows(target, 'app_user', mapped, ['id'], { preserveOnConflict: MEMBER_PRESERVED_COLUMNS });
}
counts.member = (counts.member ?? 0) + mapped.length;
}
@@ -206,17 +277,26 @@ export const migrateGateway = async (
login_token:
'Legacy bearer tokens, IP addresses, and expired sessions are not valid in the Redis session model.',
};
const client = apply && targetPool ? await targetPool.connect() : null;
const client = targetPool ? await targetPool.connect() : null;
try {
const run = async (): Promise<void> => {
await processMembers(source, client, migratedAt, counts);
await processMemberLogs(source, client, counts);
await processBannedMembers(source, client, counts);
await processRootKeyValues(source, client, counts);
await processSystem(source, client, counts);
await processMembers(source, client, apply, migratedAt, counts);
await processMemberLogs(source, apply ? client : null, counts);
await processBannedMembers(source, apply ? client : null, counts);
await processRootKeyValues(source, apply ? client : null, counts);
await processSystem(source, apply ? client : null, counts);
};
if (client) {
await withMigrationLock(client, 'sammo-legacy-gateway-v1', run);
if (client && apply) {
await withMigrationLock(client, 'sammo-legacy-gateway-v1', async () => {
await client.query('BEGIN');
try {
await run();
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
}
});
} else {
await run();
}