From 94b4c991ade2048ed6fb7e241fb0b9f0c85c2b81 Mon Sep 17 00:00:00 2001 From: Ben Hollis Date: Sat, 31 Aug 2024 13:43:32 -0700 Subject: [PATCH] Upgrade packages, get types/eslint working --- .eslintignore | 2 - .eslintrc | 55 ---- api/db/apps-queries.test.ts | 6 +- api/db/apps-queries.ts | 54 ++- api/db/index.test.ts | 23 +- api/db/index.ts | 6 +- api/db/item-annotations-queries.ts | 151 ++++----- api/db/item-hash-tags-queries.ts | 86 +++-- api/db/loadout-share-queries.test.ts | 2 +- api/db/loadout-share-queries.ts | 110 +++---- api/db/loadouts-queries.test.ts | 7 +- api/db/loadouts-queries.ts | 163 +++++---- api/db/searches-queries.ts | 202 +++++------- api/db/settings-queries.ts | 66 ++-- api/db/triumphs-queries.ts | 14 +- api/dim-gg/loadout-share-view.ts | 4 +- api/index.ts | 8 +- api/metrics/express.ts | 17 +- api/metrics/index.ts | 2 +- api/routes/auth-token.ts | 12 +- api/routes/create-app.ts | 3 +- api/routes/delete-all-data.ts | 3 +- api/routes/donate.ts | 6 +- api/routes/export.ts | 3 +- api/routes/import.ts | 14 +- api/routes/loadout-share.ts | 14 +- api/routes/profile.ts | 45 +-- api/routes/update.ts | 34 +- api/server.test.ts | 12 +- api/server.ts | 50 +-- api/shapes/loadouts.ts | 12 +- api/tsconfig.eslint.json | 8 + api/utils.ts | 104 +++++- build-dim-api-types.sh | 1 + eslint.config.js | 229 +++++++++++++ package.json | 14 +- pnpm-lock.yaml | 473 +++++++++++++++++++++------ tsconfig.dim-api-types.json | 11 +- tsconfig.json | 8 +- 39 files changed, 1266 insertions(+), 768 deletions(-) delete mode 100644 .eslintignore delete mode 100644 .eslintrc create mode 100644 api/tsconfig.eslint.json create mode 100644 eslint.config.js diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index f6b9bb4..0000000 --- a/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -*.test.ts -migrations/ \ No newline at end of file diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index d0d17be..0000000 --- a/.eslintrc +++ /dev/null @@ -1,55 +0,0 @@ -{ - "parser": "@typescript-eslint/parser", - "env": { - "node": true, - "es6": true - }, - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "prettier" - ], - "globals": {}, - "parserOptions": { - "ecmaVersion": 2018, - "sourceType": "module", - "project": "./tsconfig.json" - }, - "settings": { - "react": { - "version": "detect" - } - }, - "plugins": ["@typescript-eslint"], - "rules": { - "no-console": "off", - "no-empty": "off", - "require-atomic-updates": "off", - "curly": ["error", "all"], - "@typescript-eslint/explicit-member-accessibility": "off", - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-var-requires": "off", - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/no-use-before-define": [ - "error", - { "functions": false } - ], - "@typescript-eslint/no-parameter-properties": "off", - "@typescript-eslint/no-extraneous-class": "error", - "@typescript-eslint/no-this-alias": "error", - "@typescript-eslint/no-unnecessary-qualifier": "error", - "@typescript-eslint/no-unnecessary-type-assertion": "error", - "@typescript-eslint/prefer-function-type": "error", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/ban-types": "off", - "@typescript-eslint/no-unused-vars": [ - "error", - { - "varsIgnorePattern": "(^_|[iI]gnored)", - "argsIgnorePattern": "(^_|[iI]gnored)", - "ignoreRestSiblings": true - } - ] - } -} diff --git a/api/db/apps-queries.test.ts b/api/db/apps-queries.test.ts index 9341904..28d8f83 100644 --- a/api/db/apps-queries.test.ts +++ b/api/db/apps-queries.test.ts @@ -1,3 +1,4 @@ +import { DatabaseError } from 'pg'; import { v4 as uuid } from 'uuid'; import { ApiApp } from '../shapes/app.js'; import { getAllApps, getAppById, insertApp } from './apps-queries.js'; @@ -32,7 +33,10 @@ it('cannot create a new app with the same name as an existing one', async () => try { await insertApp(client, app); } catch (e) { - expect(e.code).toBe('23505'); + if (!(e instanceof DatabaseError)) { + fail('should have thrown a DatabaseError'); + } + expect((e).code).toBe('23505'); } }); }); diff --git a/api/db/apps-queries.ts b/api/db/apps-queries.ts index 54e18a1..5fecc7f 100644 --- a/api/db/apps-queries.ts +++ b/api/db/apps-queries.ts @@ -1,54 +1,42 @@ import { ClientBase, QueryResult } from 'pg'; import { ApiApp } from '../shapes/app.js'; -import { camelize } from '../utils.js'; +import { camelize, KeysToSnakeCase, TypesForKeys } from '../utils.js'; /** * Get all registered apps. */ export async function getAllApps(client: ClientBase): Promise { - try { - const results = await client.query({ - name: 'get_all_apps', - text: 'SELECT * FROM apps', - }); - return results.rows.map((row) => camelize(row)); - } catch (e) { - throw new Error(e.name + ': ' + e.message); - } + const results = await client.query>({ + name: 'get_all_apps', + text: 'SELECT * FROM apps', + }); + return results.rows.map((row) => camelize(row)); } /** * Get an app by its ID. */ export async function getAppById(client: ClientBase, id: string): Promise { - try { - const results = await client.query({ - name: 'get_apps', - text: 'SELECT * FROM apps where id = $1', - values: [id], - }); - if (results.rows.length > 0) { - return camelize(results.rows[0]); - } else { - return null; - } - } catch (e) { - throw new Error(e.name + ': ' + e.message); + const results = await client.query>({ + name: 'get_apps', + text: 'SELECT * FROM apps where id = $1', + values: [id], + }); + if (results.rows.length > 0) { + return camelize(results.rows[0]); + } else { + return null; } } /** * Insert a new app into the list of registered apps. */ -export async function insertApp(client: ClientBase, app: ApiApp): Promise> { - try { - return client.query({ - name: 'insert_app', - text: `insert into apps (id, bungie_api_key, dim_api_key, origin) +export async function insertApp(client: ClientBase, app: ApiApp): Promise { + return client.query>({ + name: 'insert_app', + text: `insert into apps (id, bungie_api_key, dim_api_key, origin) values ($1, $2, $3, $4)`, - values: [app.id, app.bungieApiKey, app.dimApiKey, app.origin], - }); - } catch (e) { - throw new Error(e.name + ': ' + e.message); - } + values: [app.id, app.bungieApiKey, app.dimApiKey, app.origin], + }); } diff --git a/api/db/index.test.ts b/api/db/index.test.ts index 6c098fb..1a78f5f 100644 --- a/api/db/index.test.ts +++ b/api/db/index.test.ts @@ -10,6 +10,11 @@ beforeEach(async () => { )`); }); +interface TransactionTestRow { + id: number; + test: string; +} + afterAll(async () => { try { await pool.query(`DROP TABLE transaction_test`); @@ -28,10 +33,10 @@ describe('transaction', () => { }); fail('should have thrown an error'); } catch (e) { - expect(e.message).toBe('oops'); + expect((e as Error).message).toBe('oops'); } - const result = await pool.query('select * from transaction_test'); + const result = await pool.query('select * from transaction_test'); expect(result.rows.length).toBe(1); expect(result.rows[0].id).toBe(1); }); @@ -41,7 +46,7 @@ describe('transaction', () => { await client.query("insert into transaction_test (id, test) values (3, 'testing commits')"); }); - const result = await pool.query('select * from transaction_test'); + const result = await pool.query('select * from transaction_test'); expect(result.rows.length).toBe(1); expect(result.rows[0].test).toBe('testing commits'); }); @@ -61,7 +66,9 @@ describe('readTransaction', () => { // Now request that info from our original client. // should be read-committed, so we shouldn't see that update - const result = await client.query('select * from transaction_test where id = 1'); + const result = await client.query( + 'select * from transaction_test where id = 1', + ); expect(result.rows[0].test).toBe('testing'); // Commit the update @@ -74,12 +81,16 @@ describe('readTransaction', () => { } // once that other transaction commits, we'll see its update - const result = await client.query('select * from transaction_test where id = 1'); + const result = await client.query( + 'select * from transaction_test where id = 1', + ); expect(result.rows[0].test).toBe('updated'); }); // outside, we should still see the transactional update - const result = await pool.query('select * from transaction_test where id = 1'); + const result = await pool.query( + 'select * from transaction_test where id = 1', + ); expect(result.rows[0].test).toBe('updated'); }); }); diff --git a/api/db/index.ts b/api/db/index.ts index f0628cb..b54a37c 100644 --- a/api/db/index.ts +++ b/api/db/index.ts @@ -21,7 +21,7 @@ pool.on('acquire', () => { }); pool.on('error', (e: Error) => { metrics.increment('db.pool.error.count'); - metrics.increment('db.pool.error.' + e.name + '.count'); + metrics.increment(`db.pool.error.${ e.name }.count`); }); pool.on('remove', () => { metrics.increment('db.pool.remove.count'); @@ -66,10 +66,10 @@ export async function readTransaction(fn: (client: ClientBase) => Promise) const client = await pool.connect(); try { // We used to wrap multiple reads in a transaction but I'm not sure it matters all that much. - //await client.query('BEGIN'); + // await client.query('BEGIN'); return await fn(client); } finally { - //await client.query('ROLLBACK'); + // await client.query('ROLLBACK'); client.release(); } } diff --git a/api/db/item-annotations-queries.ts b/api/db/item-annotations-queries.ts index 1f4c61d..0c0f912 100644 --- a/api/db/item-annotations-queries.ts +++ b/api/db/item-annotations-queries.ts @@ -3,6 +3,14 @@ import { metrics } from '../metrics/index.js'; import { DestinyVersion } from '../shapes/general.js'; import { ItemAnnotation, TagValue, TagVariant } from '../shapes/item-annotations.js'; +interface ItemAnnotationRow { + inventory_item_id: string; + tag: TagValue | null; + notes: string | null; + variant: TagVariant | null; + crafted_date: Date | null; +} + /** * Get all of the item annotations for a particular platform_membership_id and destiny_version. */ @@ -12,16 +20,12 @@ export async function getItemAnnotationsForProfile( platformMembershipId: string, destinyVersion: DestinyVersion, ): Promise { - try { - const results = await client.query({ - name: 'get_item_annotations', - text: 'SELECT inventory_item_id, tag, notes, variant, crafted_date FROM item_annotations WHERE membership_id = $1 and platform_membership_id = $2 and destiny_version = $3', - values: [bungieMembershipId, platformMembershipId, destinyVersion], - }); - return results.rows.map(convertItemAnnotation); - } catch (e) { - throw new Error(e.name + ': ' + e.message); - } + const results = await client.query({ + name: 'get_item_annotations', + text: 'SELECT inventory_item_id, tag, notes, variant, crafted_date FROM item_annotations WHERE membership_id = $1 and platform_membership_id = $2 and destiny_version = $3', + values: [bungieMembershipId, platformMembershipId, destinyVersion], + }); + return results.rows.map(convertItemAnnotation); } /** @@ -37,24 +41,22 @@ export async function getAllItemAnnotationsForUser( annotation: ItemAnnotation; }[] > { - try { - // TODO: this isn't indexed! - const results = await client.query({ - 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', - values: [bungieMembershipId], - }); - return results.rows.map((row) => ({ - platformMembershipId: row.platform_membership_id, - destinyVersion: row.destiny_version, - annotation: convertItemAnnotation(row), - })); - } catch (e) { - throw new Error(e.name + ': ' + e.message); - } + // TODO: this isn't indexed! + const results = await client.query< + 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', + values: [bungieMembershipId], + }); + return results.rows.map((row) => ({ + platformMembershipId: row.platform_membership_id, + destinyVersion: row.destiny_version, + annotation: convertItemAnnotation(row), + })); } -function convertItemAnnotation(row: any): ItemAnnotation { +function convertItemAnnotation(row: ItemAnnotationRow): ItemAnnotation { const result: ItemAnnotation = { id: row.inventory_item_id, }; @@ -83,7 +85,7 @@ export async function updateItemAnnotation( platformMembershipId: string, destinyVersion: DestinyVersion, itemAnnotation: ItemAnnotation, -): Promise> { +): Promise { const tagValue = clearValue(itemAnnotation.tag); // Variant will only be set when tag is set and only for "keep" values const variant = variantValue(tagValue, itemAnnotation.v); @@ -92,37 +94,32 @@ export async function updateItemAnnotation( if (tagValue === 'clear' && notesValue === 'clear') { return deleteItemAnnotation(client, bungieMembershipId, itemAnnotation.id); } - - try { - const response = await client.query({ - name: 'upsert_item_annotation', - text: `insert INTO item_annotations (membership_id, platform_membership_id, destiny_version, inventory_item_id, tag, notes, variant, crafted_date, created_by, last_updated_by) + const response = await client.query({ + name: 'upsert_item_annotation', + text: `insert INTO item_annotations (membership_id, platform_membership_id, destiny_version, inventory_item_id, tag, notes, variant, crafted_date, created_by, last_updated_by) values ($1, $2, $3, $4, (CASE WHEN $5 = 'clear'::item_tag THEN NULL ELSE $5 END)::item_tag, (CASE WHEN $6 = 'clear' THEN NULL ELSE $6 END), $9, $8, $7, $7) on conflict (membership_id, inventory_item_id) do update set (tag, notes, variant, last_updated_at, last_updated_by) = ((CASE WHEN $5 = 'clear' THEN NULL WHEN $5 IS NULL THEN item_annotations.tag ELSE $5 END), (CASE WHEN $6 = 'clear' THEN NULL WHEN $6 IS NULL THEN item_annotations.notes ELSE $6 END), (CASE WHEN $9 = 0 THEN NULL WHEN $9 IS NULL THEN item_annotations.variant ELSE $9 END), current_timestamp, $7)`, - values: [ - bungieMembershipId, - platformMembershipId, - destinyVersion, - itemAnnotation.id, - tagValue, - notesValue, - appId, - itemAnnotation.craftedDate ? new Date(itemAnnotation.craftedDate * 1000) : null, - variant, - ], - }); + values: [ + bungieMembershipId, + platformMembershipId, + destinyVersion, + itemAnnotation.id, + tagValue, + notesValue, + appId, + itemAnnotation.craftedDate ? new Date(itemAnnotation.craftedDate * 1000) : null, + variant, + ], + }); - if (response.rowCount! < 1) { - // This should never happen! - metrics.increment('db.itemAnnotations.noRowUpdated.count', 1); - throw new Error('tags - No row was updated'); - } - - return response; - } catch (e) { - throw new Error(e.name + ': ' + e.message); + if (response.rowCount! < 1) { + // This should never happen! + metrics.increment('db.itemAnnotations.noRowUpdated.count', 1); + throw new Error('tags - No row was updated'); } + + return response; } /** @@ -167,16 +164,12 @@ export async function deleteItemAnnotation( client: ClientBase, bungieMembershipId: number, inventoryItemId: string, -): Promise> { - try { - return client.query({ - name: 'delete_item_annotation', - text: `delete from item_annotations where membership_id = $1 and inventory_item_id = $2`, - values: [bungieMembershipId, inventoryItemId], - }); - } catch (e) { - throw new Error(e.name + ': ' + e.message); - } +): Promise { + return client.query({ + name: 'delete_item_annotation', + text: `delete from item_annotations where membership_id = $1 and inventory_item_id = $2`, + values: [bungieMembershipId, inventoryItemId], + }); } /** @@ -186,16 +179,12 @@ export async function deleteItemAnnotationList( client: ClientBase, bungieMembershipId: number, inventoryItemIds: string[], -): Promise> { - try { - return client.query({ - name: 'delete_item_annotation_list', - text: `delete from item_annotations where membership_id = $1 and inventory_item_id::bigint = ANY($2::bigint[])`, - values: [bungieMembershipId, inventoryItemIds], - }); - } catch (e) { - throw new Error(e.name + ': ' + e.message); - } +): Promise { + return client.query({ + name: 'delete_item_annotation_list', + text: `delete from item_annotations where membership_id = $1 and inventory_item_id::bigint = ANY($2::bigint[])`, + values: [bungieMembershipId, inventoryItemIds], + }); } /** @@ -204,14 +193,10 @@ export async function deleteItemAnnotationList( export async function deleteAllItemAnnotations( client: ClientBase, bungieMembershipId: number, -): Promise> { - try { - return client.query({ - name: 'delete_all_item_annotations', - text: `delete from item_annotations where membership_id = $1`, - values: [bungieMembershipId], - }); - } catch (e) { - throw new Error(e.name + ': ' + e.message); - } +): Promise { + return client.query({ + name: 'delete_all_item_annotations', + text: `delete from item_annotations where membership_id = $1`, + values: [bungieMembershipId], + }); } diff --git a/api/db/item-hash-tags-queries.ts b/api/db/item-hash-tags-queries.ts index cfa26f7..e04a601 100644 --- a/api/db/item-hash-tags-queries.ts +++ b/api/db/item-hash-tags-queries.ts @@ -1,6 +1,12 @@ import { ClientBase, QueryResult } from 'pg'; import { metrics } from '../metrics/index.js'; -import { ItemHashTag } from '../shapes/item-annotations.js'; +import { ItemHashTag, TagValue } from '../shapes/item-annotations.js'; + +interface ItemHashTagRow { + item_hash: string; + tag: TagValue | null; + notes: string | null; +} /** * Get all of the hash tags for a particular platform_membership_id and destiny_version. @@ -9,19 +15,15 @@ export async function getItemHashTagsForProfile( client: ClientBase, bungieMembershipId: number, ): Promise { - try { - const results = await client.query({ - name: 'get_item_hash_tags', - text: 'SELECT item_hash, tag, notes FROM item_hash_tags WHERE membership_id = $1', - values: [bungieMembershipId], - }); - return results.rows.map(convertItemHashTag); - } catch (e) { - throw new Error(e.name + ': ' + e.message); - } + const results = await client.query({ + name: 'get_item_hash_tags', + text: 'SELECT item_hash, tag, notes FROM item_hash_tags WHERE membership_id = $1', + values: [bungieMembershipId], + }); + return results.rows.map(convertItemHashTag); } -function convertItemHashTag(row: any): ItemHashTag { +function convertItemHashTag(row: ItemHashTagRow): ItemHashTag { const result: ItemHashTag = { hash: parseInt(row.item_hash, 10), }; @@ -42,7 +44,7 @@ export async function updateItemHashTag( appId: string, bungieMembershipId: number, itemHashTag: ItemHashTag, -): Promise> { +): Promise { const tagValue = clearValue(itemHashTag.tag); const notesValue = clearValue(itemHashTag.notes); @@ -50,26 +52,22 @@ export async function updateItemHashTag( return deleteItemHashTag(client, bungieMembershipId, itemHashTag.hash); } - try { - const response = await client.query({ - name: 'upsert_hash_tag', - text: `insert INTO item_hash_tags (membership_id, item_hash, tag, notes, created_by, last_updated_by) + const response = await client.query({ + name: 'upsert_hash_tag', + text: `insert INTO item_hash_tags (membership_id, item_hash, tag, notes, created_by, last_updated_by) values ($1, $2, (CASE WHEN $3 = 'clear'::item_tag THEN NULL ELSE $3 END)::item_tag, (CASE WHEN $4 = 'clear' THEN NULL ELSE $4 END), $5, $5) on conflict (membership_id, item_hash) do update set (tag, notes, last_updated_at, last_updated_by) = ((CASE WHEN $3 = 'clear' THEN NULL WHEN $3 IS NULL THEN item_hash_tags.tag ELSE $3 END), (CASE WHEN $4 = 'clear' THEN NULL WHEN $4 IS NULL THEN item_hash_tags.notes ELSE $4 END), current_timestamp, $5)`, - values: [bungieMembershipId, itemHashTag.hash, tagValue, notesValue, appId], - }); - - if (response.rowCount! < 1) { - // This should never happen! - metrics.increment('db.itemHashTags.noRowUpdated.count', 1); - throw new Error('hash tags - No row was updated'); - } + values: [bungieMembershipId, itemHashTag.hash, tagValue, notesValue, appId], + }); - return response; - } catch (e) { - throw new Error(e.name + ': ' + e.message); + if (response.rowCount! < 1) { + // This should never happen! + metrics.increment('db.itemHashTags.noRowUpdated.count', 1); + throw new Error('hash tags - No row was updated'); } + + return response; } /** @@ -94,16 +92,12 @@ export async function deleteItemHashTag( client: ClientBase, bungieMembershipId: number, itemHash: number, -): Promise> { - try { - return client.query({ - name: 'delete_item_hash_tag', - text: `delete from item_hash_tags where membership_id = $1 and item_hash = $2`, - values: [bungieMembershipId, itemHash], - }); - } catch (e) { - throw new Error(e.name + ': ' + e.message); - } +): Promise { + return client.query({ + name: 'delete_item_hash_tag', + text: `delete from item_hash_tags where membership_id = $1 and item_hash = $2`, + values: [bungieMembershipId, itemHash], + }); } /** @@ -112,14 +106,10 @@ export async function deleteItemHashTag( export async function deleteAllItemHashTags( client: ClientBase, bungieMembershipId: number, -): Promise> { - try { - return client.query({ - name: 'delete_all_item_hash_tags', - text: `delete from item_hash_tags where membership_id = $1`, - values: [bungieMembershipId], - }); - } catch (e) { - throw new Error(e.name + ': ' + e.message); - } +): Promise { + return client.query({ + name: 'delete_all_item_hash_tags', + text: `delete from item_hash_tags where membership_id = $1`, + values: [bungieMembershipId], + }); } diff --git a/api/db/loadout-share-queries.test.ts b/api/db/loadout-share-queries.test.ts index 1454a1e..1ef38b9 100644 --- a/api/db/loadout-share-queries.test.ts +++ b/api/db/loadout-share-queries.test.ts @@ -78,7 +78,7 @@ it('rejects multiple shares with the same ID', async () => { loadout, ); fail('Expected this to throw an error'); - } catch (e) {} + } catch {} }); }); diff --git a/api/db/loadout-share-queries.ts b/api/db/loadout-share-queries.ts index 7f71acc..cc73425 100644 --- a/api/db/loadout-share-queries.ts +++ b/api/db/loadout-share-queries.ts @@ -1,7 +1,7 @@ import { ClientBase, QueryResult } from 'pg'; import { metrics } from '../metrics/index.js'; import { Loadout } from '../shapes/loadouts.js'; -import { cleanItem, convertLoadout } from './loadouts-queries.js'; +import { cleanItem, convertLoadout, LoadoutRow } from './loadouts-queries.js'; /** * Get a specific loadout share by its share ID. @@ -10,19 +10,15 @@ export async function getLoadoutShare( client: ClientBase, shareId: string, ): Promise { - try { - 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; - } - } catch (e) { - throw new Error(e.name + ': ' + e.message); + 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; } } @@ -36,61 +32,53 @@ export async function addLoadoutShare( platformMembershipId: string, shareId: string, loadout: Loadout, -): Promise> { - try { - 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) +): 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, - ], - }); + 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; - } catch (e) { - throw new Error(e.name + ': ' + e.message); + 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> { - try { - 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], - }); +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; - } catch (e) { - throw new Error(e.name + ': ' + e.message); + 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/db/loadouts-queries.test.ts b/api/db/loadouts-queries.test.ts index 7785585..6e5608c 100644 --- a/api/db/loadouts-queries.test.ts +++ b/api/db/loadouts-queries.test.ts @@ -62,8 +62,8 @@ it('can record a loadout', async () => { expect(firstLoadout.lastUpdatedAt).toBeDefined(); delete firstLoadout.lastUpdatedAt; expect(firstLoadout.unequipped.length).toBe(1); - expect(firstLoadout.unequipped[0]['fizbuzz']).toBeUndefined(); - firstLoadout.unequipped[0]['fizbuzz'] = 11; + expect((firstLoadout.unequipped[0] as { fizbuzz?: number }).fizbuzz).toBeUndefined(); + (firstLoadout.unequipped[0] as { fizbuzz?: number }).fizbuzz = 11; expect(firstLoadout).toEqual(loadout); }); }); @@ -96,7 +96,8 @@ it('can delete a loadout', async () => { await transaction(async (client) => { await updateLoadout(client, appId, bungieMembershipId, platformMembershipId, 2, loadout); - await deleteLoadout(client, bungieMembershipId, loadout.id); + const success = await deleteLoadout(client, bungieMembershipId, loadout.id); + expect(success).toBe(true); const loadouts = await getLoadoutsForProfile( client, diff --git a/api/db/loadouts-queries.ts b/api/db/loadouts-queries.ts index cd71fdf..98c68a9 100644 --- a/api/db/loadouts-queries.ts +++ b/api/db/loadouts-queries.ts @@ -2,7 +2,16 @@ import { ClientBase, QueryResult } from 'pg'; import { metrics } from '../metrics/index.js'; import { DestinyVersion } from '../shapes/general.js'; import { Loadout, LoadoutItem } from '../shapes/loadouts.js'; -import { isValidItemId } from '../utils.js'; +import { isValidItemId, KeysToSnakeCase } from '../utils.js'; + +export interface LoadoutRow + extends KeysToSnakeCase< + Omit + > { + created_at: Date; + last_updated_at: Date | null; + items: { equipped: LoadoutItem[]; unequipped: LoadoutItem[] }; +} /** * Get all of the loadouts for a particular platform_membership_id and destiny_version. @@ -13,16 +22,12 @@ export async function getLoadoutsForProfile( platformMembershipId: string, destinyVersion: DestinyVersion, ): Promise { - try { - const results = await client.query({ - name: 'get_loadouts_for_platform_membership_id', - text: 'SELECT id, name, notes, class_type, emblem_hash, clear_space, items, parameters, created_at, last_updated_at FROM loadouts WHERE membership_id = $1 and platform_membership_id = $2 and destiny_version = $3', - values: [bungieMembershipId, platformMembershipId, destinyVersion], - }); - return results.rows.map(convertLoadout); - } catch (e) { - throw new Error(e.name + ': ' + e.message); - } + const results = await client.query({ + name: 'get_loadouts_for_platform_membership_id', + text: 'SELECT id, name, notes, class_type, emblem_hash, clear_space, items, parameters, created_at, last_updated_at FROM loadouts WHERE membership_id = $1 and platform_membership_id = $2 and destiny_version = $3', + values: [bungieMembershipId, platformMembershipId, destinyVersion], + }); + return results.rows.map(convertLoadout); } /** @@ -38,26 +43,24 @@ export async function getAllLoadoutsForUser( loadout: Loadout; }[] > { - try { - const results = await client.query({ - name: 'get_all_loadouts_for_user', - text: 'SELECT membership_id, platform_membership_id, destiny_version, id, name, notes, class_type, emblem_hash, clear_space, items, parameters, created_at, last_updated_at FROM loadouts WHERE membership_id = $1', - values: [bungieMembershipId], - }); - return results.rows.map((row) => { - const loadout = convertLoadout(row); - return { - platformMembershipId: row.platform_membership_id, - destinyVersion: row.destiny_version, - loadout, - }; - }); - } catch (e) { - throw new Error(e.name + ': ' + e.message); - } + const results = await client.query< + LoadoutRow & { platform_membership_id: string; destiny_version: DestinyVersion } + >({ + name: 'get_all_loadouts_for_user', + text: 'SELECT membership_id, platform_membership_id, destiny_version, id, name, notes, class_type, emblem_hash, clear_space, items, parameters, created_at, last_updated_at FROM loadouts WHERE membership_id = $1', + values: [bungieMembershipId], + }); + return results.rows.map((row) => { + const loadout = convertLoadout(row); + return { + platformMembershipId: row.platform_membership_id, + destinyVersion: row.destiny_version, + loadout, + }; + }); } -export function convertLoadout(row: any): Loadout { +export function convertLoadout(row: LoadoutRow): Loadout { const loadout: Loadout = { id: row.id, name: row.name, @@ -90,43 +93,39 @@ export async function updateLoadout( platformMembershipId: string, destinyVersion: DestinyVersion, loadout: Loadout, -): Promise> { - try { - const response = await client.query({ - name: 'upsert_loadout', - text: `insert into loadouts (id, membership_id, platform_membership_id, destiny_version, name, notes, class_type, emblem_hash, clear_space, items, parameters, created_by, last_updated_by) +): Promise { + const response = await client.query({ + name: 'upsert_loadout', + text: `insert into loadouts (id, membership_id, platform_membership_id, destiny_version, name, notes, class_type, emblem_hash, clear_space, items, parameters, created_by, last_updated_by) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $12) on conflict (membership_id, id) do update set (name, notes, class_type, emblem_hash, clear_space, items, parameters, last_updated_at, last_updated_by) = ($5, $6, $7, $8, $9, $10, $11, current_timestamp, $12)`, - values: [ - loadout.id, - bungieMembershipId, - platformMembershipId, - destinyVersion, - 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.loadouts.noRowUpdated.count', 1); - throw new Error('loadouts - No row was updated'); - } - - return response; - } catch (e) { - throw new Error(e.name + ': ' + e.message); + values: [ + loadout.id, + bungieMembershipId, + platformMembershipId, + destinyVersion, + 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.loadouts.noRowUpdated.count', 1); + throw new Error('loadouts - No row was updated'); } + + return response; } /** @@ -171,22 +170,14 @@ export async function deleteLoadout( client: ClientBase, bungieMembershipId: number, loadoutId: string, -): Promise { - try { - const response = await client.query({ - name: 'delete_loadout', - text: `delete from loadouts where membership_id = $1 and id = $2 returning *`, - values: [bungieMembershipId, loadoutId], - }); - - if (response.rowCount! < 1) { - return null; - } - - return convertLoadout(response.rows[0]); - } catch (e) { - throw new Error(e.name + ': ' + e.message); - } +): Promise { + const response = await client.query({ + name: 'delete_loadout', + text: `delete from loadouts where membership_id = $1 and id = $2`, + values: [bungieMembershipId, loadoutId], + }); + + return response.rowCount! >= 1; } /** @@ -195,14 +186,10 @@ export async function deleteLoadout( export async function deleteAllLoadouts( client: ClientBase, bungieMembershipId: number, -): Promise> { - try { - return client.query({ - name: 'delete_all_loadouts', - text: `delete from loadouts where membership_id = $1`, - values: [bungieMembershipId], - }); - } catch (e) { - throw new Error(e.name + ': ' + e.message); - } +): Promise { + return client.query({ + name: 'delete_all_loadouts', + text: `delete from loadouts where membership_id = $1`, + values: [bungieMembershipId], + }); } diff --git a/api/db/searches-queries.ts b/api/db/searches-queries.ts index 15655af..a2bcd68 100644 --- a/api/db/searches-queries.ts +++ b/api/db/searches-queries.ts @@ -4,6 +4,12 @@ import { metrics } from '../metrics/index.js'; import { ExportResponse } from '../shapes/export.js'; import { DestinyVersion } from '../shapes/general.js'; import { Search, SearchType } from '../shapes/search.js'; +import { KeysToSnakeCase } from '../utils.js'; + +interface SearchRow extends KeysToSnakeCase> { + last_updated_at: Date; + search_type: SearchType; +} /* * 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. @@ -46,22 +52,18 @@ export async function getSearchesForProfile( bungieMembershipId: number, destinyVersion: DestinyVersion, ): Promise { - try { - const results = await client.query({ - name: 'get_searches', - // TODO: order by frecency - 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( - results.rows - .map(convertSearch) - .concat(destinyVersion === 2 ? cannedSearchesForD2 : cannedSearchesForD1), - (s) => s.query, - ); - } catch (e) { - throw new Error(e.name + ': ' + e.message); - } + const results = await client.query({ + name: 'get_searches', + // TODO: order by frecency + 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( + results.rows + .map(convertSearch) + .concat(destinyVersion === 2 ? cannedSearchesForD2 : cannedSearchesForD1), + (s) => s.query, + ); } /** @@ -72,22 +74,18 @@ export async function getSearchesForUser( bungieMembershipId: number, ): Promise { // TODO: this isn't indexed! - try { - const results = await client.query({ - name: 'get_all_searches', - 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) => ({ - destinyVersion: row.destiny_version, - search: convertSearch(row), - })); - } catch (e) { - throw new Error(e.name + ': ' + e.message); - } + const results = await client.query({ + name: 'get_all_searches', + 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) => ({ + destinyVersion: row.destiny_version, + search: convertSearch(row), + })); } -function convertSearch(row: any): Search { +function convertSearch(row: SearchRow): Search { return { query: row.query, usageCount: row.usage_count, @@ -109,27 +107,23 @@ export async function updateUsedSearch( destinyVersion: DestinyVersion, query: string, type: SearchType, -): Promise> { - try { - const response = await client.query({ - name: 'upsert_search', - text: `insert INTO searches (membership_id, destiny_version, query, search_type, created_by, last_updated_by) +): Promise { + const response = await client.query({ + name: 'upsert_search', + 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, type], - }); - - if (response.rowCount! < 1) { - // This should never happen! - metrics.increment('db.searches.noRowUpdated.count', 1); - throw new Error('searches - No row was updated'); - } + values: [bungieMembershipId, destinyVersion, query, appId, type], + }); - return response; - } catch (e) { - throw new Error(e.name + ': ' + e.message); + if (response.rowCount! < 1) { + // This should never happen! + metrics.increment('db.searches.noRowUpdated.count', 1); + throw new Error('searches - No row was updated'); } + + return response; } /** @@ -143,30 +137,26 @@ export async function saveSearch( query: string, type: SearchType, saved?: boolean, -): Promise> { - try { - const response = await client.query({ - name: 'save_search', - text: `UPDATE searches SET (saved, last_updated_by) = ($4, $5) WHERE membership_id = $1 AND destiny_version = $2 AND qhash = decode(md5($3), 'hex') AND query = $3`, - values: [bungieMembershipId, destinyVersion, query, saved, appId], - }); +): Promise { + const response = await client.query({ + name: 'save_search', + text: `UPDATE searches SET (saved, last_updated_by) = ($4, $5) WHERE membership_id = $1 AND destiny_version = $2 AND qhash = decode(md5($3), 'hex') AND query = $3`, + values: [bungieMembershipId, destinyVersion, query, saved, appId], + }); - if (response.rowCount! < 1) { - // Someone saved a search they haven't used! - 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, search_type, saved, created_by, last_updated_by) + if (response.rowCount! < 1) { + // Someone saved a search they haven't used! + 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, search_type, saved, created_by, last_updated_by) values ($1, $2, $3, $5, true, $4, $4)`, - values: [bungieMembershipId, destinyVersion, query, appId, type], - }); - return insertSavedResponse; - } - - return response; - } catch (e) { - throw new Error(e.name + ': ' + e.message); + values: [bungieMembershipId, destinyVersion, query, appId, type], + }); + return insertSavedResponse; } + + return response; } /** * Insert a single search as part of an import. @@ -181,34 +171,30 @@ export async function importSearch( lastUsage: number, usageCount: number, type: SearchType, -): Promise> { - try { - const response = await client.query({ - name: 'insert_search', - text: `insert INTO searches (membership_id, destiny_version, query, saved, search_type, usage_count, last_used, created_by, last_updated_by) +): Promise { + const response = await client.query({ + name: 'insert_search', + 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, - query, - saved, - usageCount, - new Date(lastUsage), - appId, - type, - ], - }); - - if (response.rowCount! < 1) { - // This should never happen! - metrics.increment('db.searches.noRowUpdated.count', 1); - throw new Error('searches - No row was updated'); - } + values: [ + bungieMembershipId, + destinyVersion, + query, + saved, + usageCount, + new Date(lastUsage), + appId, + type, + ], + }); - return response; - } catch (e) { - throw new Error(e.name + ': ' + e.message); + if (response.rowCount! < 1) { + // This should never happen! + metrics.increment('db.searches.noRowUpdated.count', 1); + throw new Error('searches - No row was updated'); } + + return response; } /** @@ -220,16 +206,12 @@ export async function deleteSearch( destinyVersion: DestinyVersion, query: string, type: SearchType, -): Promise> { - 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 and search_type = $4`, - values: [bungieMembershipId, destinyVersion, query, type], - }); - } catch (e) { - throw new Error(e.name + ': ' + e.message); - } +): Promise { + 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 and search_type = $4`, + values: [bungieMembershipId, destinyVersion, query, type], + }); } /** @@ -238,14 +220,10 @@ export async function deleteSearch( export async function deleteAllSearches( client: ClientBase, bungieMembershipId: number, -): Promise> { - try { - return client.query({ - name: 'delete_all_searches', - text: `delete from searches where membership_id = $1`, - values: [bungieMembershipId], - }); - } catch (e) { - throw new Error(e.name + ': ' + e.message); - } +): Promise { + return client.query({ + name: 'delete_all_searches', + text: `delete from searches where membership_id = $1`, + values: [bungieMembershipId], + }); } diff --git a/api/db/settings-queries.ts b/api/db/settings-queries.ts index c39f44a..a98282f 100644 --- a/api/db/settings-queries.ts +++ b/api/db/settings-queries.ts @@ -8,16 +8,12 @@ export async function getSettings( client: ClientBase, bungieMembershipId: number, ): Promise> { - try { - const results = await client.query<{ settings: Settings }>({ - name: 'get_settings', - text: 'SELECT settings FROM settings WHERE membership_id = $1', - values: [bungieMembershipId], - }); - return results.rows.length > 0 ? results.rows[0].settings : {}; - } catch (e) { - throw new Error(e.name + ': ' + e.message); - } + const results = await client.query<{ settings: Settings }>({ + name: 'get_settings', + text: 'SELECT settings FROM settings WHERE membership_id = $1', + values: [bungieMembershipId], + }); + return results.rows.length > 0 ? results.rows[0].settings : {}; } /** @@ -28,20 +24,16 @@ export async function replaceSettings( appId: string, bungieMembershipId: number, settings: Settings, -): Promise> { - try { - const result = await client.query({ - name: 'upsert_settings', - text: `insert into settings (membership_id, settings, created_by, last_updated_by) +): Promise { + const result = await client.query({ + name: 'upsert_settings', + text: `insert into settings (membership_id, settings, created_by, last_updated_by) values ($1, $2, $3, $3) on conflict (membership_id) do update set (settings, last_updated_at, last_updated_by) = ($2, current_timestamp, $3)`, - values: [bungieMembershipId, settings, appId], - }); - return result; - } catch (e) { - throw new Error(e.name + ': ' + e.message); - } + values: [bungieMembershipId, settings, appId], + }); + return result; } /** @@ -52,19 +44,15 @@ export async function setSetting( appId: string, bungieMembershipId: number, settings: Partial, -): Promise> { - try { - return client.query({ - name: 'set_setting', - text: `insert into settings (membership_id, settings, created_by, last_updated_by) +): Promise { + return client.query({ + name: 'set_setting', + text: `insert into settings (membership_id, settings, created_by, last_updated_by) values ($1, $2, $3, $3) on conflict (membership_id) do update set (settings, last_updated_at, last_updated_by) = (settings.settings || $2, current_timestamp, $3)`, - values: [bungieMembershipId, settings, appId], - }); - } catch (e) { - throw new Error(e.name + ': ' + e.message); - } + values: [bungieMembershipId, settings, appId], + }); } /** @@ -73,14 +61,10 @@ do update set (settings, last_updated_at, last_updated_by) = (settings.settings export async function deleteSettings( client: ClientBase, bungieMembershipId: number, -): Promise> { - try { - return client.query({ - name: 'delete_settings', - text: `delete FROM settings WHERE membership_id = $1`, - values: [bungieMembershipId], - }); - } catch (e) { - throw new Error(e.name + ': ' + e.message); - } +): Promise { + return client.query({ + name: 'delete_settings', + text: `delete FROM settings WHERE membership_id = $1`, + values: [bungieMembershipId], + }); } diff --git a/api/db/triumphs-queries.ts b/api/db/triumphs-queries.ts index ee0d0e0..e9718d4 100644 --- a/api/db/triumphs-queries.ts +++ b/api/db/triumphs-queries.ts @@ -9,7 +9,7 @@ export async function getTrackedTriumphsForProfile( bungieMembershipId: number, platformMembershipId: string, ): Promise { - const results = await client.query({ + const results = await client.query<{ record_hash: string }>({ name: 'get_tracked_triumphs', text: 'SELECT record_hash FROM tracked_triumphs WHERE membership_id = $1 and platform_membership_id = $2', values: [bungieMembershipId, platformMembershipId], @@ -29,7 +29,7 @@ export async function getAllTrackedTriumphsForUser( triumphs: number[]; }[] > { - const results = await client.query({ + const results = await client.query<{ platform_membership_id: string; record_hash: string }>({ name: 'get_all_tracked_triumphs', text: 'SELECT platform_membership_id, record_hash FROM tracked_triumphs WHERE membership_id = $1', values: [bungieMembershipId], @@ -38,9 +38,7 @@ export async function getAllTrackedTriumphsForUser( const triumphsByAccount: { [platformMembershipId: string]: number[] } = {}; for (const row of results.rows) { - triumphsByAccount[row.platform_membership_id] = - triumphsByAccount[row.platform_membership_id] || []; - triumphsByAccount[row.platform_membership_id].push(parseInt(row.record_hash, 10)); + (triumphsByAccount[row.platform_membership_id] ||= []).push(parseInt(row.record_hash, 10)); } return Object.entries(triumphsByAccount).map(([platformMembershipId, triumphs]) => ({ @@ -58,7 +56,7 @@ export async function trackTriumph( bungieMembershipId: number, platformMembershipId: string, recordHash: number, -): Promise> { +): Promise { const response = await client.query({ name: 'insert_tracked_triumph', text: `insert INTO tracked_triumphs (membership_id, platform_membership_id, record_hash, created_by) @@ -78,7 +76,7 @@ export async function unTrackTriumph( bungieMembershipId: number, platformMembershipId: string, recordHash: number, -): Promise> { +): Promise { const response = await client.query({ name: 'delete_tracked_triumph', text: `delete from tracked_triumphs where membership_id = $1 and platform_membership_id = $2 and record_hash = $3`, @@ -99,7 +97,7 @@ export async function unTrackTriumph( export async function deleteAllTrackedTriumphs( client: ClientBase, bungieMembershipId: number, -): Promise> { +): Promise { return client.query({ name: 'delete_all_tracked_triumphs', text: `delete from tracked_triumphs where membership_id = $1`, diff --git a/api/dim-gg/loadout-share-view.ts b/api/dim-gg/loadout-share-view.ts index 8224593..169a2e0 100644 --- a/api/dim-gg/loadout-share-view.ts +++ b/api/dim-gg/loadout-share-view.ts @@ -21,7 +21,7 @@ export const loadoutShareViewHandler = asyncHandler(async (req, res) => { if (!loadout) { // Instruct CF to cache for 15 minutes res.set('Cache-Control', 'max-age=900'); - res.status(404).sendFile(path.join(__dirname + '/views/loadout404.html')); + res.status(404).sendFile(path.join(`${__dirname }/views/loadout404.html`)); return; } @@ -52,7 +52,7 @@ export const loadoutShareViewHandler = asyncHandler(async (req, res) => { const description = loadout.notes ? loadout.notes.length > 197 - ? loadout.notes.substring(0, 197) + '...' + ? `${loadout.notes.substring(0, 197) }...` : loadout.notes : 'Destiny 2 loadout settings shared from DIM'; diff --git a/api/index.ts b/api/index.ts index 44daf91..0239774 100644 --- a/api/index.ts +++ b/api/index.ts @@ -53,28 +53,28 @@ switch (process.env.VHOST) { vhost('beta.dim.gg', (req, res: express.Response) => { // Instruct CF to cache for 15 minutes res.set('Cache-Control', 'max-age=900'); - res.redirect('https://beta.destinyitemmanager.com' + req.originalUrl); + res.redirect(`https://beta.destinyitemmanager.com${ req.originalUrl}`); }), ); app.use( vhost('app.dim.gg', (req, res: express.Response) => { // Instruct CF to cache for 15 minutes res.set('Cache-Control', 'max-age=900'); - res.redirect('https://app.destinyitemmanager.com' + req.originalUrl); + res.redirect(`https://app.destinyitemmanager.com${ req.originalUrl}`); }), ); app.use( vhost('pr.dim.gg', (req, res: express.Response) => { // Instruct CF to cache for 15 minutes res.set('Cache-Control', 'max-age=900'); - res.redirect('https://pr.destinyitemmanager.com' + req.originalUrl); + res.redirect(`https://pr.destinyitemmanager.com${ req.originalUrl}`); }), ); app.use( vhost('guide.dim.gg', (req, res: express.Response) => { // Instruct CF to cache for 15 minutes res.set('Cache-Control', 'max-age=900'); - res.redirect('https://github.com/DestinyItemManager/DIM/wiki' + req.originalUrl); + res.redirect(`https://github.com/DestinyItemManager/DIM/wiki${ req.originalUrl}`); }), ); } diff --git a/api/metrics/express.ts b/api/metrics/express.ts index 7eca2dd..034f267 100644 --- a/api/metrics/express.ts +++ b/api/metrics/express.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { Request, RequestHandler } from 'express'; import { StatsD } from 'hot-shots'; @@ -20,11 +22,11 @@ export default function expressStatsd({ // Status Code const statusCode = res.statusCode || 'unknown_status'; - client.increment(prefix + '.response_code.' + routeName + '.' + statusCode); + client.increment(`${prefix}.response_code.${routeName}.${statusCode}`); // Response Time const duration = performance.now() - startTime; - client.timing(prefix + '.response_time.' + routeName, duration); + client.timing(`${prefix}.response_time.${routeName}`, duration); cleanup(); } @@ -58,10 +60,11 @@ function sanitize(routeName: string) { // Extracts a route name from the request or response and sets it for use by the statsd middleware export function getRouteNameForStats(req: Request) { - if (req.route?.path) { - let routeName = req.route.path; + const route = req.route; + if (route?.path) { + let routeName: string & { source?: string } = route.path; if (Object.prototype.toString.call(routeName) === '[object RegExp]') { - routeName = routeName.source; + routeName = routeName.source!; } if (req.baseUrl) { @@ -72,9 +75,9 @@ export function getRouteNameForStats(req: Request) { const sanitizedRoute = sanitize(routeName); if (sanitizedRoute !== '') { - return req.method + '_' + sanitizedRoute; + return `${req.method}_${sanitizedRoute}`; } } - return req.method + '_unknown_express_route'; //req.method + '_' + sanitize(req.path); + return `${req.method}_unknown_express_route`; // req.method + '_' + sanitize(req.path); } diff --git a/api/metrics/index.ts b/api/metrics/index.ts index e36781b..7636591 100644 --- a/api/metrics/index.ts +++ b/api/metrics/index.ts @@ -4,6 +4,6 @@ export const metrics = new StatsD({ prefix: 'dim-api.', host: process.env.GRAPHITE_SERVICE_HOST || 'localhost', port: process.env.GRAPHITE_SERVICE_PORT_STATSD - ? parseInt(process.env.GRAPHITE_SERVICE_PORT_STATSD) + ? parseInt(process.env.GRAPHITE_SERVICE_PORT_STATSD, 10) : 31202, }); diff --git a/api/routes/auth-token.ts b/api/routes/auth-token.ts index 6c51d39..bbe019b 100644 --- a/api/routes/auth-token.ts +++ b/api/routes/auth-token.ts @@ -7,6 +7,7 @@ import { AuthTokenRequest, AuthTokenResponse } from '../shapes/auth.js'; import jwt, { Secret, SignOptions } from 'jsonwebtoken'; import _ from 'lodash'; import { metrics } from '../metrics/index.js'; +import { ApiApp } from '../shapes/app.js'; import { badRequest } from '../utils.js'; const TOKEN_EXPIRES_IN = 30 * 24 * 60 * 60; // 30 days @@ -15,7 +16,7 @@ const signJwt = util.promisify { const { bungieAccessToken, membershipId } = req.body as AuthTokenRequest; - const apiApp = req.dimApp; + const apiApp = req.dimApp as ApiApp; if (!bungieAccessToken) { badRequest(res, 'No bungieAccessToken provided'); @@ -41,8 +42,8 @@ export const authTokenHandler = asyncHandler(async (req, res) => { if (!bungieResponse.ok) { // TODO: try/catch - const errorBody = await bungieResponse.json(); - if (errorBody.ErrorStatus == 'WebAuthRequired') { + const errorBody = (await bungieResponse.json()) as ApiError; + if (errorBody.ErrorStatus === 'WebAuthRequired') { metrics.increment('authToken.webAuthRequired.count'); res.status(401).send({ error: 'WebAuthRequired', @@ -122,3 +123,8 @@ export const authTokenHandler = asyncHandler(async (req, res) => { throw e; } }); + +interface ApiError { + ErrorStatus: string; + Message: string; +} diff --git a/api/routes/create-app.ts b/api/routes/create-app.ts index 68ac63a..3293f44 100644 --- a/api/routes/create-app.ts +++ b/api/routes/create-app.ts @@ -1,4 +1,5 @@ import asyncHandler from 'express-async-handler'; +import { DatabaseError } from 'pg'; import { v4 as uuid } from 'uuid'; import { getAppById, insertApp } from '../db/apps-queries.js'; import { transaction } from '../db/index.js'; @@ -52,7 +53,7 @@ export const createAppHandler = asyncHandler(async (req, res) => { await insertApp(client, app); } catch (e) { // This is a unique constraint violation, so just get the app! - if (e.code == '23505') { + if (e instanceof DatabaseError && e.code === '23505') { await client.query('ROLLBACK'); app = (await getAppById(client, request.id))!; } else { diff --git a/api/routes/delete-all-data.ts b/api/routes/delete-all-data.ts index 72d878c..0ada445 100644 --- a/api/routes/delete-all-data.ts +++ b/api/routes/delete-all-data.ts @@ -8,12 +8,13 @@ import { deleteAllSearches } from '../db/searches-queries.js'; import { deleteSettings } from '../db/settings-queries.js'; import { deleteAllTrackedTriumphs } from '../db/triumphs-queries.js'; import { DeleteAllResponse } from '../shapes/delete-all.js'; +import { UserInfo } from '../shapes/user.js'; /** * Delete My Data - this allows a user to wipe all their data from DIM storage. */ export const deleteAllDataHandler = asyncHandler(async (req, res) => { - const { bungieMembershipId } = req.user; + const { bungieMembershipId } = req.user as UserInfo; const result = await transaction(async (client) => { const deleted = await deleteAllData(client, bungieMembershipId); diff --git a/api/routes/donate.ts b/api/routes/donate.ts index 2a5ec07..526fba3 100644 --- a/api/routes/donate.ts +++ b/api/routes/donate.ts @@ -15,6 +15,10 @@ export const donateHandler = asyncHandler(async (_req, res) => { } catch (e) { res.set('Cache-Control', 'max-age=60'); res.status(500); - res.send(e.message); + if (e instanceof Error) { + res.send(e.message); + } else { + res.send(`Unknown error: ${e as string}`); + } } }); diff --git a/api/routes/export.ts b/api/routes/export.ts index 303dbfa..7b56491 100644 --- a/api/routes/export.ts +++ b/api/routes/export.ts @@ -7,9 +7,10 @@ import { getSearchesForUser } from '../db/searches-queries.js'; import { getSettings } from '../db/settings-queries.js'; import { getAllTrackedTriumphsForUser } from '../db/triumphs-queries.js'; import { ExportResponse } from '../shapes/export.js'; +import { UserInfo } from '../shapes/user.js'; export const exportHandler = asyncHandler(async (req, res) => { - const { bungieMembershipId } = req.user; + const { bungieMembershipId } = req.user as UserInfo; const response = await readTransaction(async (client) => { const settings = await getSettings(client, bungieMembershipId); diff --git a/api/routes/import.ts b/api/routes/import.ts index ffdc847..7c84071 100644 --- a/api/routes/import.ts +++ b/api/routes/import.ts @@ -7,6 +7,7 @@ import { updateLoadout } from '../db/loadouts-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 { ExportResponse } from '../shapes/export.js'; import { DestinyVersion } from '../shapes/general.js'; import { ImportResponse } from '../shapes/import.js'; @@ -14,12 +15,13 @@ 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 { UserInfo } from '../shapes/user.js'; import { badRequest } from '../utils.js'; import { deleteAllData } from './delete-all-data.js'; export const importHandler = asyncHandler(async (req, res) => { - const { bungieMembershipId } = req.user; - const { id: appId } = req.dimApp; + const { bungieMembershipId } = req.user as UserInfo; + const { id: appId } = req.dimApp as ApiApp; // Support only new API exports const importData = req.body as ExportResponse; @@ -127,8 +129,8 @@ export const importHandler = asyncHandler(async (req, res) => { }); /** Produce a new object that's only the key/values of obj that are also keys in defaults and which have values different from defaults. */ -function subtractObject(obj: object | undefined, defaults: object) { - const result = {}; +function subtractObject(obj: Partial, defaults: T): T { + const result: Partial = {}; if (obj) { for (const key in defaults) { if (obj[key] !== undefined && obj[key] !== defaults[key]) { @@ -136,11 +138,11 @@ function subtractObject(obj: object | undefined, defaults: object) { } } } - return result; + return result as T; } function extractSettings(importData: ExportResponse): Settings { - return subtractObject(importData.settings, defaultSettings) as Settings; + return subtractObject(importData.settings, defaultSettings); } type PlatformLoadout = Loadout & { diff --git a/api/routes/loadout-share.ts b/api/routes/loadout-share.ts index 6f61f95..d491072 100644 --- a/api/routes/loadout-share.ts +++ b/api/routes/loadout-share.ts @@ -1,15 +1,18 @@ import crypto from 'crypto'; import asyncHandler from 'express-async-handler'; import base32 from 'hi-base32'; +import { DatabaseError } from 'pg'; import { transaction } from '../db/index.js'; import { addLoadoutShare, getLoadoutShare, recordAccess } from '../db/loadout-share-queries.js'; import { metrics } from '../metrics/index.js'; +import { ApiApp } from '../shapes/app.js'; import { GetSharedLoadoutResponse, LoadoutShareRequest, LoadoutShareResponse, } from '../shapes/loadout-share.js'; import { Loadout } from '../shapes/loadouts.js'; +import { UserInfo } from '../shapes/user.js'; import slugify from './slugify.js'; import { validateLoadout } from './update.js'; @@ -25,9 +28,9 @@ const getShareURL = (loadout: Loadout, shareId: string) => { * Save a loadout to be shared via a dim.gg link. */ export const loadoutShareHandler = asyncHandler(async (req, res) => { - const { bungieMembershipId } = req.user; - const { id: appId } = req.dimApp; - metrics.increment('loadout_share.app.' + appId, 1); + const { bungieMembershipId } = req.user as UserInfo; + const { id: appId } = req.dimApp as ApiApp; + metrics.increment(`loadout_share.app.${appId}`, 1); const request = req.body as LoadoutShareRequest; const { platformMembershipId, loadout } = request; @@ -47,7 +50,7 @@ export const loadoutShareHandler = asyncHandler(async (req, res) => { } const shareId = await transaction(async (client) => { - const attempts = 0; + let attempts = 0; // We'll make three attempts to guess a random non-colliding number while (attempts < 4) { const shareId = generateRandomShareId(); @@ -63,12 +66,13 @@ export const loadoutShareHandler = asyncHandler(async (req, res) => { return shareId; } catch (e) { // This is a unique constraint violation, generate another random share ID - if (e.code == '23505') { + if (e instanceof DatabaseError && e.code === '23505') { // try again! } else { throw e; } } + attempts++; } return 'ran-out'; }); diff --git a/api/routes/profile.ts b/api/routes/profile.ts index b757370..d0ed6f2 100644 --- a/api/routes/profile.ts +++ b/api/routes/profile.ts @@ -8,9 +8,11 @@ import { getSearchesForProfile } from '../db/searches-queries.js'; import { getSettings } from '../db/settings-queries.js'; import { getTrackedTriumphsForProfile } from '../db/triumphs-queries.js'; import { metrics } from '../metrics/index.js'; +import { ApiApp } from '../shapes/app.js'; import { DestinyVersion } from '../shapes/general.js'; import { ProfileResponse } from '../shapes/profile.js'; import { defaultSettings } from '../shapes/settings.js'; +import { UserInfo } from '../shapes/user.js'; import { badRequest, checkPlatformMembershipId, isValidPlatformMembershipId } from '../utils.js'; const validComponents = new Set([ @@ -23,9 +25,9 @@ const validComponents = new Set([ ]); export const profileHandler = asyncHandler(async (req, res) => { - const { bungieMembershipId, profileIds } = req.user; - const { id: appId } = req.dimApp; - metrics.increment('profile.app.' + appId, 1); + const { bungieMembershipId, profileIds } = req.user as UserInfo; + const { id: appId } = req.dimApp as ApiApp; + metrics.increment(`profile.app.${appId}`, 1); const platformMembershipId = req.query.platformMembershipId?.toString(); @@ -36,12 +38,13 @@ export const profileHandler = asyncHandler(async (req, res) => { checkPlatformMembershipId(platformMembershipId, profileIds, 'profile'); - const destinyVersion: DestinyVersion = req.query.destinyVersion - ? (parseInt(req.query.destinyVersion.toString(), 10) as DestinyVersion) - : 2; + const destinyVersion: DestinyVersion = + req.query.destinyVersion && typeof req.query.destinyVersion === 'string' + ? (parseInt(req.query.destinyVersion.toString(), 10) as DestinyVersion) + : 2; if (destinyVersion !== 1 && destinyVersion !== 2) { - badRequest(res, `destinyVersion ${destinyVersion} is not in the right format`); + badRequest(res, `destinyVersion ${destinyVersion as number} is not in the right format`); return; } @@ -70,20 +73,20 @@ export const profileHandler = asyncHandler(async (req, res) => { const storedSettings = await getSettings(client, bungieMembershipId); // Clean out deprecated settings (TODO purge from DB) - delete storedSettings['allowIdPostToDtr']; - delete storedSettings['colorA11y']; - delete storedSettings['itemDetails']; - delete storedSettings['itemPickerEquip']; - delete storedSettings['itemSort']; - delete storedSettings['loAssumeMasterwork']; - delete storedSettings['loLockItemEnergyType']; - delete storedSettings['loMinPower']; - delete storedSettings['loMinStatTotal']; - delete storedSettings['loStatSortOrder']; - delete storedSettings['loUpgradeSpendTier']; - delete storedSettings['reviewsModeSelection']; - delete storedSettings['reviewsPlatformSelectionV2']; - delete storedSettings['showReviews']; + delete (storedSettings as Record).allowIdPostToDtr; + delete (storedSettings as Record).colorA11y; + delete (storedSettings as Record).itemDetails; + delete (storedSettings as Record).itemPickerEquip; + delete (storedSettings as Record).itemSort; + delete (storedSettings as Record).loAssumeMasterwork; + delete (storedSettings as Record).loLockItemEnergyType; + delete (storedSettings as Record).loMinPower; + delete (storedSettings as Record).loMinStatTotal; + delete (storedSettings as Record).loStatSortOrder; + delete (storedSettings as Record).loUpgradeSpendTier; + delete (storedSettings as Record).reviewsModeSelection; + delete (storedSettings as Record).reviewsPlatformSelectionV2; + delete (storedSettings as Record).showReviews; response.settings = { ...defaultSettings, diff --git a/api/routes/update.ts b/api/routes/update.ts index 111a4b0..bc2ffd4 100644 --- a/api/routes/update.ts +++ b/api/routes/update.ts @@ -18,6 +18,7 @@ import { import { setSetting as setSettingInDb } from '../db/settings-queries.js'; import { trackTriumph as trackTriumphInDb, unTrackTriumph } from '../db/triumphs-queries.js'; import { metrics } from '../metrics/index.js'; +import { ApiApp } from '../shapes/app.js'; import { DestinyVersion } from '../shapes/general.js'; import { ItemAnnotation } from '../shapes/item-annotations.js'; import { Loadout } from '../shapes/loadouts.js'; @@ -32,6 +33,7 @@ import { } from '../shapes/profile.js'; import { SearchType } from '../shapes/search.js'; import { Settings } from '../shapes/settings.js'; +import { UserInfo } from '../shapes/user.js'; import { badRequest, checkPlatformMembershipId, @@ -46,9 +48,9 @@ import { * Note that you can't mix updates for multiple profiles - you'll have to make multiple requests. */ export const updateHandler = asyncHandler(async (req, res) => { - const { bungieMembershipId, profileIds } = req.user; - const { id: appId } = req.dimApp; - metrics.increment('update.app.' + appId, 1); + const { bungieMembershipId, profileIds } = req.user as UserInfo; + const { id: appId } = req.dimApp as ApiApp; + metrics.increment(`update.app.${appId}`, 1); const request = req.body as ProfileUpdateRequest; const { platformMembershipId, updates } = request; const destinyVersion = request.destinyVersion ?? 2; @@ -61,7 +63,7 @@ export const updateHandler = asyncHandler(async (req, res) => { checkPlatformMembershipId(platformMembershipId, profileIds, 'update'); if (destinyVersion !== 1 && destinyVersion !== 2) { - badRequest(res, `destinyVersion ${destinyVersion} is not in the right format`); + badRequest(res, `destinyVersion ${destinyVersion as number} is not in the right format`); return; } @@ -76,7 +78,7 @@ export const updateHandler = asyncHandler(async (req, res) => { for (const update of updates) { let result: ProfileUpdateResult; - metrics.increment('update.action.' + update.action + '.count'); + metrics.increment(`update.action.${update.action}.count`); switch (update.action) { case 'setting': @@ -153,13 +155,13 @@ export const updateHandler = asyncHandler(async (req, res) => { default: console.warn( - `Unknown action type: ${(update as any).action} from ${appId}, ${req.header( + `Unknown action type: ${(update as { action: string }).action} from ${appId}, ${req.header( 'User-Agent', )}, ${req.header('Referer')}`, ); result = { status: 'InvalidArgument', - message: `Unknown action type: ${(update as any).action}`, + message: `Unknown action type: ${(update as { action: string }).action}`, }; } results.push(result); @@ -225,14 +227,14 @@ async function updateLoadout( export function validateLoadout(metricPrefix: string, loadout: Loadout) { if (!loadout.name) { - metrics.increment(metricPrefix + '.validation.loadoutNameMissing.count'); + metrics.increment(`${metricPrefix}.validation.loadoutNameMissing.count`); return { status: 'InvalidArgument', message: 'Loadout name missing', }; } if (loadout.name.length > 120) { - metrics.increment(metricPrefix + '.validation.loadoutNameTooLong.count'); + metrics.increment(`${metricPrefix}.validation.loadoutNameTooLong.count`); return { status: 'InvalidArgument', message: 'Loadout names must be under 120 characters', @@ -240,7 +242,7 @@ export function validateLoadout(metricPrefix: string, loadout: Loadout) { } if (loadout.notes && loadout.notes.length > 2048) { - metrics.increment(metricPrefix + '.validation.loadoutNotesTooLong.count'); + metrics.increment(`${metricPrefix}.validation.loadoutNotesTooLong.count`); return { status: 'InvalidArgument', message: 'Loadout notes must be under 2048 characters', @@ -248,14 +250,14 @@ export function validateLoadout(metricPrefix: string, loadout: Loadout) { } if (!loadout.id) { - metrics.increment(metricPrefix + '.loadoutIdMissing.count'); + metrics.increment(`${metricPrefix}.loadoutIdMissing.count`); return { status: 'InvalidArgument', message: 'Loadout id missing', }; } if (loadout.id && loadout.id.length > 120) { - metrics.increment(metricPrefix + '.validation.loadoutIdTooLong.count'); + metrics.increment(`${metricPrefix}.validation.loadoutIdTooLong.count`); return { status: 'InvalidArgument', message: 'Loadout ids must be under 120 characters', @@ -263,21 +265,21 @@ export function validateLoadout(metricPrefix: string, loadout: Loadout) { } if (!Number.isFinite(loadout.classType)) { - metrics.increment(metricPrefix + '.validation.classTypeMissing.count'); + metrics.increment(`${metricPrefix}.validation.classTypeMissing.count`); return { status: 'InvalidArgument', message: 'Loadout class type missing or malformed', }; } if (loadout.classType < 0 || loadout.classType > 3) { - metrics.increment(metricPrefix + '.validation.classTypeOutOfRange.count'); + metrics.increment(`${metricPrefix}.validation.classTypeOutOfRange.count`); return { status: 'InvalidArgument', message: 'Loadout class type out of range', }; } if ([...loadout.equipped, ...loadout.unequipped].some((i) => i.id && !isValidItemId(i.id))) { - metrics.increment(metricPrefix + '.validation.itemIdFormat.count'); + metrics.increment(`${metricPrefix}.validation.itemIdFormat.count`); return { status: 'InvalidArgument', message: 'Item ID is invalid', @@ -295,7 +297,7 @@ async function deleteLoadout( const start = new Date(); const loadout = await deleteLoadoutInDb(client, bungieMembershipId, loadoutId); metrics.timing('update.deleteLoadout', start); - if (loadout == null) { + if (loadout === null) { return { status: 'NotFound', message: 'No loadout found with that ID' }; } diff --git a/api/server.test.ts b/api/server.test.ts index 8d2bb01..e67e8c4 100644 --- a/api/server.test.ts +++ b/api/server.test.ts @@ -1,6 +1,5 @@ import { readFile } from 'fs'; import { sign } from 'jsonwebtoken'; -import _ from 'lodash'; import { makeFetch } from 'supertest-fetch'; import { promisify } from 'util'; import { v4 as uuid } from 'uuid'; @@ -8,6 +7,7 @@ import { refreshApps } from './apps/index.js'; import { closeDbPool } from './db/index.js'; import { app } from './server.js'; import { ApiApp } from './shapes/app.js'; +import { DeleteAllResponse } from './shapes/delete-all.js'; import { ExportResponse } from './shapes/export.js'; import { PlatformInfoResponse } from './shapes/global-settings.js'; import { ImportResponse } from './shapes/import.js'; @@ -161,9 +161,11 @@ describe('profile', () => { }); it('can delete all data with /delete_all_data', async () => { - const response = await postRequestAuthed('/delete_all_data').expect(200); + const response = (await postRequestAuthed('/delete_all_data') + .expect(200) + .json()) as DeleteAllResponse; - expect((await response.json()).deleted).toEqual({ + expect(response.deleted).toEqual({ itemHashTags: 71, loadouts: 37, searches: 205, @@ -175,7 +177,7 @@ describe('profile', () => { // Now re-export and make sure it's all gone const exportResponse = (await getRequestAuthed('/export').expect(200).json()) as ExportResponse; - expect(_.size(exportResponse.settings)).toBe(0); + expect(Object.keys(exportResponse.settings).length).toBe(0); expect(exportResponse.loadouts.length).toBe(0); expect(exportResponse.tags.length).toBe(0); }); @@ -274,7 +276,7 @@ describe('loadouts', () => { expect(resultLoadout.clearSpace).toBe(loadout.clearSpace); expect(resultLoadout.equipped).toEqual(loadout.equipped); // This property should have been stripped - expect((resultLoadout.unequipped[0] as any).fizbuzz).toBeUndefined(); + expect((resultLoadout.unequipped[0] as { fizbuzz?: string }).fizbuzz).toBeUndefined(); }); it('can update a loadout', async () => { diff --git a/api/server.ts b/api/server.ts index 1aa1371..1e3f274 100644 --- a/api/server.ts +++ b/api/server.ts @@ -1,7 +1,8 @@ import * as Sentry from '@sentry/node'; import cors from 'cors'; -import express from 'express'; +import express, { ErrorRequestHandler } from 'express'; import { expressjwt as jwt } from 'express-jwt'; +import { JwtPayload } from 'jsonwebtoken'; import { apiKey, isAppOrigin } from './apps/index.js'; import expressStatsd from './metrics/express.js'; import { metrics } from './metrics/index.js'; @@ -15,6 +16,8 @@ import { getLoadoutShareHandler, loadoutShareHandler } from './routes/loadout-sh import { platformInfoHandler } from './routes/platform-info.js'; import { profileHandler } from './routes/profile.js'; import { updateHandler } from './routes/update.js'; +import { ApiApp } from './shapes/app.js'; +import { UserInfo } from './shapes/user.js'; export const app = express(); @@ -67,8 +70,9 @@ app.use(apiKeyCors); // Validate that the API key in the header is valid for this origin. app.use((req, res, next) => { - if (req.dimApp && req.headers.origin && req.dimApp.origin !== req.headers.origin) { - console.warn('OriginMismatch', req.dimApp?.id, req.dimApp?.origin, req.headers.origin); + const dimApp = req.dimApp as ApiApp | undefined; + if (dimApp && req.headers.origin && dimApp.origin !== req.headers.origin) { + console.warn('OriginMismatch', dimApp?.id, dimApp?.origin, req.headers.origin); metrics.increment('apiKey.wrongOrigin.count'); // TODO: sentry res.status(401).send({ @@ -104,19 +108,20 @@ app.use((req, _, next) => { console.error('JWT expected', req.path); next(new Error('Expected JWT info')); } else { - if (req.jwt.exp) { + const jwt = req.jwt as JwtPayload & { profileIds?: string[] }; + if (jwt.exp) { const nowSecs = Date.now() / 1000; - if (req.jwt.exp > nowSecs) { - metrics.timing('authToken.age', req.jwt.exp - nowSecs); + if (jwt.exp > nowSecs) { + metrics.timing('authToken.age', jwt.exp - nowSecs); } else { metrics.increment('authToken.expired.count'); } } req.user = { - bungieMembershipId: parseInt(req.jwt.sub, 10), - dimApiKey: req.jwt.iss!, - profileIds: req.jwt['profileIds'] ?? [], + bungieMembershipId: parseInt(jwt.sub!, 10), + dimApiKey: jwt.iss!, + profileIds: jwt.profileIds ?? [], }; next(); } @@ -124,8 +129,10 @@ app.use((req, _, next) => { // Validate that the auth token and the API key in the header match. app.use((req, res, next) => { - if (req.dimApp && req.dimApp.dimApiKey !== req.jwt.iss) { - console.warn('ApiKeyMismatch', req.dimApp?.id, req.dimApp?.dimApiKey, req.jwt.iss); + const dimApp = req.dimApp as ApiApp | undefined; + const jwt = req.jwt as JwtPayload & { profileIds?: string[] }; + if (dimApp && dimApp.dimApiKey !== jwt.iss) { + console.warn('ApiKeyMismatch', dimApp?.id, dimApp?.dimApiKey, jwt.iss); metrics.increment('apiKey.mismatch.count'); res.status(401).send({ error: 'ApiKeyMismatch', @@ -151,28 +158,33 @@ app.post('/delete_all_data', deleteAllDataHandler); // Share a loadout app.post('/loadout_share', loadoutShareHandler); -app.use((err: Error, req, res, _next) => { +const errorHandler: ErrorRequestHandler = (err, req, res, _next) => { + const dimApp = req.dimApp as ApiApp | undefined; + const user = req.user as UserInfo | undefined; Sentry.captureException(err); // Allow any origin to see the response res.header('Access-Control-Allow-Origin', '*'); - if (err.name === 'UnauthorizedError') { - console.warn('Unauthorized', req.dimApp?.id, req.originalUrl, err); + if (err instanceof Error && err.name === 'UnauthorizedError') { + console.warn('Unauthorized', dimApp?.id, req.originalUrl, err); res.status(401).send({ error: err.name, message: err.message, }); } else { + const e = err instanceof Error ? err : new Error(`${err}`); + console.error( 'ServerError', - req.dimApp?.id, + dimApp?.id, req.method, req.originalUrl, - req.user?.bungieMembershipId, + user?.bungieMembershipId, err, ); res.status(500).send({ - error: err.name, - message: err.message, + error: e.name, + message: e.message, }); } -}); +}; +app.use(errorHandler); diff --git a/api/shapes/loadouts.ts b/api/shapes/loadouts.ts index 0dfe311..76a18fe 100644 --- a/api/shapes/loadouts.ts +++ b/api/shapes/loadouts.ts @@ -198,12 +198,12 @@ export interface LoadoutParameters { */ export const defaultLoadoutParameters: LoadoutParameters = { statConstraints: [ - { statHash: 2996146975 }, //Mobility - { statHash: 392767087 }, //Resilience - { statHash: 1943323491 }, //Recovery - { statHash: 1735777505 }, //Discipline - { statHash: 144602215 }, //Intellect - { statHash: 4244567218 }, //Strength + { statHash: 2996146975 }, // Mobility + { statHash: 392767087 }, // Resilience + { statHash: 1943323491 }, // Recovery + { statHash: 1735777505 }, // Discipline + { statHash: 144602215 }, // Intellect + { statHash: 4244567218 }, // Strength ], mods: [], assumeArmorMasterwork: AssumeArmorMasterwork.None, diff --git a/api/tsconfig.eslint.json b/api/tsconfig.eslint.json new file mode 100644 index 0000000..b6b017c --- /dev/null +++ b/api/tsconfig.eslint.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "noEmit": true + }, + "extends": "./tsconfig.json", + "exclude": ["node_modules", "dist"], + "include": ["**/*.ts", "**/*.js", "**/*.cjs", "**/*.mjs", "**/*.json"] +} diff --git a/api/utils.ts b/api/utils.ts index c5215e7..acbb2ea 100644 --- a/api/utils.ts +++ b/api/utils.ts @@ -2,7 +2,100 @@ import { Response } from 'express'; import _ from 'lodash'; import { metrics } from './metrics/index.js'; -export function camelize(data: object) { +/** + * This is a utility function to extract the types of a subset of value types + * from an object based on an ordered set of keys. It's useful for typing a + * postgres insert. + * @example + * type MyArgsList = TypesForKeys<{a: string, b: number, c: boolean}, ['a', 'c']>; + */ +export type TypesForKeys, K extends (keyof T)[]> = { + [Index in keyof K]: T[K[Index]]; +}; + +/** Convert a snake_case string to camelCase */ +type CamelCase = S extends `${infer P1}_${infer P2}${infer P3}` + ? `${Lowercase}${Uppercase}${CamelCase}` + : Lowercase; + +/** + * Convert an object to a new object with snake_case keys replaced with camelCase. + */ +export type KeysToCamelCase = { + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + [K in keyof T as CamelCase]: T[K] extends {} ? KeysToCamelCase : T[K]; +}; + +type UpperCaseLetters = + | 'A' + | 'B' + | 'C' + | 'D' + | 'E' + | 'F' + | 'G' + | 'H' + | 'I' + | 'J' + | 'K' + | 'L' + | 'M' + | 'N' + | 'O' + | 'P' + | 'Q' + | 'R' + | 'S' + | 'T' + | 'U' + | 'V' + | 'W' + | 'X' + | 'Y' + | 'Z' + | '0' + | '1' + | '2' + | '3' + | '4' + | '5' + | '6' + | '7' + | '8' + | '9'; + +type SnakeCaseSeq = S extends `${infer P1}${infer P2}` + ? P1 extends UpperCaseLetters + ? `_${Lowercase}${SnakeCaseSeq}` + : `${P1}${SnakeCaseSeq}` + : Lowercase; + +/** + * Convert a camelCase string to snake_case + */ +export type SnakeCase = S extends `${infer P1}${infer P2}` + ? `${Lowercase}${SnakeCaseSeq}` + : Lowercase; + +type ObjectToSnakeCase = { + [K in keyof T as SnakeCase]: T[K] extends Record + ? KeysToSnakeCase + : T[K]; +}; + +/** + * Convert an object to a new object with camelCase keys replaced with snake_case. + */ +export type KeysToSnakeCase = { + [K in keyof T as SnakeCase]: T[K] extends any[] + ? KeysToSnakeCase[] + : ObjectToSnakeCase; +}; + +/** + * Convert an object to a new object with snake_case keys replaced with camelCase. + */ +export function camelize(data: KeysToSnakeCase): T { return _.mapKeys(data, (_value, key) => _.camelCase(key)) as T; } @@ -43,14 +136,13 @@ export function checkPlatformMembershipId( if (platformMembershipId) { if (profileIds.length) { metrics.increment( - metricsPrefix + - '.profileIds.' + - (profileIds.includes(platformMembershipId) ? 'match' : 'noMatch') + - '.count', + `${metricsPrefix}.profileIds.${ + profileIds.includes(platformMembershipId) ? 'match' : 'noMatch' + }.count`, 1, ); } else { - metrics.increment(metricsPrefix + '.profileIds.missing.count', 1); + metrics.increment(`${metricsPrefix}.profileIds.missing.count`, 1); } } } diff --git a/build-dim-api-types.sh b/build-dim-api-types.sh index 8e602b1..1697770 100755 --- a/build-dim-api-types.sh +++ b/build-dim-api-types.sh @@ -3,6 +3,7 @@ shopt -s extglob # Prepare the generated source directory rm -f dim-api-types/*.js +rm -f dim-api-types/*.cjs rm -f dim-api-types/*.ts rm -f api/shapes/index.ts rm -rf dim-api-types/esm diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..79f6f2e --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,229 @@ +import eslint from '@eslint/js'; +import * as regexpPlugin from 'eslint-plugin-regexp'; +import sonarjs from 'eslint-plugin-sonarjs'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +// TODO: different configs for JS vs TS +export default tseslint.config( + { name: 'eslint/recommended', ...eslint.configs.recommended }, + ...tseslint.configs.recommendedTypeChecked, + { + name: 'typescript-eslint/parser-options', + languageOptions: { + parserOptions: { + project: './api/tsconfig.eslint.json', + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + ...tseslint.configs.stylisticTypeChecked, + regexpPlugin.configs['flat/recommended'], + { name: 'sonarjs/recommended', ...sonarjs.configs.recommended }, + { + name: 'global ignores', + ignores: ['*.test.ts', '*/migrations/*'], + }, + { + name: 'dim-api-custom', + languageOptions: { + ecmaVersion: 'latest', + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + sourceType: 'module', + globals: { + ...globals.node, + }, + }, + linterOptions: { + reportUnusedDisableDirectives: true, + }, + rules: { + 'no-console': 'off', + 'no-empty': 'off', + 'require-atomic-updates': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + varsIgnorePattern: '(^_|[iI]gnored)', + argsIgnorePattern: '(^_|[iI]gnored)', + ignoreRestSiblings: true, + }, + ], + 'no-restricted-properties': [ + 1, + { + object: '_', + property: 'forEach', + message: 'Please use a for in loop.', + }, + { + object: '_', + property: 'filter', + message: 'Please use the native js filter.', + }, + { + object: '_', + property: 'map', + message: 'Please use the native js map.', + }, + { + object: '_', + property: 'uniq', + message: 'Please use Array.from(new Set(foo)) or [...new Set(foo)] instead.', + }, + { + object: '_', + property: 'forIn', + message: 'Please use Object.values or Object.entries instead', + }, + { + object: '_', + property: 'noop', + message: + 'Import noop directly instead of using it through _.noop, to satisfy the unbound-method lint', + }, + { + object: '_', + property: 'groupBy', + message: 'Use Object.groupBy or Map.groupBy instead.', + }, + { + object: '_', + property: 'cloneDeep', + message: 'Use structuredClone instead.', + }, + ], + 'no-restricted-syntax': [ + 'error', + { + selector: 'TSEnumDeclaration:not([const=true])', + message: 'Please only use `const enum`s.', + }, + ], + // TODO: Switch to @stylistic/eslint-plugin-js for this one rule + 'spaced-comment': [ + 'error', + 'always', + { exceptions: ['@__INLINE__'], block: { balanced: true } }, + ], + 'arrow-body-style': ['error', 'as-needed'], + curly: ['error', 'all'], + eqeqeq: ['error', 'always'], + 'no-return-await': 'off', + '@typescript-eslint/return-await': ['error', 'in-try-catch'], + 'prefer-regex-literals': 'error', + 'prefer-promise-reject-errors': 'error', + 'prefer-spread': 'error', + radix: 'error', + yoda: 'error', + 'prefer-template': 'error', + 'class-methods-use-this': ['error', { exceptMethods: ['render'] }], + 'no-unmodified-loop-condition': 'error', + 'no-unreachable-loop': 'error', + 'no-unused-private-class-members': 'error', + 'func-name-matching': 'error', + 'logical-assignment-operators': 'error', + 'no-lonely-if': 'error', + 'no-unneeded-ternary': 'error', + 'no-useless-call': 'error', + 'no-useless-concat': 'error', + 'no-useless-rename': 'error', + '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/no-misused-promises': [ + 'error', + { + checksVoidReturn: false, + }, + ], + '@typescript-eslint/ban-types': 'off', + '@typescript-eslint/explicit-member-accessibility': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/method-signature-style': 'error', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-use-before-define': ['error', { functions: false }], + '@typescript-eslint/no-parameter-properties': 'off', + '@typescript-eslint/no-extraneous-class': 'error', + '@typescript-eslint/no-this-alias': 'error', + '@typescript-eslint/no-unnecessary-type-constraint': 'error', + '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error', + '@typescript-eslint/no-unnecessary-qualifier': 'error', + '@typescript-eslint/no-unnecessary-type-assertion': 'error', + '@typescript-eslint/no-unnecessary-type-arguments': 'error', + '@typescript-eslint/prefer-function-type': 'error', + '@typescript-eslint/prefer-for-of': 'error', + '@typescript-eslint/prefer-optional-chain': 'error', + '@typescript-eslint/prefer-as-const': 'error', + '@typescript-eslint/prefer-reduce-type-parameter': 'error', + '@typescript-eslint/prefer-includes': 'error', + '@typescript-eslint/prefer-string-starts-ends-with': 'error', + '@typescript-eslint/prefer-ts-expect-error': 'error', + '@typescript-eslint/prefer-regexp-exec': 'off', + '@typescript-eslint/array-type': 'error', + '@typescript-eslint/no-non-null-asserted-optional-chain': 'error', + '@typescript-eslint/unified-signatures': 'error', + '@typescript-eslint/no-base-to-string': 'error', + '@typescript-eslint/non-nullable-type-assertion-style': 'error', + '@typescript-eslint/switch-exhaustiveness-check': 'error', + '@typescript-eslint/consistent-type-definitions': 'error', + '@typescript-eslint/consistent-generic-constructors': 'error', + '@typescript-eslint/no-duplicate-enum-values': 'error', + '@typescript-eslint/only-throw-error': 'error', + '@typescript-eslint/no-unused-expressions': [ + 'error', + { allowShortCircuit: true, allowTernary: true }, + ], + '@typescript-eslint/no-for-in-array': 'error', + '@typescript-eslint/consistent-indexed-object-style': 'off', + '@typescript-eslint/no-floating-promises': 'off', + '@typescript-eslint/require-await': 'off', + '@typescript-eslint/no-unsafe-enum-comparison': 'off', + '@typescript-eslint/prefer-nullish-coalescing': [ + 'off', + { + ignoreConditionalTests: true, + ignoreTernaryTests: false, + ignoreMixedLogicalExpressions: true, + ignorePrimitives: { + boolean: true, + number: false, + string: true, + }, + }, + ], + '@typescript-eslint/no-unsafe-argument': 'error', + '@typescript-eslint/no-unsafe-assignment': 'error', + '@typescript-eslint/no-unsafe-call': 'error', + '@typescript-eslint/no-unsafe-member-access': 'error', + '@typescript-eslint/no-unsafe-return': 'error', + '@typescript-eslint/no-redundant-type-constituents': 'off', + 'no-implied-eval': 'off', + '@typescript-eslint/no-implied-eval': 'error', + 'sonarjs/cognitive-complexity': 'off', + 'sonarjs/no-small-switch': 'off', + 'sonarjs/no-duplicate-string': 'off', + 'sonarjs/prefer-immediate-return': 'off', + 'sonarjs/no-nested-switch': 'off', + 'sonarjs/no-nested-template-literals': 'off', + }, + }, + { + files: ['src/**/*.cjs'], + rules: { + '@typescript-eslint/no-require-imports': 'off', + }, + }, + { + name: 'tests', + files: ['**/*.test.ts'], + rules: { + // We don't want to allow importing test modules in app modules, but of course you can do it in other test modules. + 'no-restricted-imports': 'off', + }, + }, +); diff --git a/package.json b/package.json index f4b2e00..32405a4 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ "docker:build": "rm -rf dist && pnpm build:api && docker build -t destinyitemmanager/dim-api .", "docker:run": "docker run -p 3000:3000 destinyitemmanager/dim-api:latest", "docker:push": "docker push destinyitemmanager/dim-api:latest", - "lint": "eslint --fix api --ext .js,.ts", - "lint-check": "eslint api --ext .js,.ts", + "lint": "eslint --fix api", + "lint-check": "eslint api", "test": "jest --verbose --coverage api --forceExit", "test:watch": "jest --watch", "dim-api-types:build": "./build-dim-api-types.sh" @@ -27,6 +27,8 @@ "@babel/plugin-transform-object-rest-spread": "^7.24.7", "@babel/preset-env": "^7.25.4", "@babel/preset-typescript": "^7.24.7", + "@eslint/compat": "^1.1.1", + "@eslint/js": "^9.9.1", "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-node-resolve": "^15.2.3", "@sentry/cli": "^2.34.1", @@ -43,8 +45,11 @@ "@typescript-eslint/parser": "^6.21.0", "db-migrate": "^0.11.14", "db-migrate-pg": "^1.5.2", - "eslint": "^8.57.0", + "eslint": "^9.9.1", "eslint-config-prettier": "^9.1.0", + "eslint-plugin-regexp": "^2.6.0", + "eslint-plugin-sonarjs": "^1.0.0", + "globals": "^15.9.0", "jest": "^29.7.0", "prettier": "^3.3.3", "prettier-plugin-organize-imports": "^3.2.4", @@ -54,7 +59,8 @@ "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", "tsx": "^4.19.0", - "typescript": "^5.5.4" + "typescript": "^5.5.4", + "typescript-eslint": "^8.3.0" }, "dependencies": { "@godaddy/terminus": "^4.12.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7aea536..9c7d9e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,6 +82,12 @@ devDependencies: '@babel/preset-typescript': specifier: ^7.24.7 version: 7.24.7(@babel/core@7.25.2) + '@eslint/compat': + specifier: ^1.1.1 + version: 1.1.1 + '@eslint/js': + specifier: ^9.9.1 + version: 9.9.1 '@rollup/plugin-babel': specifier: ^6.0.4 version: 6.0.4(@babel/core@7.25.2)(rollup@3.29.4) @@ -120,10 +126,10 @@ devDependencies: version: 3.0.9 '@typescript-eslint/eslint-plugin': specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.5.4) + version: 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@9.9.1)(typescript@5.5.4) '@typescript-eslint/parser': specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.0)(typescript@5.5.4) + version: 6.21.0(eslint@9.9.1)(typescript@5.5.4) db-migrate: specifier: ^0.11.14 version: 0.11.14 @@ -131,11 +137,20 @@ devDependencies: specifier: ^1.5.2 version: 1.5.2 eslint: - specifier: ^8.57.0 - version: 8.57.0 + specifier: ^9.9.1 + version: 9.9.1 eslint-config-prettier: specifier: ^9.1.0 - version: 9.1.0(eslint@8.57.0) + version: 9.1.0(eslint@9.9.1) + eslint-plugin-regexp: + specifier: ^2.6.0 + version: 2.6.0(eslint@9.9.1) + eslint-plugin-sonarjs: + specifier: ^1.0.0 + version: 1.0.4(eslint@9.9.1) + globals: + specifier: ^15.9.0 + version: 15.9.0 jest: specifier: ^29.7.0 version: 29.7.0(@types/node@20.8.4)(ts-node@10.9.2) @@ -166,6 +181,9 @@ devDependencies: typescript: specifier: ^5.5.4 version: 5.5.4 + typescript-eslint: + specifier: ^8.3.0 + version: 8.3.0(eslint@9.9.1)(typescript@5.5.4) packages: @@ -1816,29 +1834,50 @@ packages: dev: true optional: true - /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0): + /@eslint-community/eslint-utils@4.4.0(eslint@9.9.1): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: - eslint: 8.57.0 + eslint: 9.9.1 eslint-visitor-keys: 3.4.3 dev: true + /@eslint-community/regexpp@4.11.0: + resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + /@eslint-community/regexpp@4.9.1: resolution: {integrity: sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} dev: true - /@eslint/eslintrc@2.1.4: - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /@eslint/compat@1.1.1: + resolution: {integrity: sha512-lpHyRyplhGPL5mGEh6M9O5nnKk0Gz4bFI+Zu6tKlPpDUN7XshWvH9C/px4UVm87IAANE0W81CEsNGbS1KlzXpA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dev: true + + /@eslint/config-array@0.18.0: + resolution: {integrity: sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dependencies: + '@eslint/object-schema': 2.1.4 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@eslint/eslintrc@3.1.0: + resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: ajv: 6.12.6 debug: 4.3.4 - espree: 9.6.1 - globals: 13.23.0 + espree: 10.1.0 + globals: 14.0.0 ignore: 5.2.4 import-fresh: 3.3.0 js-yaml: 4.1.0 @@ -1848,9 +1887,14 @@ packages: - supports-color dev: true - /@eslint/js@8.57.0: - resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /@eslint/js@9.9.1: + resolution: {integrity: sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dev: true + + /@eslint/object-schema@2.1.4: + resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dev: true /@godaddy/terminus@4.12.1: @@ -1859,26 +1903,14 @@ packages: stoppable: 1.1.0 dev: false - /@humanwhocodes/config-array@0.11.14: - resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead - dependencies: - '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.4 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - dev: true - /@humanwhocodes/module-importer@1.0.1: resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} dev: true - /@humanwhocodes/object-schema@2.0.3: - resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} - deprecated: Use @eslint/object-schema instead + /@humanwhocodes/retry@0.3.0: + resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==} + engines: {node: '>=18.18'} dev: true /@istanbuljs/load-nyc-config@1.1.0: @@ -2652,7 +2684,7 @@ packages: '@types/yargs-parser': 21.0.1 dev: true - /@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.5.4): + /@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@9.9.1)(typescript@5.5.4): resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -2664,13 +2696,13 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.9.1 - '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/parser': 6.21.0(eslint@9.9.1)(typescript@5.5.4) '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.0)(typescript@5.5.4) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/type-utils': 6.21.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/utils': 6.21.0(eslint@9.9.1)(typescript@5.5.4) '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.3.4 - eslint: 8.57.0 + eslint: 9.9.1 graphemer: 1.4.0 ignore: 5.2.4 natural-compare: 1.4.0 @@ -2681,7 +2713,34 @@ packages: - supports-color dev: true - /@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.5.4): + /@typescript-eslint/eslint-plugin@8.3.0(@typescript-eslint/parser@8.3.0)(eslint@9.9.1)(typescript@5.5.4): + resolution: {integrity: sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.11.0 + '@typescript-eslint/parser': 8.3.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.3.0 + '@typescript-eslint/type-utils': 8.3.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/utils': 8.3.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.3.0 + eslint: 9.9.1 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.3.0(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@6.21.0(eslint@9.9.1)(typescript@5.5.4): resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -2696,7 +2755,28 @@ packages: '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.5.4) '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.3.4 - eslint: 8.57.0 + eslint: 9.9.1 + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@8.3.0(eslint@9.9.1)(typescript@5.5.4): + resolution: {integrity: sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 8.3.0 + '@typescript-eslint/types': 8.3.0 + '@typescript-eslint/typescript-estree': 8.3.0(typescript@5.5.4) + '@typescript-eslint/visitor-keys': 8.3.0 + debug: 4.3.4 + eslint: 9.9.1 typescript: 5.5.4 transitivePeerDependencies: - supports-color @@ -2710,7 +2790,15 @@ packages: '@typescript-eslint/visitor-keys': 6.21.0 dev: true - /@typescript-eslint/type-utils@6.21.0(eslint@8.57.0)(typescript@5.5.4): + /@typescript-eslint/scope-manager@8.3.0: + resolution: {integrity: sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dependencies: + '@typescript-eslint/types': 8.3.0 + '@typescript-eslint/visitor-keys': 8.3.0 + dev: true + + /@typescript-eslint/type-utils@6.21.0(eslint@9.9.1)(typescript@5.5.4): resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -2721,20 +2809,44 @@ packages: optional: true dependencies: '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.5.4) - '@typescript-eslint/utils': 6.21.0(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/utils': 6.21.0(eslint@9.9.1)(typescript@5.5.4) debug: 4.3.4 - eslint: 8.57.0 + eslint: 9.9.1 ts-api-utils: 1.0.3(typescript@5.5.4) typescript: 5.5.4 transitivePeerDependencies: - supports-color dev: true + /@typescript-eslint/type-utils@8.3.0(eslint@9.9.1)(typescript@5.5.4): + resolution: {integrity: sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 8.3.0(typescript@5.5.4) + '@typescript-eslint/utils': 8.3.0(eslint@9.9.1)(typescript@5.5.4) + debug: 4.3.4 + ts-api-utils: 1.3.0(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - eslint + - supports-color + dev: true + /@typescript-eslint/types@6.21.0: resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} engines: {node: ^16.0.0 || >=18.0.0} dev: true + /@typescript-eslint/types@8.3.0: + resolution: {integrity: sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dev: true + /@typescript-eslint/typescript-estree@6.21.0(typescript@5.5.4): resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} engines: {node: ^16.0.0 || >=18.0.0} @@ -2757,25 +2869,63 @@ packages: - supports-color dev: true - /@typescript-eslint/utils@6.21.0(eslint@8.57.0)(typescript@5.5.4): + /@typescript-eslint/typescript-estree@8.3.0(typescript@5.5.4): + resolution: {integrity: sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 8.3.0 + '@typescript-eslint/visitor-keys': 8.3.0 + debug: 4.3.4 + fast-glob: 3.3.2 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.3.0(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils@6.21.0(eslint@9.9.1)(typescript@5.5.4): resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.1) '@types/json-schema': 7.0.13 '@types/semver': 7.5.3 '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.5.4) - eslint: 8.57.0 + eslint: 9.9.1 semver: 7.5.4 transitivePeerDependencies: - supports-color - typescript dev: true + /@typescript-eslint/utils@8.3.0(eslint@9.9.1)(typescript@5.5.4): + resolution: {integrity: sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.1) + '@typescript-eslint/scope-manager': 8.3.0 + '@typescript-eslint/types': 8.3.0 + '@typescript-eslint/typescript-estree': 8.3.0(typescript@5.5.4) + eslint: 9.9.1 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + /@typescript-eslint/visitor-keys@6.21.0: resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} engines: {node: ^16.0.0 || >=18.0.0} @@ -2784,8 +2934,12 @@ packages: eslint-visitor-keys: 3.4.3 dev: true - /@ungap/structured-clone@1.2.0: - resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + /@typescript-eslint/visitor-keys@8.3.0: + resolution: {integrity: sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dependencies: + '@typescript-eslint/types': 8.3.0 + eslint-visitor-keys: 3.4.3 dev: true /accepts@1.3.8: @@ -2796,12 +2950,12 @@ packages: negotiator: 0.6.3 dev: false - /acorn-jsx@5.3.2(acorn@8.10.0): + /acorn-jsx@5.3.2(acorn@8.12.1): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - acorn: 8.10.0 + acorn: 8.12.1 dev: true /acorn-walk@8.2.0: @@ -2815,6 +2969,12 @@ packages: hasBin: true dev: true + /acorn@8.12.1: + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + /agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -3260,6 +3420,11 @@ packages: engines: {node: '>=0.1.90'} dev: true + /comment-parser@1.4.1: + resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==} + engines: {node: '>= 12.0.0'} + dev: true + /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -3475,13 +3640,6 @@ packages: path-type: 4.0.0 dev: true - /doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - dependencies: - esutils: 2.0.3 - dev: true - /dotenv@16.4.5: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} @@ -3600,18 +3758,43 @@ packages: engines: {node: '>=10'} dev: true - /eslint-config-prettier@9.1.0(eslint@8.57.0): + /eslint-config-prettier@9.1.0(eslint@9.9.1): resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} hasBin: true peerDependencies: eslint: '>=7.0.0' dependencies: - eslint: 8.57.0 + eslint: 9.9.1 dev: true - /eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /eslint-plugin-regexp@2.6.0(eslint@9.9.1): + resolution: {integrity: sha512-FCL851+kislsTEQEMioAlpDuK5+E5vs0hi1bF8cFlPlHcEjeRhuAzEsGikXRreE+0j4WhW2uO54MqTjXtYOi3A==} + engines: {node: ^18 || >=20} + peerDependencies: + eslint: '>=8.44.0' + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.1) + '@eslint-community/regexpp': 4.9.1 + comment-parser: 1.4.1 + eslint: 9.9.1 + jsdoc-type-pratt-parser: 4.1.0 + refa: 0.12.1 + regexp-ast-analysis: 0.7.1 + scslre: 0.3.0 + dev: true + + /eslint-plugin-sonarjs@1.0.4(eslint@9.9.1): + resolution: {integrity: sha512-jF0eGCUsq/HzMub4ExAyD8x1oEgjOyB9XVytYGyWgSFvdiJQJp6IuP7RmtauCf06o6N/kZErh+zW4b10y1WZ+Q==} + engines: {node: '>=16'} + peerDependencies: + eslint: ^8.0.0 || ^9.0.0 + dependencies: + eslint: 9.9.1 + dev: true + + /eslint-scope@8.0.2: + resolution: {integrity: sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 @@ -3622,41 +3805,47 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /eslint@8.57.0: - resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /eslint-visitor-keys@4.0.0: + resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dev: true + + /eslint@9.9.1: + resolution: {integrity: sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) - '@eslint-community/regexpp': 4.9.1 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.0 - '@humanwhocodes/config-array': 0.11.14 + '@eslint-community/eslint-utils': 4.4.0(eslint@9.9.1) + '@eslint-community/regexpp': 4.11.0 + '@eslint/config-array': 0.18.0 + '@eslint/eslintrc': 3.1.0 + '@eslint/js': 9.9.1 '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.3.0 '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.2.0 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 debug: 4.3.4 - doctrine: 3.0.0 escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 + eslint-scope: 8.0.2 + eslint-visitor-keys: 4.0.0 + espree: 10.1.0 esquery: 1.5.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 + file-entry-cache: 8.0.0 find-up: 5.0.0 glob-parent: 6.0.2 - globals: 13.23.0 - graphemer: 1.4.0 ignore: 5.2.4 imurmurhash: 0.1.4 is-glob: 4.0.3 is-path-inside: 3.0.3 - js-yaml: 4.1.0 json-stable-stringify-without-jsonify: 1.0.1 levn: 0.4.1 lodash.merge: 4.6.2 @@ -3669,13 +3858,13 @@ packages: - supports-color dev: true - /espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /espree@10.1.0: + resolution: {integrity: sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: - acorn: 8.10.0 - acorn-jsx: 5.3.2(acorn@8.10.0) - eslint-visitor-keys: 3.4.3 + acorn: 8.12.1 + acorn-jsx: 5.3.2(acorn@8.12.1) + eslint-visitor-keys: 4.0.0 dev: true /esprima@4.0.1: @@ -3831,6 +4020,17 @@ packages: micromatch: 4.0.5 dev: true + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + /fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} dev: true @@ -3851,11 +4051,11 @@ packages: bser: 2.1.1 dev: true - /file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} + /file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} dependencies: - flat-cache: 3.1.1 + flat-cache: 4.0.1 dev: true /file-uri-to-path@1.0.0: @@ -3914,13 +4114,12 @@ packages: path-exists: 4.0.0 dev: true - /flat-cache@3.1.1: - resolution: {integrity: sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==} - engines: {node: '>=12.0.0'} + /flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} dependencies: flatted: 3.2.9 keyv: 4.5.4 - rimraf: 3.0.2 dev: true /flatted@3.2.9: @@ -4018,11 +4217,14 @@ packages: engines: {node: '>=4'} dev: true - /globals@13.23.0: - resolution: {integrity: sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==} - engines: {node: '>=8'} - dependencies: - type-fest: 0.20.2 + /globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + dev: true + + /globals@15.9.0: + resolution: {integrity: sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==} + engines: {node: '>=18'} dev: true /globby@11.1.0: @@ -4126,6 +4328,11 @@ packages: engines: {node: '>= 4'} dev: true + /ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + dev: true + /immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} dev: false @@ -4748,6 +4955,11 @@ packages: argparse: 2.0.1 dev: true + /jsdoc-type-pratt-parser@4.1.0: + resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==} + engines: {node: '>=12.0.0'} + dev: true + /jsesc@0.5.0: resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} hasBin: true @@ -5014,6 +5226,13 @@ packages: brace-expansion: 2.0.1 dev: true + /minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} dev: true @@ -5548,6 +5767,13 @@ packages: picomatch: 2.3.1 dev: true + /refa@0.12.1: + resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dependencies: + '@eslint-community/regexpp': 4.9.1 + dev: true + /regenerate-unicode-properties@10.1.1: resolution: {integrity: sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==} engines: {node: '>=4'} @@ -5569,6 +5795,14 @@ packages: '@babel/runtime': 7.23.1 dev: true + /regexp-ast-analysis@0.7.1: + resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dependencies: + '@eslint-community/regexpp': 4.9.1 + refa: 0.12.1 + dev: true + /regexpu-core@5.3.2: resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} engines: {node: '>=4'} @@ -5649,14 +5883,6 @@ packages: glob: 7.2.3 dev: true - /rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - dependencies: - glob: 7.2.3 - dev: true - /rollup@3.29.4: resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} @@ -5682,6 +5908,15 @@ packages: /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + /scslre@0.3.0: + resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} + engines: {node: ^14.0.0 || >=16.0.0} + dependencies: + '@eslint-community/regexpp': 4.9.1 + refa: 0.12.1 + regexp-ast-analysis: 0.7.1 + dev: true + /semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -5975,6 +6210,15 @@ packages: typescript: 5.5.4 dev: true + /ts-api-utils@1.3.0(typescript@5.5.4): + resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 5.5.4 + dev: true + /ts-jest@29.2.5(@babel/core@7.25.2)(jest@29.7.0)(typescript@5.5.4): resolution: {integrity: sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} @@ -6118,11 +6362,6 @@ packages: engines: {node: '>=4'} dev: true - /type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - dev: true - /type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} @@ -6136,6 +6375,24 @@ packages: mime-types: 2.1.35 dev: false + /typescript-eslint@8.3.0(eslint@9.9.1)(typescript@5.5.4): + resolution: {integrity: sha512-EvWjwWLwwKDIJuBjk2I6UkV8KEQcwZ0VM10nR1rIunRDIP67QJTZAHBXTX0HW/oI1H10YESF8yWie8fRQxjvFA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/eslint-plugin': 8.3.0(@typescript-eslint/parser@8.3.0)(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/parser': 8.3.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/utils': 8.3.0(eslint@9.9.1)(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - eslint + - supports-color + dev: true + /typescript@5.5.4: resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} engines: {node: '>=14.17'} diff --git a/tsconfig.dim-api-types.json b/tsconfig.dim-api-types.json index 00d6bea..3fdb581 100644 --- a/tsconfig.dim-api-types.json +++ b/tsconfig.dim-api-types.json @@ -1,11 +1,18 @@ { "compilerOptions": { "outDir": "./dim-api-types", + "strict": true, "sourceMap": false, "strictNullChecks": true, - "module": "commonjs", + "module": "CommonJS", "target": "es5", - "moduleResolution": "node", + "moduleResolution": "NodeNext", + "noUnusedLocals": true, + "noUnusedParameters": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "useUnknownInCatchVariables": true, "declaration": true, "emitDeclarationOnly": true, "esModuleInterop": true diff --git a/tsconfig.json b/tsconfig.json index 8ed5861..3daffc9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,16 @@ { "compilerOptions": { + "strict": true, "esModuleInterop": true, "sourceMap": true, - "strictNullChecks": true, - "strictBindCallApply": true, "module": "NodeNext", - "target": "es2022", + "target": "ESNext", "moduleResolution": "NodeNext", "noUnusedLocals": true, "noUnusedParameters": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, - "skipLibCheck": true + "skipLibCheck": true, + "useUnknownInCatchVariables": true } }