diff --git a/api/db/loadout-share-queries.test.ts b/api/db/loadout-share-queries.test.ts deleted file mode 100644 index 1ef38b9..0000000 --- a/api/db/loadout-share-queries.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { v4 as uuid } from 'uuid'; -import { Loadout, LoadoutItem } from '../shapes/loadouts.js'; -import { closeDbPool, transaction } from './index.js'; -import { addLoadoutShare, getLoadoutShare, recordAccess } from './loadout-share-queries.js'; - -const appId = 'settings-queries-test-app'; -const bungieMembershipId = 4321; -const platformMembershipId = '213512057'; - -const shareID = 'ABCDEFG'; - -beforeEach(() => - transaction(async (client) => { - await client.query("delete from loadout_shares where id = 'ABCDEFG'"); - }), -); - -afterAll(() => closeDbPool()); - -const loadout: Loadout = { - id: uuid(), - name: 'Test Loadout', - classType: 1, - clearSpace: false, - equipped: [ - { - hash: 100, - id: '1234', - socketOverrides: { 7: 9 }, - }, - ], - unequipped: [ - // This item has an extra property which shouldn't be saved - { - hash: 200, - id: '5678', - amount: 10, - fizbuzz: 11, - } as any as LoadoutItem, - ], -}; - -it('can record a shared loadout', async () => { - await transaction(async (client) => { - await addLoadoutShare( - client, - appId, - bungieMembershipId, - platformMembershipId, - shareID, - loadout, - ); - - const sharedLoadout = await getLoadoutShare(client, shareID); - - expect(sharedLoadout?.name).toBe(loadout.name); - }); -}); - -it('rejects multiple shares with the same ID', async () => { - await transaction(async (client) => { - await addLoadoutShare( - client, - appId, - bungieMembershipId, - platformMembershipId, - shareID, - loadout, - ); - - try { - await addLoadoutShare( - client, - appId, - bungieMembershipId, - platformMembershipId, - shareID, - loadout, - ); - fail('Expected this to throw an error'); - } catch {} - }); -}); - -it('can record visits', async () => { - await transaction(async (client) => { - await addLoadoutShare( - client, - appId, - bungieMembershipId, - platformMembershipId, - shareID, - loadout, - ); - - await recordAccess(client, shareID); - }); -}); diff --git a/api/db/loadout-share-queries.ts b/api/db/loadout-share-queries.ts deleted file mode 100644 index cc73425..0000000 --- a/api/db/loadout-share-queries.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { ClientBase, QueryResult } from 'pg'; -import { metrics } from '../metrics/index.js'; -import { Loadout } from '../shapes/loadouts.js'; -import { cleanItem, convertLoadout, LoadoutRow } from './loadouts-queries.js'; - -/** - * Get a specific loadout share by its share ID. - */ -export async function getLoadoutShare( - client: ClientBase, - shareId: string, -): Promise { - const results = await client.query({ - name: 'get_loadout_share', - text: 'SELECT id, name, notes, class_type, emblem_hash, clear_space, items, parameters, created_at FROM loadout_shares WHERE id = $1', - values: [shareId], - }); - if (results.rowCount === 1) { - return convertLoadout(results.rows[0]); - } else { - return undefined; - } -} - -/** - * Create a new loadout share. These are intended to be immutable. - */ -export async function addLoadoutShare( - client: ClientBase, - appId: string, - bungieMembershipId: number, - platformMembershipId: string, - shareId: string, - loadout: Loadout, -): Promise { - const response = await client.query({ - name: 'add_loadout_share', - text: `insert into loadout_shares (id, membership_id, platform_membership_id, name, notes, class_type, emblem_hash, clear_space, items, parameters, created_by) -values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`, - values: [ - shareId, - bungieMembershipId, - platformMembershipId, - loadout.name, - loadout.notes, - loadout.classType, - loadout.emblemHash || null, - loadout.clearSpace, - { - equipped: loadout.equipped.map(cleanItem), - unequipped: loadout.unequipped.map(cleanItem), - }, - loadout.parameters, - appId, - ], - }); - - if (response.rowCount! < 1) { - // This should never happen! - metrics.increment('db.loadoutShares.noRowUpdated.count', 1); - throw new Error('loadout share - No row was updated'); - } - - return response; -} - -/** - * Touch the last_accessed_at and visits fields to keep track of access. - */ -export async function recordAccess(client: ClientBase, shareId: string): Promise { - const response = await client.query({ - name: 'loadout_share_record_access', - text: `update loadout_shares set last_accessed_at = current_timestamp, visits = visits + 1 where id = $1`, - values: [shareId], - }); - - if (response.rowCount! < 1) { - // This should never happen! - metrics.increment('db.loadoutShares.noRowUpdated.count', 1); - throw new Error('loadout share - No row was updated'); - } - - return response; -} diff --git a/api/routes/loadout-share.ts b/api/routes/loadout-share.ts index c3d274d..3e714ce 100644 --- a/api/routes/loadout-share.ts +++ b/api/routes/loadout-share.ts @@ -1,8 +1,6 @@ import crypto from 'crypto'; import asyncHandler from 'express-async-handler'; import base32 from 'hi-base32'; -import { transaction } from '../db/index.js'; -import { getLoadoutShare, recordAccess } from '../db/loadout-share-queries.js'; import { metrics } from '../metrics/index.js'; import { ApiApp } from '../shapes/app.js'; import { @@ -54,7 +52,7 @@ export const loadoutShareHandler = asyncHandler(async (req, res) => { }); } - const validationResult = validateLoadout('loadout_share', loadout, appId); + const validationResult = validateLoadout('loadout_share', loadout); if (validationResult) { res.status(400).send(validationResult); return; @@ -117,25 +115,10 @@ export const getLoadoutShareHandler = asyncHandler(async (req, res) => { }); export async function loadLoadoutShare(shareId: string) { - // First look in Stately - try { - const loadout = await getLoadoutShareStately(shareId); - if (loadout) { - // Record when this was viewed and increment the view counter. Not using it much for now but I'd like to know. - await recordAccessStately(shareId); - return loadout; - } - } catch (e) { - console.error('Failed to load loadout share from Stately', e); - } - - // Fall back to Postgres - return transaction(async (client) => { - const loadout = await getLoadoutShare(client, shareId); - if (loadout) { - // Record when this was viewed and increment the view counter. Not using it much for now but I'd like to know. - await recordAccess(client, shareId); - } + const loadout = await getLoadoutShareStately(shareId); + if (loadout) { + // Record when this was viewed and increment the view counter. Not using it much for now but I'd like to know. + await recordAccessStately(shareId); return loadout; - }); + } } diff --git a/api/routes/update.ts b/api/routes/update.ts index 99edac2..7507d49 100644 --- a/api/routes/update.ts +++ b/api/routes/update.ts @@ -232,7 +232,7 @@ function validateUpdates( break; case 'loadout': - result = validateUpdateLoadout(update.payload, appId); + result = validateUpdateLoadout(update.payload); break; case 'tag': @@ -514,11 +514,11 @@ async function updateLoadout( metrics.timing('update.loadout', start); } -function validateUpdateLoadout(loadout: Loadout, appId: string): ProfileUpdateResult { - return validateLoadout('update', loadout, appId) ?? { status: 'Success' }; +function validateUpdateLoadout(loadout: Loadout): ProfileUpdateResult { + return validateLoadout('update', loadout) ?? { status: 'Success' }; } -export function validateLoadout(metricPrefix: string, loadout: Loadout, appId: string) { +export function validateLoadout(metricPrefix: string, loadout: Loadout) { if (!loadout.name) { metrics.increment(`${metricPrefix}.validation.loadoutNameMissing.count`); return { diff --git a/api/shapes/profile.ts b/api/shapes/profile.ts index 79e1d32..d14b073 100644 --- a/api/shapes/profile.ts +++ b/api/shapes/profile.ts @@ -13,6 +13,10 @@ export interface ProfileResponse { /** Hashes of tracked triumphs */ triumphs?: number[]; searches?: Search[]; + + syncTokens?: { + [key: string]: string; + }; } /** diff --git a/api/stately/init/migrate-loadout-shares.ts b/api/stately/init/migrate-loadout-shares.ts new file mode 100644 index 0000000..a5c8108 --- /dev/null +++ b/api/stately/init/migrate-loadout-shares.ts @@ -0,0 +1,6 @@ +// import { migrateLoadoutShareChunk } from '../migrator/loadout-shares.js'; + +// while (true) { +// await migrateLoadoutShareChunk(); +// console.log('Migrated loadout shares'); +// } diff --git a/api/stately/loadout-share-queries.ts b/api/stately/loadout-share-queries.ts index bd18830..4c52e2b 100644 --- a/api/stately/loadout-share-queries.ts +++ b/api/stately/loadout-share-queries.ts @@ -1,4 +1,4 @@ -import { keyPath, StatelyError } from '@stately-cloud/client'; +import { keyPath, StatelyError, WithPutOptions } from '@stately-cloud/client'; import { Loadout } from '../shapes/loadouts.js'; import { client } from './client.js'; import { LoadoutShare as StatelyLoadoutShare } from './generated/index.js'; @@ -56,6 +56,28 @@ export async function addLoadoutShare( } } +/** + * Put loadout shares - this is meant for migrations. + */ +export async function addLoadoutSharesForMigration( + shares: { + platformMembershipId: string; + shareId: string; + loadout: Loadout; + }[], +): Promise { + const statelyShares = shares.map( + ({ platformMembershipId, shareId, loadout }): WithPutOptions => ({ + item: convertLoadoutShareToStately(loadout, platformMembershipId, shareId), + // Preserve the original timestamps + overwriteMetadataTimestamps: true, + }), + ); + + // We overwrite here - shares are immutable, so this is fine. + await client.putBatch(...statelyShares); +} + /** * Touch the last_accessed_at and visits fields to keep track of access. */ diff --git a/api/stately/loadouts-queries.ts b/api/stately/loadouts-queries.ts index 5e9fe27..6628d17 100644 --- a/api/stately/loadouts-queries.ts +++ b/api/stately/loadouts-queries.ts @@ -273,6 +273,8 @@ export function convertLoadoutCommonFieldsToStately( unequipped: (loadout.unequipped || []).map(convertLoadoutItemToStately), notes: loadout.notes, parameters: convertLoadoutParametersToStately(loadout.parameters), + createdAt: BigInt(loadout.createdAt ?? 0n), + lastUpdatedAt: BigInt(loadout.lastUpdatedAt ?? 0n), }; } diff --git a/api/stately/migrator/loadout-shares.ts b/api/stately/migrator/loadout-shares.ts new file mode 100644 index 0000000..9940d32 --- /dev/null +++ b/api/stately/migrator/loadout-shares.ts @@ -0,0 +1,15 @@ +// import { transaction } from '../../db/index.js'; +// import { deleteLoadoutShares, getLoadoutShares } from '../../db/loadout-share-queries.js'; +// import { addLoadoutSharesForMigration } from '../loadout-share-queries.js'; + +// export async function migrateLoadoutShareChunk() { +// await transaction(async (db) => { +// const loadouts = await getLoadoutShares(db, 50); +// await Promise.all(loadouts.map((loadout) => addLoadoutSharesForMigration([loadout]))); +// console.log('Added to stately'); +// await deleteLoadoutShares( +// db, +// loadouts.map((loadout) => loadout.shareId), +// ); +// }); +// } diff --git a/package.json b/package.json index 9dd42b2..22b94c7 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "@google-cloud/profiler": "^6.0.2", "@sentry/node": "^7.119.2", "@sentry/tracing": "^7.114.0", - "@stately-cloud/client": "^0.17.1", + "@stately-cloud/client": "^0.19.0", "bungie-api-ts": "^5.1.0", "cors": "^2.8.5", "dotenv": "^16.4.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e40190..0de1cf0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,8 +21,8 @@ dependencies: specifier: ^7.114.0 version: 7.114.0 '@stately-cloud/client': - specifier: ^0.17.1 - version: 0.17.1(@bufbuild/protobuf@2.2.2) + specifier: ^0.19.0 + version: 0.19.0(@bufbuild/protobuf@2.2.2) bungie-api-ts: specifier: ^5.1.0 version: 5.1.0 @@ -2656,8 +2656,8 @@ packages: '@sinonjs/commons': 3.0.1 dev: true - /@stately-cloud/client@0.17.1(@bufbuild/protobuf@2.2.2): - resolution: {integrity: sha512-R6P99w1DxNUSztuSMVyvKxnnGeWNcaAyp9+oC0n/sLl8G+zTmT564QBsPHJDHLvrfQVjkTRQqovtdqVcgflXkg==} + /@stately-cloud/client@0.19.0(@bufbuild/protobuf@2.2.2): + resolution: {integrity: sha512-uvHXW/v/1W118h8SsbYScYulENScGCzt18/9wsPil0YcI8GhmBSsBUc3rKNubn34YPEJHZwphafug8bTztx+Ng==} engines: {node: '>=18'} peerDependencies: '@bufbuild/protobuf': ^2.2.0