diff --git a/api/apps/index.ts b/api/apps/index.ts index 633e7fb..701dcb2 100644 --- a/api/apps/index.ts +++ b/api/apps/index.ts @@ -1,7 +1,7 @@ import * as Sentry from '@sentry/node'; import { ListToken } from '@stately-cloud/client'; +import { keyBy } from 'es-toolkit'; import { RequestHandler } from 'express'; -import _ from 'lodash'; import { getAllApps as getAllAppsPostgres } from '../db/apps-queries.js'; import { pool } from '../db/index.js'; import { metrics } from '../metrics/index.js'; @@ -114,7 +114,7 @@ async function fetchAppsFromPostgres() { const client = await pool.connect(); try { apps = await getAllAppsPostgres(client); - appsByApiKey = _.keyBy(apps, (a) => a.dimApiKey.toLowerCase()); + appsByApiKey = keyBy(apps, (a) => a.dimApiKey.toLowerCase()); origins = new Set(); for (const app of apps) { origins.add(app.origin); @@ -126,7 +126,7 @@ async function fetchAppsFromPostgres() { } function digestApps() { - appsByApiKey = _.keyBy(apps, (a) => a.dimApiKey.toLowerCase()); + appsByApiKey = keyBy(apps, (a) => a.dimApiKey.toLowerCase()); origins = new Set(); for (const app of apps) { origins.add(app.origin); diff --git a/api/db/searches-queries.ts b/api/db/searches-queries.ts index a2bcd68..b2ac1b4 100644 --- a/api/db/searches-queries.ts +++ b/api/db/searches-queries.ts @@ -1,4 +1,4 @@ -import _ from 'lodash'; +import { uniqBy } from 'es-toolkit'; import { ClientBase, QueryResult } from 'pg'; import { metrics } from '../metrics/index.js'; import { ExportResponse } from '../shapes/export.js'; @@ -58,7 +58,7 @@ export async function getSearchesForProfile( 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( + return uniqBy( results.rows .map(convertSearch) .concat(destinyVersion === 2 ? cannedSearchesForD2 : cannedSearchesForD1), diff --git a/api/routes/auth-token.ts b/api/routes/auth-token.ts index 4aadfa1..13732df 100644 --- a/api/routes/auth-token.ts +++ b/api/routes/auth-token.ts @@ -4,8 +4,8 @@ import asyncHandler from 'express-async-handler'; import util from 'util'; import { AuthTokenRequest, AuthTokenResponse } from '../shapes/auth.js'; +import { sortBy } from 'es-toolkit/compat'; import jwt, { type Secret, type SignOptions } from 'jsonwebtoken'; -import _ from 'lodash'; import { metrics } from '../metrics/index.js'; import { ApiApp } from '../shapes/app.js'; import { badRequest } from '../utils.js'; @@ -66,7 +66,7 @@ export const authTokenHandler = asyncHandler(async (req, res) => { const serverMembershipId = responseData.Response.bungieNetUser.membershipId; if (serverMembershipId === membershipId) { const primaryMembershipId = responseData.Response.primaryMembershipId; - const profileIds = _.sortBy( + const profileIds = sortBy( responseData.Response.destinyMemberships // Filter out accounts that are tied to another platform's cross-save account. // .filter((m) => !m.crossSaveOverride || m.crossSaveOverride === m.membershipType) diff --git a/api/routes/import.ts b/api/routes/import.ts index f05857a..885794f 100644 --- a/api/routes/import.ts +++ b/api/routes/import.ts @@ -1,5 +1,5 @@ +import { isEmpty } from 'es-toolkit/compat'; import asyncHandler from 'express-async-handler'; -import _ from 'lodash'; import { readTransaction, transaction } from '../db/index.js'; import { updateItemAnnotation } from '../db/item-annotations-queries.js'; import { updateItemHashTag } from '../db/item-hash-tags-queries.js'; @@ -46,7 +46,7 @@ export const importHandler = asyncHandler(async (req, res) => { extractImportData(importData); if ( - _.isEmpty(settings) && + isEmpty(settings) && loadouts.length === 0 && itemAnnotations.length === 0 && triumphs.length === 0 && diff --git a/api/routes/update.ts b/api/routes/update.ts index 471259f..f16da01 100644 --- a/api/routes/update.ts +++ b/api/routes/update.ts @@ -1,7 +1,7 @@ import { captureMessage } from '@sentry/node'; +import { isEmpty } from 'es-toolkit/compat'; import express from 'express'; import asyncHandler from 'express-async-handler'; -import _ from 'lodash'; import { ClientBase } from 'pg'; import { readTransaction, transaction } from '../db/index.js'; import { @@ -129,7 +129,7 @@ export const updateHandler = asyncHandler(async (req, res) => { extractImportData(exportResponse); if ( - _.isEmpty(settings) && + isEmpty(settings) && loadouts.length === 0 && itemAnnotations.length === 0 && triumphs.length === 0 && diff --git a/api/stately/loadouts-queries.test.ts b/api/stately/loadouts-queries.test.ts index 7849ac4..d2d143a 100644 --- a/api/stately/loadouts-queries.test.ts +++ b/api/stately/loadouts-queries.test.ts @@ -1,5 +1,5 @@ import { toBinary } from '@bufbuild/protobuf'; -import _ from 'lodash'; +import { omit } from 'es-toolkit'; import { v4 as uuid } from 'uuid'; import { defaultLoadoutParameters, Loadout, LoadoutParameters } from '../shapes/loadouts.js'; import { client } from './client.js'; @@ -48,9 +48,14 @@ it('can roundtrip between DIM loadout and Stately loadout', () => { const statelyLoadout = convertLoadoutToStately(loadout, platformMembershipId, 2); expect(() => toBinary(LoadoutSchema, statelyLoadout)).not.toThrow(); const loadout2 = convertLoadoutFromStately(statelyLoadout); - expect(_.omit(loadout2, 'profileId', 'destinyVersion', 'createdAt', 'lastUpdatedAt')).toEqual( - loadout, - ); + expect( + omit(loadout2, [ + 'profileId' as keyof Loadout, + 'destinyVersion' as keyof Loadout, + 'createdAt', + 'lastUpdatedAt', + ]), + ).toEqual(loadout); }); it('can roundtrip loadout parameters', () => { diff --git a/api/stately/loadouts-queries.ts b/api/stately/loadouts-queries.ts index ef37dee..7dd9ed9 100644 --- a/api/stately/loadouts-queries.ts +++ b/api/stately/loadouts-queries.ts @@ -1,7 +1,7 @@ import { MessageInitShape } from '@bufbuild/protobuf'; import { keyPath } from '@stately-cloud/client'; import { DestinyClass } from 'bungie-api-ts/destiny2'; -import _ from 'lodash'; +import { isEmpty } from 'es-toolkit/compat'; import { DestinyVersion } from '../shapes/general.js'; import { AssumeArmorMasterwork, @@ -146,7 +146,7 @@ export function convertLoadoutParametersFromStately( // DIM's AssumArmorMasterwork enum starts at 1 assumeArmorMasterwork: (assumeArmorMasterwork ?? 0) + 1, statConstraints: statConstraintsFromStately(statConstraints), - modsByBucket: _.isEmpty(modsByBucket) + modsByBucket: isEmpty(modsByBucket) ? undefined : listToMap('bucketHash', 'modHashes', modsByBucket), artifactUnlocks: artifactUnlocks ? stripTypeName(artifactUnlocks) : undefined, @@ -205,7 +205,7 @@ function convertLoadoutItemFromStately(item: StatelyLoadoutItem): LoadoutItem { if (item.id) { result.id = item.id.toString(); } - if (!_.isEmpty(item.socketOverrides)) { + if (!isEmpty(item.socketOverrides)) { result.socketOverrides = listToMap('socketIndex', 'itemHash', item.socketOverrides); } if (item.craftedDate) { @@ -297,7 +297,7 @@ export function convertLoadoutParametersToStately( loParameters: LoadoutParameters | undefined, ): MessageInitShape | undefined { let loParametersFixed: MessageInitShape | undefined; - if (!_.isEmpty(loParameters)) { + if (!isEmpty(loParameters)) { const { assumeArmorMasterwork, exoticArmorHash, diff --git a/api/stately/searches-queries.ts b/api/stately/searches-queries.ts index 01b2e31..c0593bc 100644 --- a/api/stately/searches-queries.ts +++ b/api/stately/searches-queries.ts @@ -1,5 +1,5 @@ import { keyPath } from '@stately-cloud/client'; -import _ from 'lodash'; +import { sortBy, uniqBy } from 'es-toolkit'; import crypto from 'node:crypto'; import { metrics } from '../metrics/index.js'; import { ExportResponse } from '../shapes/export.js'; @@ -79,8 +79,8 @@ export async function getSearchesForProfile( results.push(...(destinyVersion === 2 ? cannedSearchesForD2 : cannedSearchesForD1)); - return _.sortBy( - _.uniqBy(results, (s) => s.query), + return sortBy( + uniqBy(results, (s) => s.query), [(s) => -s.lastUsage, (s) => s.usageCount], ); } diff --git a/api/stately/settings-queries.test.ts b/api/stately/settings-queries.test.ts index 1cae8f7..ec4e415 100644 --- a/api/stately/settings-queries.test.ts +++ b/api/stately/settings-queries.test.ts @@ -1,4 +1,4 @@ -import _ from 'lodash'; +import { omit } from 'es-toolkit'; import { defaultLoadoutParameters } from '../shapes/loadouts.js'; import { defaultSettings, Settings } from '../shapes/settings.js'; import { @@ -14,14 +14,14 @@ it('can roundtrip between DIM settings and Stately settings', () => { const settings: Settings = defaultSettings; const statelySettings = convertToStatelyItem(settings, 1234); const settings2 = convertToDimSettings(statelySettings); - expect(_.omit(settings2, 'memberId')).toEqual(settings); + expect(omit(settings2, ['memberId' as keyof Settings])).toEqual(settings); }); it('can roundtrip between DIM settings and Stately settings with loadout parameters', () => { const settings: Settings = { ...defaultSettings, loParameters: defaultLoadoutParameters }; const statelySettings = convertToStatelyItem(settings, 1234); const settings2 = convertToDimSettings(statelySettings); - expect(_.omit(settings2, 'memberId')).toEqual(settings); + expect(omit(settings2, ['memberId' as keyof Settings])).toEqual(settings); }); it('can insert settings where none exist before', async () => { diff --git a/api/stately/stately-utils.ts b/api/stately/stately-utils.ts index 667d464..1393371 100644 --- a/api/stately/stately-utils.ts +++ b/api/stately/stately-utils.ts @@ -1,6 +1,6 @@ // Utilities for dealing with Stately Items (protobufs) and other Stately-specific utilities. -import _ from 'lodash'; +import { mapValues } from 'es-toolkit'; /** Recursively convert bigints to regular numbers in an object. */ type ObjectBigIntToNumber = { @@ -25,8 +25,8 @@ export function bigIntToNumber(value: T): BigIntToNumber { return Number(value) as BigIntToNumber; } else if (Array.isArray(value)) { return value.map(bigIntToNumber) as BigIntToNumber; - } else if (typeof value === 'object') { - return _.mapValues(value, bigIntToNumber) as BigIntToNumber; + } else if (typeof value === 'object' && value !== null) { + return mapValues(value, bigIntToNumber) as BigIntToNumber; } return value as BigIntToNumber; } @@ -51,8 +51,8 @@ export function numberToBigInt(value: T): NumberToBigInt { return BigInt(value) as NumberToBigInt; } else if (Array.isArray(value)) { return value.map(numberToBigInt) as NumberToBigInt; - } else if (typeof value === 'object') { - return _.mapValues(value, numberToBigInt) as NumberToBigInt; + } else if (typeof value === 'object' && value !== null) { + return mapValues(value, numberToBigInt) as NumberToBigInt; } return value as NumberToBigInt; } diff --git a/api/utils.ts b/api/utils.ts index a858e13..cb76cd2 100644 --- a/api/utils.ts +++ b/api/utils.ts @@ -1,5 +1,5 @@ +import { camelCase, mapKeys } from 'es-toolkit'; import { Response } from 'express'; -import _ from 'lodash'; /** * This is a utility function to extract the types of a subset of value types @@ -95,7 +95,7 @@ export type KeysToSnakeCase = { * 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; + return mapKeys(data, (_value, key) => camelCase(key as string)) as T; } export function badRequest(res: Response, message: string) { diff --git a/package.json b/package.json index 00cd2c5..330fd99 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "@types/express": "^4.17.21", "@types/jest": "^29.5.14", "@types/jsonwebtoken": "^9.0.7", - "@types/lodash": "^4.17.13", "@types/morgan": "^1.9.9", "@types/pg": "^8.11.10", "@types/uuid": "^10.0.0", @@ -76,6 +75,7 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "ejs": "^3.1.10", + "es-toolkit": "^1.29.0", "express": "^4.21.1", "express-async-handler": "^1.2.0", "express-hot-shots": "^1.0.2", @@ -83,7 +83,6 @@ "hi-base32": "^0.5.1", "hot-shots": "^10.2.1", "jsonwebtoken": "^9.0.2", - "lodash": "^4.17.21", "morgan": "^1.10.0", "pg": "^8.13.1", "pg-protocol": "^1.7.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac2b534..42ffb0a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ dependencies: ejs: specifier: ^3.1.10 version: 3.1.10 + es-toolkit: + specifier: ^1.29.0 + version: 1.29.0 express: specifier: ^4.21.1 version: 4.21.1 @@ -56,9 +59,6 @@ dependencies: jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 - lodash: - specifier: ^4.17.21 - version: 4.17.21 morgan: specifier: ^1.10.0 version: 1.10.0 @@ -124,9 +124,6 @@ devDependencies: '@types/jsonwebtoken': specifier: ^9.0.7 version: 9.0.7 - '@types/lodash': - specifier: ^4.17.13 - version: 4.17.13 '@types/morgan': specifier: ^1.9.9 version: 1.9.9 @@ -2821,10 +2818,6 @@ packages: dependencies: '@types/node': 22.9.0 - /@types/lodash@4.17.13: - resolution: {integrity: sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==} - dev: true - /@types/long@4.0.2: resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} dev: false @@ -3905,6 +3898,10 @@ packages: engines: {node: '>= 0.4'} dev: false + /es-toolkit@1.29.0: + resolution: {integrity: sha512-GjTll+E6APcfAQA09D89HdT8Qn2Yb+TeDSDBTMcxAo+V+w1amAtCI15LJu4YPH/UCPoSo/F47Gr1LIM0TE0lZA==} + dev: false + /esbuild@0.23.1: resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} engines: {node: '>=18'} @@ -5467,6 +5464,7 @@ packages: /lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: true /long@5.2.3: resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==}