Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User import #260

Merged
merged 9 commits into from
Jan 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/db/item-annotations-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export async function getAllItemAnnotationsForUser(
ItemAnnotationRow & { platform_membership_id: string; destiny_version: DestinyVersion }
>({
name: 'get_all_item_annotations',
text: 'SELECT platform_membership_id, destiny_version, inventory_item_id, tag, notes, variant, crafted_date FROM item_annotations WHERE membership_id = $1',
text: 'SELECT platform_membership_id, destiny_version, inventory_item_id, tag, notes, variant, crafted_date FROM item_annotations WHERE inventory_item_id != 0 and membership_id = $1',
values: [bungieMembershipId],
});
return results.rows.map((row) => ({
Expand Down
70 changes: 0 additions & 70 deletions api/db/migration-state-queries.test.ts

This file was deleted.

12 changes: 10 additions & 2 deletions api/db/migration-state-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ interface MigrationStateRow {
last_error: string | null;
}

export async function getUsersToMigrate(client: ClientBase): Promise<number[]> {
const results = await client.query<MigrationStateRow>({
name: 'get_users_to_migrate',
text: 'select membership_id from migration_state where state != 3 limit 1000',
});
return results.rows.map((row) => row.membership_id);
}

export async function getMigrationState(
client: ClientBase,
bungieMembershipId: number,
Expand All @@ -41,7 +49,7 @@ export async function getMigrationState(
} else {
return {
bungieMembershipId,
state: MigrationState.Postgres,
state: MigrationState.Stately,
lastStateChangeAt: 0,
attemptCount: 0,
};
Expand Down Expand Up @@ -206,7 +214,7 @@ export async function doMigration(
});
metrics.increment('migration.finish.count');
} catch (e) {
console.error('Stately migration failed', e);
console.error(`Stately migration failed for ${bungieMembershipId}`, e);
await transaction(async (client) => {
await abortMigrationToStately(
client,
Expand Down
127 changes: 3 additions & 124 deletions api/routes/import.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,12 @@
import { isEmpty } from 'es-toolkit/compat';
import asyncHandler from 'express-async-handler';
import { readTransaction, transaction } from '../db/index.js';
import { updateItemAnnotation } from '../db/item-annotations-queries.js';
import { updateItemHashTag } from '../db/item-hash-tags-queries.js';
import { updateLoadout } from '../db/loadouts-queries.js';
import {
doMigration,
getDesiredMigrationState,
getMigrationState,
MigrationState,
} from '../db/migration-state-queries.js';
import { importSearch } from '../db/searches-queries.js';
import { replaceSettings } from '../db/settings-queries.js';
import { trackTriumph } from '../db/triumphs-queries.js';
import { ApiApp } from '../shapes/app.js';
import { readTransaction } from '../db/index.js';
import { doMigration, getMigrationState, MigrationState } from '../db/migration-state-queries.js';
import { ExportResponse } from '../shapes/export.js';
import { DestinyVersion } from '../shapes/general.js';
import { ImportResponse } from '../shapes/import.js';
import { ItemAnnotation, ItemHashTag } from '../shapes/item-annotations.js';
import { Loadout } from '../shapes/loadouts.js';
import { SearchType } from '../shapes/search.js';
import { defaultSettings, Settings } from '../shapes/settings.js';
import { UserInfo } from '../shapes/user.js';
import { deleteAllDataForUser } from '../stately/bulk-queries.js';
Expand All @@ -33,11 +20,9 @@ import { convertToStatelyItem } from '../stately/settings-queries.js';
import { batches } from '../stately/stately-utils.js';
import { importTriumphs } from '../stately/triumphs-queries.js';
import { badRequest, delay, subtractObject } from '../utils.js';
import { deleteAllData } from './delete-all-data.js';

export const importHandler = asyncHandler(async (req, res) => {
const { bungieMembershipId, profileIds } = req.user as UserInfo;
const { id: appId } = req.dimApp as ApiApp;

// Support only new API exports
const importData = req.body as ExportResponse;
Expand All @@ -60,12 +45,6 @@ export const importHandler = asyncHandler(async (req, res) => {
getMigrationState(client, bungieMembershipId),
);

// this is a great time to do the migration
const desiredMigrationState = await getDesiredMigrationState(migrationState);
const shouldMigrateToStately =
desiredMigrationState === MigrationState.Stately &&
migrationState.state !== desiredMigrationState;

let numTriumphs = 0;
const importToStately = async () => {
numTriumphs = await statelyImport(
Expand All @@ -82,22 +61,7 @@ export const importHandler = asyncHandler(async (req, res) => {

switch (migrationState.state) {
case MigrationState.Postgres:
if (shouldMigrateToStately) {
await doMigration(bungieMembershipId, importToStately, async (client) =>
deleteAllData(client, bungieMembershipId),
);
} else {
numTriumphs = await pgImport(
bungieMembershipId,
appId,
settings,
loadouts,
itemAnnotations,
triumphs,
searches,
itemHashTags,
);
}
await doMigration(bungieMembershipId, importToStately);
break;
case MigrationState.Stately:
await importToStately();
Expand Down Expand Up @@ -138,90 +102,6 @@ export function extractImportData(importData: ExportResponse) {
};
}

async function pgImport(
bungieMembershipId: number,
appId: string,
settings: Partial<Settings>,
loadouts: PlatformLoadout[],
itemAnnotations: PlatformItemAnnotation[],
triumphs: ExportResponse['triumphs'],
searches: ExportResponse['searches'],
itemHashTags: ItemHashTag[],
): Promise<number> {
let numTriumphs = 0;
await transaction(async (client) => {
await deleteAllData(client, bungieMembershipId);

// TODO: pass a list of keys that are being set to default?
await replaceSettings(client, appId, bungieMembershipId, settings);

// TODO: query first so we can delete after?
for (const loadout of loadouts) {
// For now, ignore ancient loadouts
if (!loadout.platformMembershipId || !loadout.destinyVersion) {
continue;
}
await updateLoadout(
client,
appId,
bungieMembershipId,
loadout.platformMembershipId,
loadout.destinyVersion,
loadout,
);
}

// TODO: query first so we can delete after?
for (const annotation of itemAnnotations) {
await updateItemAnnotation(
client,
appId,
bungieMembershipId,
annotation.platformMembershipId,
annotation.destinyVersion,
annotation,
);
}

for (const tag of itemHashTags) {
await updateItemHashTag(client, appId, bungieMembershipId, tag);
}

if (Array.isArray(triumphs)) {
for (const triumphData of triumphs) {
if (Array.isArray(triumphData?.triumphs)) {
for (const triumph of triumphData.triumphs) {
trackTriumph(
client,
appId,
bungieMembershipId,
triumphData.platformMembershipId,
triumph,
);
numTriumphs++;
}
}
}
}

for (const search of searches) {
importSearch(
client,
appId,
bungieMembershipId,
search.destinyVersion,
search.search.query,
search.search.saved,
search.search.lastUsage,
search.search.usageCount,
search.search.type ?? SearchType.Item,
);
}
});

return numTriumphs;
}

export async function statelyImport(
bungieMembershipId: number,
platformMembershipIds: string[],
Expand Down Expand Up @@ -271,7 +151,6 @@ export async function statelyImport(
// Put the settings in first since it's in a different group
await client.put({
item: settingsItem,
mustNotExist: true,
});
// OK now put them in as fast as we can
for (const batch of batches(items)) {
Expand Down
2 changes: 1 addition & 1 deletion api/routes/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export const updateHandler = asyncHandler(async (req, res) => {
triumphs,
searches,
itemHashTags,
!migrationState.lastError,
migrationState.attemptCount > 0 || Boolean(migrationState.lastError),
);
};

Expand Down
34 changes: 34 additions & 0 deletions api/stately/init/migrate-users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { chunk } from 'es-toolkit';
import { readTransaction } from '../../db/index.js';
import { getUsersToMigrate } from '../../db/migration-state-queries.js';
import { delay } from '../../utils.js';
import { migrateUser } from '../migrator/user.js';

while (true) {
try {
const bungieMembershipIds = await readTransaction(async (client) => getUsersToMigrate(client));
if (bungieMembershipIds.length === 0) {
console.log('No users to migrate');
break;
}
for (const idChunk of chunk(bungieMembershipIds, 10)) {
await Promise.all(
idChunk.map(async (bungieMembershipId) => {
try {
await migrateUser(bungieMembershipId);
console.log(`Migrated user ${bungieMembershipId}`);
} catch (e) {
if (e instanceof Error) {
console.error(`Error migrating user ${bungieMembershipId}: ${e}`);
}
}
}),
);
}
} catch (e) {
if (e instanceof Error) {
console.error(`Error getting users to migrate: ${e}`);
}
await delay(1000);
}
}
18 changes: 11 additions & 7 deletions api/stately/loadouts-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,18 +264,22 @@ export function convertLoadoutCommonFieldsToStately(
MessageInitShape<typeof LoadoutSchema> | MessageInitShape<typeof LoadoutShareSchema>,
'id' | '$typeName'
> {
return {
const out = {
destinyVersion,
profileId: BigInt(platformMembershipId),
name: loadout.name,
name: loadout.name || 'Unnamed',
classType: loadout.classType as number,
equipped: (loadout.equipped || []).map(convertLoadoutItemToStately),
unequipped: (loadout.unequipped || []).map(convertLoadoutItemToStately),
notes: loadout.notes,
parameters: convertLoadoutParametersToStately(loadout.parameters),
createdAt: BigInt(loadout.createdAt ?? 0n),
lastUpdatedAt: BigInt(loadout.lastUpdatedAt ?? 0n),
createdAt: BigInt(loadout.createdAt ? new Date(loadout.createdAt).getTime() : 0n),
lastUpdatedAt: BigInt(loadout.lastUpdatedAt ? new Date(loadout.lastUpdatedAt).getTime() : 0n),
};
if (out.lastUpdatedAt < out.createdAt) {
out.lastUpdatedAt = out.createdAt;
}
return out;
}

function convertLoadoutItemToStately(item: LoadoutItem): StatelyLoadoutItem {
Expand Down Expand Up @@ -316,7 +320,7 @@ export function convertLoadoutParametersToStately(
modsByBucket: modsByBucket
? Object.entries(modsByBucket).map(([bucketHash, modHashes]) => ({
bucketHash: Number(bucketHash),
modHashes,
modHashes: modHashes.filter((h) => Number.isInteger(h)),
}))
: undefined,
};
Expand All @@ -328,8 +332,8 @@ export function statConstraintsToStately(statConstraints: StatConstraint[] | und
return statConstraints && statConstraints.length > 0
? statConstraints.map((c) => ({
statHash: c.statHash,
minTier: Math.max(0, c.minTier ?? 0),
maxTier: Math.min(c.maxTier ?? 10, 10),
minTier: Math.max(0, Math.floor(c.minTier ?? 0)),
maxTier: Math.min(Math.ceil(c.maxTier ?? 10), 10),
}))
: [];
}
Expand Down
Loading
Loading