Skip to content

Commit

Permalink
Merge pull request #215 from DestinyItemManager/search-type
Browse files Browse the repository at this point in the history
Track the type of saved searches
  • Loading branch information
bhollis authored May 22, 2024
2 parents caa3518 + 48fa464 commit e81207a
Show file tree
Hide file tree
Showing 18 changed files with 161 additions and 59 deletions.
4 changes: 2 additions & 2 deletions api/db/apps-queries.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { v4 as uuid } from 'uuid';
import { ApiApp } from '../shapes/app.js';
import { getAllApps, getAppById, insertApp } from './apps-queries.js';
import { pool, transaction } from './index.js';
import { closeDbPool, pool, transaction } from './index.js';

const appId = 'apps-queries-test-app';
const app: ApiApp = {
Expand All @@ -13,7 +13,7 @@ const app: ApiApp = {

beforeEach(() => pool.query({ text: 'delete from apps where id = $1', values: [appId] }));

afterAll(() => pool.end());
afterAll(() => closeDbPool());

it('can create a new app', async () => {
await transaction(async (client) => {
Expand Down
4 changes: 2 additions & 2 deletions api/db/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { pool, readTransaction, transaction } from './index.js';
import { closeDbPool, pool, readTransaction, transaction } from './index.js';

beforeEach(async () => {
try {
Expand All @@ -14,7 +14,7 @@ afterAll(async () => {
try {
await pool.query(`DROP TABLE transaction_test`);
} catch {}
await pool.end();
await closeDbPool();
});

describe('transaction', () => {
Expand Down
7 changes: 6 additions & 1 deletion api/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,17 @@ pool.on('remove', () => {
metrics.increment('db.pool.remove.count');
});

setInterval(() => {
const metricsInterval = setInterval(() => {
metrics.gauge('db.pool.total', pool.totalCount);
metrics.gauge('db.pool.idle', pool.idleCount);
metrics.gauge('db.pool.waiting', pool.waitingCount);
}, 10000);

export async function closeDbPool() {
clearInterval(metricsInterval);
return pool.end();
}

/**
* A helper that gets a connection from the pool and then executes fn within a transaction.
*/
Expand Down
4 changes: 2 additions & 2 deletions api/db/item-annotations-queries.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { TagVariant } from '../shapes/item-annotations.js';
import { pool, transaction } from './index.js';
import { closeDbPool, transaction } from './index.js';
import {
deleteAllItemAnnotations,
deleteItemAnnotation,
Expand All @@ -18,7 +18,7 @@ beforeEach(() =>
}),
);

afterAll(() => pool.end());
afterAll(() => closeDbPool());

it('can insert tags where none exist before', async () => {
await transaction(async (client) => {
Expand Down
4 changes: 2 additions & 2 deletions api/db/item-hash-tags-queries.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { pool, transaction } from './index.js';
import { closeDbPool, transaction } from './index.js';
import {
deleteAllItemHashTags,
deleteItemHashTag,
Expand All @@ -15,7 +15,7 @@ beforeEach(() =>
}),
);

afterAll(() => pool.end());
afterAll(() => closeDbPool());

it('can insert item hash tags where none exist before', async () => {
await transaction(async (client) => {
Expand Down
4 changes: 2 additions & 2 deletions api/db/loadout-share-queries.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { v4 as uuid } from 'uuid';
import { Loadout, LoadoutItem } from '../shapes/loadouts.js';
import { pool, transaction } from './index.js';
import { closeDbPool, transaction } from './index.js';
import { addLoadoutShare, getLoadoutShare, recordAccess } from './loadout-share-queries.js';

const appId = 'settings-queries-test-app';
Expand All @@ -15,7 +15,7 @@ beforeEach(() =>
}),
);

afterAll(() => pool.end());
afterAll(() => closeDbPool());

const loadout: Loadout = {
id: uuid(),
Expand Down
4 changes: 2 additions & 2 deletions api/db/loadouts-queries.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { v4 as uuid } from 'uuid';
import { Loadout, LoadoutItem } from '../shapes/loadouts.js';
import { pool, transaction } from './index.js';
import { closeDbPool, transaction } from './index.js';
import { deleteLoadout, getLoadoutsForProfile, updateLoadout } from './loadouts-queries.js';

const appId = 'settings-queries-test-app';
Expand All @@ -13,7 +13,7 @@ beforeEach(() =>
}),
);

afterAll(() => pool.end());
afterAll(() => closeDbPool());

const loadout: Loadout = {
id: uuid(),
Expand Down
62 changes: 47 additions & 15 deletions api/db/searches-queries.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { pool, transaction } from './index.js';
import { SearchType } from '../shapes/search.js';
import { closeDbPool, transaction } from './index.js';
import {
deleteAllSearches,
deleteSearch,
Expand All @@ -18,11 +19,11 @@ beforeEach(() =>
}),
);

afterAll(() => pool.end());
afterAll(() => closeDbPool());

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);
});
});
34 changes: 21 additions & 13 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 All @@ -48,7 +50,7 @@ export async function getSearchesForProfile(
const results = await client.query({
name: 'get_searches',
// TODO: order by frecency
text: 'SELECT query, saved, usage_count, last_updated_at FROM searches WHERE membership_id = $1 and destiny_version = $2 order by last_updated_at DESC, usage_count DESC LIMIT 500',
text: 'SELECT query, saved, usage_count, search_type, last_updated_at FROM searches WHERE membership_id = $1 and destiny_version = $2 order by last_updated_at DESC, usage_count DESC LIMIT 500',
values: [bungieMembershipId, destinyVersion],
});
return _.uniqBy(
Expand All @@ -73,7 +75,7 @@ export async function getSearchesForUser(
try {
const results = await client.query({
name: 'get_all_searches',
text: 'SELECT destiny_version, query, saved, usage_count, last_updated_at FROM searches WHERE membership_id = $1',
text: 'SELECT destiny_version, query, saved, usage_count, search_type, last_updated_at FROM searches WHERE membership_id = $1',
values: [bungieMembershipId],
});
return results.rows.map((row) => ({
Expand All @@ -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],
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
4 changes: 2 additions & 2 deletions api/db/settings-queries.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { pool, transaction } from './index.js';
import { closeDbPool, transaction } from './index.js';
import { getSettings, setSetting } from './settings-queries.js';

const appId = 'settings-queries-test-app';
const bungieMembershipId = 4321;

afterAll(() => pool.end());
afterAll(() => closeDbPool());

it('can insert settings where none exist before', async () => {
await transaction(async (client) => {
Expand Down
Loading

0 comments on commit e81207a

Please sign in to comment.