Skip to content

Commit

Permalink
Track the type of saved searches
Browse files Browse the repository at this point in the history
  • Loading branch information
bhollis committed May 21, 2024
1 parent caa3518 commit eceead1
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 35 deletions.
58 changes: 45 additions & 13 deletions api/db/searches-queries.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SearchType } from '../shapes/search.js';
import { pool, transaction } from './index.js';
import {
deleteAllSearches,
Expand All @@ -22,7 +23,7 @@ afterAll(() => pool.end());

it('can record a used search where none was recorded before', async () => {
await transaction(async (client) => {
await updateUsedSearch(client, appId, bungieMembershipId, 2, 'tag:junk');
await updateUsedSearch(client, appId, bungieMembershipId, 2, 'tag:junk', SearchType.Item);

const searches = (await getSearchesForProfile(client, bungieMembershipId, 2)).filter(
(s) => s.usageCount > 0,
Expand All @@ -35,8 +36,8 @@ it('can record a used search where none was recorded before', async () => {

it('can track search multiple times', async () => {
await transaction(async (client) => {
await updateUsedSearch(client, appId, bungieMembershipId, 2, 'tag:junk');
await updateUsedSearch(client, appId, bungieMembershipId, 2, 'tag:junk');
await updateUsedSearch(client, appId, bungieMembershipId, 2, 'tag:junk', SearchType.Item);
await updateUsedSearch(client, appId, bungieMembershipId, 2, 'tag:junk', SearchType.Item);

const searches = (await getSearchesForProfile(client, bungieMembershipId, 2)).filter(
(s) => s.usageCount > 0,
Expand All @@ -49,8 +50,8 @@ it('can track search multiple times', async () => {

it('can mark a search as favorite', async () => {
await transaction(async (client) => {
await updateUsedSearch(client, appId, bungieMembershipId, 2, 'tag:junk');
await saveSearch(client, appId, bungieMembershipId, 2, 'tag:junk', true);
await updateUsedSearch(client, appId, bungieMembershipId, 2, 'tag:junk', SearchType.Item);
await saveSearch(client, appId, bungieMembershipId, 2, 'tag:junk', SearchType.Item, true);

const searches = (await getSearchesForProfile(client, bungieMembershipId, 2)).filter(
(s) => s.usageCount > 0,
Expand All @@ -59,7 +60,7 @@ it('can mark a search as favorite', async () => {
expect(searches[0].saved).toBe(true);
expect(searches[0].usageCount).toBe(1);

await saveSearch(client, appId, bungieMembershipId, 2, 'tag:junk', false);
await saveSearch(client, appId, bungieMembershipId, 2, 'tag:junk', SearchType.Item, false);

const searches2 = await getSearchesForProfile(client, bungieMembershipId, 2);
expect(searches2[0].query).toBe('tag:junk');
Expand All @@ -71,7 +72,7 @@ it('can mark a search as favorite', async () => {
});
it('can mark a search as favorite even when it hasnt been used', async () => {
await transaction(async (client) => {
await saveSearch(client, appId, bungieMembershipId, 2, 'tag:junk', true);
await saveSearch(client, appId, bungieMembershipId, 2, 'tag:junk', SearchType.Item, true);

const searches = (await getSearchesForProfile(client, bungieMembershipId, 2)).filter(
(s) => s.usageCount > 0,
Expand All @@ -84,8 +85,8 @@ it('can mark a search as favorite even when it hasnt been used', async () => {

it('can get all searches across profiles', async () => {
await transaction(async (client) => {
await updateUsedSearch(client, appId, bungieMembershipId, 2, 'tag:junk');
await updateUsedSearch(client, appId, bungieMembershipId, 1, 'is:tagged');
await updateUsedSearch(client, appId, bungieMembershipId, 2, 'tag:junk', SearchType.Item);
await updateUsedSearch(client, appId, bungieMembershipId, 1, 'is:tagged', SearchType.Item);

const searches = await getSearchesForUser(client, bungieMembershipId);
expect(searches.length).toEqual(2);
Expand All @@ -97,7 +98,7 @@ it('can increment usage for one of the built-in searches', async () => {
const searches = await getSearchesForProfile(client, bungieMembershipId, 2);
const query = searches[searches.length - 1].query;

await updateUsedSearch(client, appId, bungieMembershipId, 2, query);
await updateUsedSearch(client, appId, bungieMembershipId, 2, query, SearchType.Item);

const searches2 = await getSearchesForProfile(client, bungieMembershipId, 2);
const search = searches2.find((s) => s.query === query);
Expand All @@ -108,8 +109,8 @@ it('can increment usage for one of the built-in searches', async () => {

it('can delete a search', async () => {
await transaction(async (client) => {
await updateUsedSearch(client, appId, bungieMembershipId, 2, 'tag:junk');
await deleteSearch(client, bungieMembershipId, 2, 'tag:junk');
await updateUsedSearch(client, appId, bungieMembershipId, 2, 'tag:junk', SearchType.Item);
await deleteSearch(client, bungieMembershipId, 2, 'tag:junk', SearchType.Item);

const searches = (await getSearchesForProfile(client, bungieMembershipId, 2)).filter(
(s) => s.usageCount > 0,
Expand All @@ -120,7 +121,17 @@ it('can delete a search', async () => {

it('can import a search', async () => {
await transaction(async (client) => {
await importSearch(client, appId, bungieMembershipId, 2, 'tag:junk', true, 1598199188576, 5);
await importSearch(
client,
appId,
bungieMembershipId,
2,
'tag:junk',
true,
1598199188576,
5,
SearchType.Item,
);

const searches = (await getSearchesForProfile(client, bungieMembershipId, 2)).filter(
(s) => s.usageCount > 0,
Expand All @@ -130,3 +141,24 @@ it('can import a search', async () => {
expect(searches[0].usageCount).toBe(5);
});
});

it('can record searches for loadouts', async () => {
await transaction(async (client) => {
await updateUsedSearch(
client,
appId,
bungieMembershipId,
2,
'subclass:void',
SearchType.Loadout,
);

const searches = (await getSearchesForProfile(client, bungieMembershipId, 2)).filter(
(s) => s.usageCount > 0,
);
expect(searches[0].query).toBe('subclass:void');
expect(searches[0].saved).toBe(false);
expect(searches[0].usageCount).toBe(1);
expect(searches[0].type).toBe(SearchType.Loadout);
});
});
32 changes: 20 additions & 12 deletions api/db/searches-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ClientBase, QueryResult } from 'pg';
import { metrics } from '../metrics/index.js';
import { ExportResponse } from '../shapes/export.js';
import { DestinyVersion } from '../shapes/general.js';
import { Search } from '../shapes/search.js';
import { Search, SearchType } from '../shapes/search.js';

/*
* These "canned searches" get sent to everyone as a "starter pack" of example searches that'll show up in the recent search dropdown and autocomplete.
Expand All @@ -17,6 +17,7 @@ const cannedSearchesForD2: Search[] = [
saved: false,
usageCount: 0,
lastUsage: 0,
type: SearchType.Item,
}));

const cannedSearchesForD1: Search[] = ['-is:equipped is:haslight is:incurrentchar'].map(
Expand All @@ -25,6 +26,7 @@ const cannedSearchesForD1: Search[] = ['-is:equipped is:haslight is:incurrentcha
saved: false,
usageCount: 0,
lastUsage: 0,
type: SearchType.Item,
}),
);
/*
Expand Down Expand Up @@ -91,6 +93,7 @@ function convertSearch(row: any): Search {
usageCount: row.usage_count,
saved: row.saved,
lastUsage: row.last_updated_at.getTime(),
type: row.search_type,
};
}

Expand All @@ -105,15 +108,16 @@ export async function updateUsedSearch(
bungieMembershipId: number,
destinyVersion: DestinyVersion,
query: string,
type: SearchType,
): Promise<QueryResult<any>> {
try {
const response = await client.query({
name: 'upsert_search',
text: `insert INTO searches (membership_id, destiny_version, query, created_by, last_updated_by)
values ($1, $2, $3, $4, $4)
text: `insert INTO searches (membership_id, destiny_version, query, search_type, created_by, last_updated_by)
values ($1, $2, $3, $5, $4, $4)
on conflict (membership_id, destiny_version, qhash)
do update set (usage_count, last_used, last_updated_at, last_updated_by) = (searches.usage_count + 1, current_timestamp, current_timestamp, $4)`,
values: [bungieMembershipId, destinyVersion, query, appId],
do update set (usage_count, last_used, last_updated_at, last_updated_by) = (searches.usage_count + 1, current_timestamp, current_timestamp, $5, $4)`,
values: [bungieMembershipId, destinyVersion, query, appId, type],
});

if (response.rowCount < 1) {
Expand All @@ -137,6 +141,7 @@ export async function saveSearch(
bungieMembershipId: number,
destinyVersion: DestinyVersion,
query: string,
type: SearchType,
saved?: boolean,
): Promise<QueryResult<any>> {
try {
Expand All @@ -151,9 +156,9 @@ export async function saveSearch(
metrics.increment('db.searches.noRowUpdated.count', 1);
const insertSavedResponse = await client.query({
name: 'insert_search_fallback',
text: `insert INTO searches (membership_id, destiny_version, query, saved, created_by, last_updated_by)
values ($1, $2, $3, true, $4, $4)`,
values: [bungieMembershipId, destinyVersion, query, appId],
text: `insert INTO searches (membership_id, destiny_version, query, search_type, saved, created_by, last_updated_by)
values ($1, $2, $3, $5, true, $4, $4)`,
values: [bungieMembershipId, destinyVersion, query, appId, type],
});
return insertSavedResponse;
}
Expand All @@ -175,12 +180,13 @@ export async function importSearch(
saved: boolean,
lastUsage: number,
usageCount: number,
type: SearchType,
): Promise<QueryResult<any>> {
try {
const response = await client.query({
name: 'insert_search',
text: `insert INTO searches (membership_id, destiny_version, query, saved, usage_count, last_used, created_by, last_updated_by)
values ($1, $2, $3, $4, $5, $6, $7, $7)`,
text: `insert INTO searches (membership_id, destiny_version, query, saved, search_type, usage_count, last_used, created_by, last_updated_by)
values ($1, $2, $3, $4, $8, $5, $6, $7, $7)`,
values: [
bungieMembershipId,
destinyVersion,
Expand All @@ -189,6 +195,7 @@ values ($1, $2, $3, $4, $5, $6, $7, $7)`,
usageCount,
new Date(lastUsage),
appId,
type,
],
});

Expand All @@ -212,12 +219,13 @@ export async function deleteSearch(
bungieMembershipId: number,
destinyVersion: DestinyVersion,
query: string,
type: SearchType,
): Promise<QueryResult<any>> {
try {
return client.query({
name: 'delete_search',
text: `delete from searches where membership_id = $1 and destiny_version = $2 and qhash = decode(md5($3), 'hex') and query = $3`,
values: [bungieMembershipId, destinyVersion, query],
text: `delete from searches where membership_id = $1 and destiny_version = $2 and qhash = decode(md5($3), 'hex') and query = $3 and search_type = $4`,
values: [bungieMembershipId, destinyVersion, query, type],
});
} catch (e) {
throw new Error(e.name + ': ' + e.message);
Expand Down
2 changes: 2 additions & 0 deletions api/routes/import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { DestinyVersion } from '../shapes/general.js';
import { ImportResponse } from '../shapes/import.js';
import { ItemAnnotation } 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 { badRequest } from '../utils.js';
import { deleteAllData } from './delete-all-data.js';
Expand Down Expand Up @@ -123,6 +124,7 @@ export const importHandler = asyncHandler(async (req, res) => {
search.search.saved,
search.search.lastUsage,
search.search.usageCount,
search.search.type ?? SearchType.Item,
);
}
});
Expand Down
29 changes: 20 additions & 9 deletions api/routes/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ import { DestinyVersion } from '../shapes/general.js';
import { ItemAnnotation } from '../shapes/item-annotations.js';
import { Loadout } from '../shapes/loadouts.js';
import {
DeleteSearchUpdate,
ItemHashTagUpdate,
ProfileUpdateRequest,
ProfileUpdateResult,
SavedSearchUpdate,
TrackTriumphUpdate,
UsedSearchUpdate,
} from '../shapes/profile.js';
import { SearchType } from '../shapes/search.js';
import { Settings } from '../shapes/settings.js';
import {
badRequest,
Expand Down Expand Up @@ -146,12 +148,7 @@ export const updateHandler = asyncHandler(async (req, res) => {
break;

case 'delete_search':
result = await deleteSearch(
client,
bungieMembershipId,
destinyVersion,
update.payload.query,
);
result = await deleteSearch(client, bungieMembershipId, destinyVersion, update.payload);
break;

default:
Expand Down Expand Up @@ -428,7 +425,14 @@ async function recordSearch(
}

const start = new Date();
await updateUsedSearch(client, appId, bungieMembershipId, destinyVersion, payload.query);
await updateUsedSearch(
client,
appId,
bungieMembershipId,
destinyVersion,
payload.query,
payload.type ?? SearchType.Item,
);
metrics.timing('update.recordSearch', start);

return { status: 'Success' };
Expand Down Expand Up @@ -461,6 +465,7 @@ async function saveSearch(
bungieMembershipId,
destinyVersion,
payload.query,
payload.type ?? SearchType.Item,
payload.saved,
);
metrics.timing('update.saveSearch', start);
Expand All @@ -472,10 +477,16 @@ async function deleteSearch(
client: ClientBase,
bungieMembershipId: number,
destinyVersion: DestinyVersion,
query: string,
payload: DeleteSearchUpdate['payload'],
): Promise<ProfileUpdateResult> {
const start = new Date();
await deleteSearchInDb(client, bungieMembershipId, destinyVersion, query);
await deleteSearchInDb(
client,
bungieMembershipId,
destinyVersion,
payload.query,
payload.type ?? SearchType.Item,
);
metrics.timing('update.deleteSearch', start);

return { status: 'Success' };
Expand Down
5 changes: 4 additions & 1 deletion api/shapes/profile.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DestinyVersion } from './general.js';
import { ItemAnnotation, ItemHashTag } from './item-annotations.js';
import { Loadout } from './loadouts.js';
import { Search } from './search.js';
import { Search, SearchType } from './search.js';
import { Settings } from './settings.js';

export interface ProfileResponse {
Expand Down Expand Up @@ -87,6 +87,7 @@ export interface UsedSearchUpdate {
action: 'search';
payload: {
query: string;
type: SearchType;
};
}

Expand All @@ -97,6 +98,7 @@ export interface SavedSearchUpdate {
action: 'save_search';
payload: {
query: string;
type: SearchType;
/**
* Whether the search should be saved
*/
Expand All @@ -111,6 +113,7 @@ export interface DeleteSearchUpdate {
action: 'delete_search';
payload: {
query: string;
type: SearchType;
};
}

Expand Down
10 changes: 10 additions & 0 deletions api/shapes/search.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export const enum SearchType {
Item = 1,
Loadout = 2,
}

/**
* A search query. This can either be from history (recent searches), pinned (saved searches), or suggested.
*/
Expand All @@ -15,4 +20,9 @@ export interface Search {
* The last time this was used, as a unix millisecond timestamp.
*/
lastUsage: number;
/**
* Which kind of thing is this search for? Searches of different types are
* stored together and need to be filtered to the specific type.
*/
type: SearchType;
}

0 comments on commit eceead1

Please sign in to comment.