From efcf04487dd0c3185321087cc6721195e2308ba9 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Wed, 19 Mar 2025 10:01:49 +0100 Subject: [PATCH] chore: make it build with strict null checks set to true (#9554) As part of preparation for ESM and node/TSC updates, this PR will make Unleash build with strictNullChecks set to true, since that's what's in our tsconfig file. Hence, this PR also removes the `--strictNullChecks false` flag in our compile tasks in package.json. TL;DR - Clean up your code rather than turning off compiler security features :) --- package.json | 12 ++-- src/lib/addons/datadog.ts | 2 +- src/lib/addons/new-relic.ts | 2 +- src/lib/addons/slack-app.ts | 2 +- src/lib/addons/slack.ts | 2 +- src/lib/addons/teams.ts | 2 +- src/lib/addons/webhook.ts | 2 +- src/lib/app.test.ts | 7 +- src/lib/app.ts | 1 + src/lib/create-config.ts | 29 ++++---- src/lib/db/client-instance-store.ts | 5 +- src/lib/db/public-signup-token-store.ts | 1 + src/lib/db/user-feedback-store.ts | 4 +- src/lib/db/user-splash-store.ts | 4 +- .../domain/project-health/project-health.ts | 5 +- .../fakes/fake-client-feature-toggle-store.ts | 2 +- .../features/context/context-field-store.ts | 6 +- src/lib/features/context/context-service.ts | 25 ++++++- .../export-import-permissions.e2e.test.ts | 2 +- .../export-import-service.ts | 2 +- .../import-permissions-service.ts | 14 ++-- .../feature-search/feature-search-store.ts | 16 +++-- .../archive-feature-toggle-controller.ts | 14 +++- .../feature-toggle-row-converter.ts | 4 +- .../fakes/fake-feature-strategies-store.ts | 20 +++--- .../fakes/fake-feature-toggle-store.ts | 15 +++-- .../feature-toggle-controller.ts | 17 +++-- .../feature-toggle/feature-toggle-service.ts | 67 ++++++++++++++++--- .../feature-toggle/feature-toggle-store.ts | 26 +++---- .../types/feature-toggle-store-type.ts | 2 +- .../features/frontend-api/create-context.ts | 2 +- .../frontend-api/frontend-api-service.ts | 13 ++-- .../frontend-api/global-frontend-api-cache.ts | 5 +- .../client-metrics-service.e2e.test.ts | 15 +++-- .../client-metrics-store-v2-type.ts | 2 +- .../client-metrics-store-v2.e2e.test.ts | 4 +- .../metrics/instance/instance-service.ts | 7 +- .../strategy/remote-address-strategy.ts | 1 + .../strategy/user-with-id-strategy.ts | 4 +- .../playground/offline-unleash-client.test.ts | 1 + .../environment-service.ts | 10 ++- .../fake-environment-store.ts | 10 +-- .../project-insights-service.ts | 2 +- .../project/project-service.e2e.test.ts | 2 +- src/lib/features/project/project-service.ts | 24 +++++-- .../project/project-store.e2e.test.ts | 16 ++--- .../segment/client-segment.e2e.test.ts | 4 +- src/lib/features/segment/segment-service.ts | 18 ++++- .../features/tag-type/fake-tag-type-store.ts | 2 +- src/lib/features/tag-type/tag-type-service.ts | 7 +- .../traffic-data-usage-store.test.ts | 4 +- src/lib/metrics.ts | 6 +- src/lib/middleware/content_type_checker.ts | 5 +- src/lib/middleware/rbac-middleware.ts | 7 +- src/lib/openapi/validate.ts | 1 + src/lib/routes/admin-api/strategy.ts | 2 +- src/lib/routes/admin-api/telemetry.ts | 3 - src/lib/routes/admin-api/user-feedback.ts | 2 +- src/lib/services/access-service.ts | 12 ++++ src/lib/services/account-service.ts | 8 ++- src/lib/services/addon-service.ts | 30 ++++++--- src/lib/services/api-token-service.ts | 6 +- src/lib/services/favorites-service.ts | 14 +++- src/lib/services/feature-tag-service.ts | 7 ++ src/lib/services/group-service.ts | 10 ++- .../services/public-signup-token-service.ts | 12 +++- src/lib/services/session-service.ts | 2 +- src/lib/services/strategy-schema.ts | 4 +- src/lib/services/strategy-service.ts | 22 ++---- src/lib/services/tag-schema.test.ts | 3 + src/lib/services/tag-type-schema.test.ts | 6 +- src/lib/services/user-service.test.ts | 4 -- src/lib/services/user-service.ts | 15 +++-- src/lib/types/group.ts | 9 +-- src/lib/types/model.ts | 4 +- src/lib/types/stores/account-store.ts | 2 +- src/lib/types/stores/api-token-store.ts | 2 +- src/lib/types/stores/favorite-features.ts | 2 +- src/lib/types/stores/favorite-projects.ts | 2 +- src/lib/types/stores/reset-token-store.ts | 2 +- .../fakes/fake-inactive-users-store.ts | 2 +- src/lib/util/anyEventEmitter.test.ts | 4 +- src/lib/util/collect-ids.ts | 2 +- src/lib/util/extract-user.ts | 6 +- src/lib/util/snakeCase.ts | 2 +- src/lib/util/time-utils.ts | 4 +- src/lib/util/validateOrigin.ts | 7 +- src/lib/util/version.ts | 2 - src/test/config/create-config.test.ts | 4 +- .../api/admin/project/api-token.e2e.test.ts | 2 +- src/test/e2e/api/openapi/openapi.e2e.test.ts | 7 +- src/test/e2e/helpers/test-helper.ts | 4 +- src/test/e2e/seed/segment.seed.ts | 2 +- .../e2e/services/playground-service.test.ts | 16 +++-- .../services/reset-token-service.e2e.test.ts | 12 ++-- .../e2e/stores/api-token-store.e2e.test.ts | 8 +-- .../client-application-store.e2e.test.ts | 8 +-- .../feature-environment-store.e2e.test.ts | 2 +- .../e2e/stores/feature-tag-store.e2e.test.ts | 4 +- .../e2e/stores/feature-type-store.e2e.test.ts | 4 +- src/test/e2e/stores/user-store.e2e.test.ts | 7 +- src/test/fixtures/access-service-mock.ts | 4 ++ src/test/fixtures/fake-access-store.ts | 2 +- src/test/fixtures/fake-account-store.ts | 6 +- src/test/fixtures/fake-api-token-store.ts | 5 +- .../fake-client-applications-store.ts | 2 +- .../fixtures/fake-favorite-features-store.ts | 4 +- .../fixtures/fake-favorite-projects-store.ts | 4 +- src/test/fixtures/fake-feature-tag-store.ts | 4 +- src/test/fixtures/fake-group-store.ts | 2 +- src/test/fixtures/fake-project-store.ts | 12 ++-- src/test/fixtures/fake-public-signup-store.ts | 14 +++- src/test/fixtures/fake-reset-token-store.ts | 12 ++-- src/test/fixtures/fake-session-store.ts | 4 +- src/test/fixtures/fake-user-feedback-store.ts | 7 +- src/test/fixtures/fake-user-splash-store.ts | 7 +- src/test/fixtures/no-logger.ts | 2 +- yarn.lock | 18 ++--- 118 files changed, 577 insertions(+), 310 deletions(-) diff --git a/package.json b/package.json index f7eaccda95..dd1940ed96 100644 --- a/package.json +++ b/package.json @@ -35,21 +35,21 @@ "scripts": { "start": "TZ=UTC node ./dist/server.js", "copy-templates": "copyfiles -u 1 src/mailtemplates/**/* dist/", - "build:backend": "tsc --pretty --strictNullChecks false", + "build:backend": "tsc --pretty", "build:frontend": "yarn --cwd ./frontend run build", "build:frontend:if-needed": "./scripts/build-frontend-if-needed.sh", "build": "yarn run clean && concurrently \"yarn:copy-templates\" \"yarn:build:frontend\" \"yarn:build:backend\"", - "dev:backend": "TZ=UTC NODE_ENV=development tsc-watch --strictNullChecks false --onSuccess \"node dist/server-dev.js\"", + "dev:backend": "TZ=UTC NODE_ENV=development tsc-watch --onSuccess \"node dist/server-dev.js\"", "dev:frontend": "wait-on tcp:4242 && yarn --cwd ./frontend run dev", "dev:frontend:cloud": "UNLEASH_BASE_PATH=/demo/ yarn run dev:frontend", "dev": "concurrently \"yarn:dev:backend\" \"yarn:dev:frontend\"", "prepare:backend": "concurrently \"yarn:copy-templates\" \"yarn:build:backend\"", - "start:dev": "yarn run clean && TZ=UTC NODE_ENV=development tsc-watch --strictNullChecks false --onSuccess \"node dist/server-dev.js\"", + "start:dev": "yarn run clean && TZ=UTC NODE_ENV=development tsc-watch --onSuccess \"node dist/server-dev.js\"", "db-migrate": "db-migrate --migrations-dir ./src/migrations", "lint": "biome check .", "lint:fix": "biome check . --write", "local:package": "del-cli --force build && mkdir build && cp -r dist docs CHANGELOG.md LICENSE README.md package.json build", - "build:watch": "yarn run clean && tsc -w --strictNullChecks false", + "build:watch": "tsc -w", "prepare": "husky && yarn --cwd ./frontend install && if [ ! -d ./dist ]; then yarn build; fi", "test": "NODE_ENV=test PORT=4243 node --trace-warnings node_modules/.bin/jest", "test:unit": "NODE_ENV=test PORT=4243 jest --testPathIgnorePatterns=src/test/e2e --testPathIgnorePatterns=dist", @@ -60,7 +60,7 @@ "test:coverage": "NODE_ENV=test PORT=4243 jest --coverage --testLocationInResults --outputFile=\"coverage/report.json\" --forceExit", "test:coverage:jest": "NODE_ENV=test PORT=4243 jest --silent --ci --json --coverage --testLocationInResults --outputFile=\"report.json\" --forceExit", "test:updateSnapshot": "NODE_ENV=test PORT=4243 jest --updateSnapshot", - "seed:setup": "ts-node --compilerOptions '{\"strictNullChecks\": false}' src/test/e2e/seed/segment.seed.ts", + "seed:setup": "ts-node src/test/e2e/seed/segment.seed.ts", "seed:serve": "UNLEASH_DATABASE_NAME=unleash_test UNLEASH_DATABASE_SCHEMA=seed yarn run start:dev", "clean": "del-cli --force dist", "heroku-postbuild": "cd frontend && yarn && yarn build", @@ -220,7 +220,7 @@ "supertest": "7.0.0", "ts-node": "10.9.2", "tsc-watch": "6.2.1", - "typescript": "5.4.5", + "typescript": "5.8.2", "wait-on": "^7.2.0" }, "resolutions": { diff --git a/src/lib/addons/datadog.ts b/src/lib/addons/datadog.ts index 57b2666b2d..afa76a2e43 100644 --- a/src/lib/addons/datadog.ts +++ b/src/lib/addons/datadog.ts @@ -32,7 +32,7 @@ interface DDRequestBody { export default class DatadogAddon extends Addon { private msgFormatter: FeatureEventFormatter; - flagResolver: IFlagResolver; + declare flagResolver: IFlagResolver; constructor(config: IAddonConfig) { super(definition, config); diff --git a/src/lib/addons/new-relic.ts b/src/lib/addons/new-relic.ts index 458a0de039..dc20624cdd 100644 --- a/src/lib/addons/new-relic.ts +++ b/src/lib/addons/new-relic.ts @@ -39,7 +39,7 @@ interface INewRelicRequestBody { export default class NewRelicAddon extends Addon { private msgFormatter: FeatureEventFormatter; - flagResolver: IFlagResolver; + declare flagResolver: IFlagResolver; constructor(config: IAddonConfig) { super(definition, config); diff --git a/src/lib/addons/slack-app.ts b/src/lib/addons/slack-app.ts index a3f3cccdec..32b3c35517 100644 --- a/src/lib/addons/slack-app.ts +++ b/src/lib/addons/slack-app.ts @@ -34,7 +34,7 @@ interface ISlackAppAddonParameters { export default class SlackAppAddon extends Addon { private msgFormatter: FeatureEventFormatter; - flagResolver: IFlagResolver; + declare flagResolver: IFlagResolver; private accessToken?: string; diff --git a/src/lib/addons/slack.ts b/src/lib/addons/slack.ts index a9b1e18c92..dff056b55d 100644 --- a/src/lib/addons/slack.ts +++ b/src/lib/addons/slack.ts @@ -25,7 +25,7 @@ interface ISlackAddonParameters { export default class SlackAddon extends Addon { private msgFormatter: FeatureEventFormatter; - flagResolver: IFlagResolver; + declare flagResolver: IFlagResolver; constructor(args: IAddonConfig) { super(slackDefinition, args); diff --git a/src/lib/addons/teams.ts b/src/lib/addons/teams.ts index 726d166f76..c1ae709867 100644 --- a/src/lib/addons/teams.ts +++ b/src/lib/addons/teams.ts @@ -38,7 +38,7 @@ interface ITeamsParameters { export default class TeamsAddon extends Addon { private msgFormatter: FeatureEventFormatter; - flagResolver: IFlagResolver; + declare flagResolver: IFlagResolver; constructor(args: IAddonConfig) { if (args.flagResolver.isEnabled('teamsIntegrationChangeRequests')) { diff --git a/src/lib/addons/webhook.ts b/src/lib/addons/webhook.ts index 99d226d262..39aba8c951 100644 --- a/src/lib/addons/webhook.ts +++ b/src/lib/addons/webhook.ts @@ -25,7 +25,7 @@ interface IParameters { export default class Webhook extends Addon { private msgFormatter: FeatureEventFormatter; - flagResolver: IFlagResolver; + declare flagResolver: IFlagResolver; constructor(args: IAddonConfig) { super(definition, args); diff --git a/src/lib/app.test.ts b/src/lib/app.test.ts index 9d27f5fd66..735e9cc27f 100644 --- a/src/lib/app.test.ts +++ b/src/lib/app.test.ts @@ -18,10 +18,10 @@ jest.mock( }, ); -const getApp = require('./app').default; - +import getApp from './app'; test('should not throw when valid config', async () => { const config = createTestConfig(); + // @ts-expect-error - We're passing in empty stores and services const app = await getApp(config, {}, {}); expect(typeof app.listen).toBe('function'); }); @@ -33,6 +33,7 @@ test('should call preHook', async () => { called++; }, }); + // @ts-expect-error - We're passing in empty stores and services await getApp(config, {}, {}); expect(called).toBe(1); }); @@ -44,6 +45,7 @@ test('should call preRouterHook', async () => { called++; }, }); + // @ts-expect-error - We're passing in empty stores and services await getApp(config, {}, {}); expect(called).toBe(1); }); @@ -82,6 +84,7 @@ describe('compression middleware', () => { disableCompression: disableCompression as any, }, }); + // @ts-expect-error - We're passing in empty stores and services await getApp(config, {}, {}); expect(compression).toBeCalledTimes(+expectCompressionEnabled); }, diff --git a/src/lib/app.ts b/src/lib/app.ts index 2d407d7405..7888fb39ee 100644 --- a/src/lib/app.ts +++ b/src/lib/app.ts @@ -195,6 +195,7 @@ export default async function getApp( } // Setup API routes + // @ts-expect-error - our db is possibly undefined and our indexrouter doesn't currently handle that app.use(`${baseUriPath}/`, new IndexRouter(config, services, db).router); if (process.env.NODE_ENV !== 'production') { diff --git a/src/lib/create-config.ts b/src/lib/create-config.ts index 89743f784b..01d928833f 100644 --- a/src/lib/create-config.ts +++ b/src/lib/create-config.ts @@ -335,17 +335,22 @@ const parseEnvVarInitialAdminUser = (): UsernameAdminUser | undefined => { return username && password ? { username, password } : undefined; }; -const defaultAuthentication: IAuthOption = { - demoAllowAdminLogin: parseEnvVarBoolean( - process.env.AUTH_DEMO_ALLOW_ADMIN_LOGIN, - false, - ), - enableApiToken: parseEnvVarBoolean(process.env.AUTH_ENABLE_API_TOKEN, true), - type: authTypeFromString(process.env.AUTH_TYPE), - customAuthHandler: defaultCustomAuthDenyAll, - createAdminUser: true, - initialAdminUser: parseEnvVarInitialAdminUser(), - initApiTokens: [], +const buildDefaultAuthOption = () => { + return { + demoAllowAdminLogin: parseEnvVarBoolean( + process.env.AUTH_DEMO_ALLOW_ADMIN_LOGIN, + false, + ), + enableApiToken: parseEnvVarBoolean( + process.env.AUTH_ENABLE_API_TOKEN, + true, + ), + type: authTypeFromString(process.env.AUTH_TYPE), + customAuthHandler: defaultCustomAuthDenyAll, + createAdminUser: true, + initialAdminUser: parseEnvVarInitialAdminUser(), + initApiTokens: [], + }; }; const defaultImport: WithOptional = { @@ -563,7 +568,7 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig { const initApiTokens = loadInitApiTokens(); const authentication: IAuthOption = mergeAll([ - defaultAuthentication, + buildDefaultAuthOption(), (options.authentication ? removeUndefinedKeys(options.authentication) : options.authentication) || {}, diff --git a/src/lib/db/client-instance-store.ts b/src/lib/db/client-instance-store.ts index b2c76cf968..06c2d52ba4 100644 --- a/src/lib/db/client-instance-store.ts +++ b/src/lib/db/client-instance-store.ts @@ -7,9 +7,8 @@ import type { } from '../types/stores/client-instance-store'; import { subDays } from 'date-fns'; import type { Db } from './db'; - -const metricsHelper = require('../util/metrics-helper'); -const { DB_TIME } = require('../metric-events'); +import metricsHelper from '../util/metrics-helper'; +import { DB_TIME } from '../metric-events'; const COLUMNS = [ 'app_name', diff --git a/src/lib/db/public-signup-token-store.ts b/src/lib/db/public-signup-token-store.ts index dc04f1127d..b9ae7dd85a 100644 --- a/src/lib/db/public-signup-token-store.ts +++ b/src/lib/db/public-signup-token-store.ts @@ -153,6 +153,7 @@ export class PublicSignupTokenStore implements IPublicSignupTokenStore { newToken: IPublicSignupTokenCreate, ): Promise { const response = await this.db(TABLE).insert( + // @ts-expect-error - knex expects us to return a DbRecordArr, we return OurType, which works fine. toRow(newToken), ['secret'], ); diff --git a/src/lib/db/user-feedback-store.ts b/src/lib/db/user-feedback-store.ts index a929762462..4d53643601 100644 --- a/src/lib/db/user-feedback-store.ts +++ b/src/lib/db/user-feedback-store.ts @@ -25,7 +25,7 @@ const fieldToRow = (fields: IUserFeedback): IUserFeedbackTable => ({ }); const rowToField = (row: IUserFeedbackTable): IUserFeedback => ({ - neverShow: row.nevershow, + neverShow: row.nevershow || false, feedbackId: row.feedback_id, given: row.given, userId: row.user_id, @@ -71,7 +71,7 @@ export default class UserFeedbackStore implements IUserFeedbackStore { .merge() .returning(COLUMNS); - return rowToField(insertedFeedback[0]); + return rowToField(insertedFeedback[0] as IUserFeedbackTable); } async delete({ userId, feedbackId }: IUserFeedbackKey): Promise { diff --git a/src/lib/db/user-splash-store.ts b/src/lib/db/user-splash-store.ts index d1ee22f47a..ae8a0dc8f4 100644 --- a/src/lib/db/user-splash-store.ts +++ b/src/lib/db/user-splash-store.ts @@ -23,7 +23,7 @@ const fieldToRow = (fields: IUserSplash): IUserSplashTable => ({ }); const rowToField = (row: IUserSplashTable): IUserSplash => ({ - seen: row.seen, + seen: row.seen || false, splashId: row.splash_id, userId: row.user_id, }); @@ -65,7 +65,7 @@ export default class UserSplashStore implements IUserSplashStore { .merge() .returning(COLUMNS); - return rowToField(insertedSplash[0]); + return rowToField(insertedSplash[0] as IUserSplashTable); } async delete({ userId, splashId }: IUserSplashKey): Promise { diff --git a/src/lib/domain/project-health/project-health.ts b/src/lib/domain/project-health/project-health.ts index 07a7b6be5b..ababe0a4b2 100644 --- a/src/lib/domain/project-health/project-health.ts +++ b/src/lib/domain/project-health/project-health.ts @@ -22,7 +22,9 @@ const getPotentiallyStaleCount = ( const today = new Date().valueOf(); return features.filter((feature) => { - const diff = today - feature.createdAt?.valueOf(); + const diff = feature.createdAt + ? today - feature.createdAt.valueOf() + : 0; const featureTypeExpectedLifetime = featureTypes.find( (t) => t.id === feature.type, )?.lifetimeDays; @@ -30,6 +32,7 @@ const getPotentiallyStaleCount = ( return ( !feature.stale && featureTypeExpectedLifetime !== null && + featureTypeExpectedLifetime !== undefined && diff >= featureTypeExpectedLifetime * hoursToMilliseconds(24) ); }).length; diff --git a/src/lib/features/client-feature-toggles/fakes/fake-client-feature-toggle-store.ts b/src/lib/features/client-feature-toggles/fakes/fake-client-feature-toggle-store.ts index 56654ed84d..1bfe9a6469 100644 --- a/src/lib/features/client-feature-toggles/fakes/fake-client-feature-toggle-store.ts +++ b/src/lib/features/client-feature-toggles/fakes/fake-client-feature-toggle-store.ts @@ -38,7 +38,7 @@ export default class FakeClientFeatureToggleStore ...t, enabled: true, strategies: [], - description: t.description || '', + description: t.description, type: t.type || 'Release', stale: t.stale || false, variants: [], diff --git a/src/lib/features/context/context-field-store.ts b/src/lib/features/context/context-field-store.ts index 1cd74900ca..ebabef8a48 100644 --- a/src/lib/features/context/context-field-store.ts +++ b/src/lib/features/context/context-field-store.ts @@ -50,7 +50,7 @@ const mapRow = (row: ContextFieldDB): IContextField => ({ interface ICreateContextField { name: string; - description: string; + description?: string | null; stickiness: boolean; sort_order: number; legal_values?: string; @@ -80,8 +80,8 @@ class ContextFieldStore implements IContextFieldStore { return { name: data.name, description: data.description, - stickiness: data.stickiness, - sort_order: data.sortOrder, // eslint-disable-line + stickiness: data.stickiness || false, + sort_order: data.sortOrder || 0, legal_values: JSON.stringify(data.legalValues || []), }; } diff --git a/src/lib/features/context/context-service.ts b/src/lib/features/context/context-service.ts index b96b1e680d..8ffb83043d 100644 --- a/src/lib/features/context/context-service.ts +++ b/src/lib/features/context/context-service.ts @@ -21,7 +21,7 @@ import { import type { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType'; import type EventService from '../events/event-service'; import { contextSchema, legalValueSchema } from '../../services/context-schema'; -import { NameExistsError } from '../../error'; +import { NameExistsError, NotFoundError } from '../../error'; import { nameSchema } from '../../schema/feature-schema'; import type { LegalValueSchema } from '../../openapi'; @@ -63,7 +63,13 @@ class ContextService { } async getContextField(name: string): Promise { - return this.contextFieldStore.get(name); + const field = await this.contextFieldStore.get(name); + if (field === undefined) { + throw new NotFoundError( + `Could not find context field with name ${name}`, + ); + } + return field; } async getStrategiesByContextField( @@ -125,6 +131,11 @@ class ContextService { const contextField = await this.contextFieldStore.get( updatedContextField.name, ); + if (contextField === undefined) { + throw new NotFoundError( + `Could not find context field with name: ${updatedContextField.name}`, + ); + } const value = await contextSchema.validateAsync(updatedContextField); await this.contextFieldStore.update(value); @@ -147,6 +158,11 @@ class ContextService { const contextField = await this.contextFieldStore.get( contextFieldLegalValue.name, ); + if (contextField === undefined) { + throw new NotFoundError( + `Context field with name ${contextFieldLegalValue.name} was not found`, + ); + } const validatedLegalValue = await legalValueSchema.validateAsync( contextFieldLegalValue.legalValue, ); @@ -186,6 +202,11 @@ class ContextService { const contextField = await this.contextFieldStore.get( contextFieldLegalValue.name, ); + if (contextField === undefined) { + throw new NotFoundError( + `Could not find context field with name ${contextFieldLegalValue.name}`, + ); + } const newContextField = { ...contextField, diff --git a/src/lib/features/export-import-toggles/export-import-permissions.e2e.test.ts b/src/lib/features/export-import-toggles/export-import-permissions.e2e.test.ts index 0d2881069d..ab5bbc4d87 100644 --- a/src/lib/features/export-import-toggles/export-import-permissions.e2e.test.ts +++ b/src/lib/features/export-import-toggles/export-import-permissions.e2e.test.ts @@ -262,7 +262,7 @@ beforeAll(async () => { contextFieldStore = db.stores.contextFieldStore; const roles = await accessService.getRootRoles(); - adminRole = roles.find((role) => role.name === RoleName.ADMIN); + adminRole = roles.find((role) => role.name === RoleName.ADMIN)!; await createUserEditorAccess( regularUserName, diff --git a/src/lib/features/export-import-toggles/export-import-service.ts b/src/lib/features/export-import-toggles/export-import-service.ts index 7558007a09..60f2ef1f59 100644 --- a/src/lib/features/export-import-toggles/export-import-service.ts +++ b/src/lib/features/export-import-toggles/export-import-service.ts @@ -463,7 +463,7 @@ export default class ExportImportService this.contextService.createContextField( { name: contextField.name, - description: contextField.description || '', + description: contextField.description, legalValues: contextField.legalValues, stickiness: contextField.stickiness, }, diff --git a/src/lib/features/export-import-toggles/import-permissions-service.ts b/src/lib/features/export-import-toggles/import-permissions-service.ts index 8c4f0b021f..51629843de 100644 --- a/src/lib/features/export-import-toggles/import-permissions-service.ts +++ b/src/lib/features/export-import-toggles/import-permissions-service.ts @@ -48,12 +48,14 @@ export class ImportPermissionsService { ): Promise { const availableContextFields = await this.contextService.getAll(); - return dto.data.contextFields?.filter( - (contextField) => - !availableContextFields.some( - (availableField) => - availableField.name === contextField.name, - ), + return ( + dto.data.contextFields?.filter( + (contextField) => + !availableContextFields.some( + (availableField) => + availableField.name === contextField.name, + ), + ) || [] ); } diff --git a/src/lib/features/feature-search/feature-search-store.ts b/src/lib/features/feature-search/feature-search-store.ts index 4c73107e08..e7dbda8daa 100644 --- a/src/lib/features/feature-search/feature-search-store.ts +++ b/src/lib/features/feature-search/feature-search-store.ts @@ -253,7 +253,7 @@ class FeatureSearchStore implements IFeatureSearchStore { const rankingSql = this.buildRankingSql( favoritesFirst, - sortBy, + sortBy || '', validatedSortOrder, lastSeenQuery, ); @@ -705,12 +705,14 @@ const applyMultiQueryParams = ( ) => (dbSubQuery: Knex.QueryBuilder) => Knex.QueryBuilder, ): void => { queryParams.forEach((param) => { - const values = param.values.map((val) => - (Array.isArray(fields) - ? val.split(/:(.+)/).filter(Boolean) - : [val] - ).map((s) => s.trim()), - ); + const values = param.values + .filter((v) => typeof v === 'string') + .map((val) => + (Array.isArray(fields) + ? val!.split(/:(.+)/).filter(Boolean) + : [val] + ).map((s) => s.trim()), + ); const baseSubQuery = createBaseQuery(values); switch (param.operator) { diff --git a/src/lib/features/feature-toggle/archive-feature-toggle-controller.ts b/src/lib/features/feature-toggle/archive-feature-toggle-controller.ts index cdce65624d..9cbafdfadc 100644 --- a/src/lib/features/feature-toggle/archive-feature-toggle-controller.ts +++ b/src/lib/features/feature-toggle/archive-feature-toggle-controller.ts @@ -150,11 +150,23 @@ export default class ArchiveController extends Controller { true, extractUserIdFromUser(user), ); + this.openApiService.respondWithValidation( 200, res, archivedFeaturesSchema.$id, - { version: 2, features: serializeDates(features) }, + { + version: 2, + features: serializeDates( + features.map((feature) => { + return { + ...feature, + stale: feature.stale || false, + archivedAt: feature.archivedAt!, + }; + }), + ), + }, ); } diff --git a/src/lib/features/feature-toggle/converters/feature-toggle-row-converter.ts b/src/lib/features/feature-toggle/converters/feature-toggle-row-converter.ts index d6d1b26869..ff8feeb5a1 100644 --- a/src/lib/features/feature-toggle/converters/feature-toggle-row-converter.ts +++ b/src/lib/features/feature-toggle/converters/feature-toggle-row-converter.ts @@ -147,7 +147,7 @@ export class FeatureToggleRowConverter { feature.name = row.name; feature.description = row.description; feature.project = row.project; - feature.stale = row.stale; + feature.stale = row.stale || false; feature.type = row.type; feature.lastSeenAt = row.last_seen_at; feature.variants = row.variants || []; @@ -176,13 +176,13 @@ export class FeatureToggleRowConverter { const result = rows.reduce((acc, r) => { let feature: PartialDeep = acc[r.name] ?? { strategies: [], + stale: r.stale || false, }; feature = this.createBaseFeature(r, feature, featureQuery); feature.createdAt = r.created_at; feature.favorite = r.favorite; - this.addLastSeenByEnvironment(feature, r); acc[r.name] = feature; diff --git a/src/lib/features/feature-toggle/fakes/fake-feature-strategies-store.ts b/src/lib/features/feature-toggle/fakes/fake-feature-strategies-store.ts index b8b6706505..028e92e2ac 100644 --- a/src/lib/features/feature-toggle/fakes/fake-feature-strategies-store.ts +++ b/src/lib/features/feature-toggle/fakes/fake-feature-strategies-store.ts @@ -1,4 +1,4 @@ -import { randomUUID } from 'crypto'; +import { randomUUID } from 'node:crypto'; import type { FeatureToggleWithEnvironment, IFeatureOverview, @@ -67,7 +67,7 @@ export default class FakeFeatureStrategiesStore return this.featureStrategies.some((s) => s.id === id); } - async get(id: string): Promise { + async get(id: string): Promise { return this.featureStrategies.find((s) => s.id === id); } @@ -180,8 +180,8 @@ export default class FakeFeatureStrategiesStore archived: boolean = false, ): Promise { const rows = this.featureToggles.filter((toggle) => { - if (featureQuery.namePrefix) { - if (featureQuery.project) { + if (featureQuery?.namePrefix) { + if (featureQuery?.project) { return ( (toggle.name.startsWith(featureQuery.namePrefix) && featureQuery.project.some((project) => @@ -192,7 +192,7 @@ export default class FakeFeatureStrategiesStore } return toggle.name.startsWith(featureQuery.namePrefix); } - if (featureQuery.project) { + if (featureQuery?.project) { return ( featureQuery.project.some((project) => project.includes(toggle.project), @@ -205,7 +205,7 @@ export default class FakeFeatureStrategiesStore ...t, enabled: true, strategies: [], - description: t.description || '', + description: t.description || undefined, type: t.type || 'Release', stale: t.stale || false, variants: [], @@ -233,7 +233,7 @@ export default class FakeFeatureStrategiesStore this.environmentAndFeature.set(environment, []); } this.environmentAndFeature - .get(environment) + .get(environment)! .push({ feature: feature_name, enabled }); return Promise.resolve(); } @@ -245,7 +245,7 @@ export default class FakeFeatureStrategiesStore this.environmentAndFeature.set( environment, this.environmentAndFeature - .get(environment) + .get(environment)! .filter((e) => e.featureName !== feature_name), ); return Promise.resolve(); @@ -271,7 +271,9 @@ export default class FakeFeatureStrategiesStore } return f; }); - return Promise.resolve(this.featureStrategies.find((f) => f.id === id)); + return Promise.resolve( + this.featureStrategies.find((f) => f.id === id)!, + ); } async deleteConfigurationsForProjectAndEnvironment( diff --git a/src/lib/features/feature-toggle/fakes/fake-feature-toggle-store.ts b/src/lib/features/feature-toggle/fakes/fake-feature-toggle-store.ts index 7aecc5aaf1..63ba15d8c2 100644 --- a/src/lib/features/feature-toggle/fakes/fake-feature-toggle-store.ts +++ b/src/lib/features/feature-toggle/fakes/fake-feature-toggle-store.ts @@ -87,8 +87,11 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore { return this.features.filter((f) => names.includes(f.name)); } - async getProjectId(name: string): Promise { - return this.get(name).then((f) => f.project); + async getProjectId(name: string | undefined): Promise { + if (name === undefined) { + return Promise.resolve(undefined); + } + return Promise.resolve(this.get(name).then((f) => f.project)); } private getFilterQuery(query: Partial) { @@ -164,7 +167,7 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore { if (revive) { revive.archived = false; } - return this.update(revive.project, revive); + return this.update(revive!.project, revive!); } async getFeatureToggleList( @@ -195,7 +198,7 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore { if (exists) { const id = this.features.findIndex((f) => f.name === data.name); const old = this.features.find((f) => f.name === data.name); - const updated = { ...old, ...data }; + const updated = { project, ...old, ...data }; this.features.splice(id, 1); this.features.push(updated); return updated; @@ -293,8 +296,9 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore { } if ( + queryModifiers.date && new Date(feature[queryModifiers.dateAccessor]).getTime() >= - new Date(queryModifiers.date).getTime() + new Date(queryModifiers.date).getTime() ) { return true; } @@ -303,6 +307,7 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore { feature[queryModifiers.dateAccessor], ).getTime(); if ( + queryModifiers.range && featureDate >= new Date(queryModifiers.range[0]).getTime() && featureDate <= new Date(queryModifiers.range[1]).getTime() ) { diff --git a/src/lib/features/feature-toggle/feature-toggle-controller.ts b/src/lib/features/feature-toggle/feature-toggle-controller.ts index bd456dfecb..25a2fa4bca 100644 --- a/src/lib/features/feature-toggle/feature-toggle-controller.ts +++ b/src/lib/features/feature-toggle/feature-toggle-controller.ts @@ -670,7 +670,7 @@ export default class ProjectFeaturesController extends Controller { 201, res, featureSchema.$id, - serializeDates(created), + serializeDates({ ...created, stale: created.stale || false }), ); } @@ -693,7 +693,7 @@ export default class ProjectFeaturesController extends Controller { 201, res, featureSchema.$id, - serializeDates(created), + serializeDates({ ...created, stale: created.stale || false }), ); } @@ -740,8 +740,13 @@ export default class ProjectFeaturesController extends Controller { environmentVariants: variantEnvironments === 'true', userId: user.id, }); - - res.status(200).json(serializeDates(this.maybeAnonymise(feature))); + const maybeAnonymized = this.maybeAnonymise(feature); + res.status(200).json( + serializeDates({ + ...maybeAnonymized, + stale: maybeAnonymized.stale || false, + }), + ); } async updateFeature( @@ -771,7 +776,7 @@ export default class ProjectFeaturesController extends Controller { 200, res, featureSchema.$id, - serializeDates(created), + serializeDates({ ...created, stale: created.stale || false }), ); } @@ -795,7 +800,7 @@ export default class ProjectFeaturesController extends Controller { 200, res, featureSchema.$id, - serializeDates(updated), + serializeDates({ ...updated, stale: updated.stale || false }), ); } diff --git a/src/lib/features/feature-toggle/feature-toggle-service.ts b/src/lib/features/feature-toggle/feature-toggle-service.ts index b7a1edd6ab..96e96f2735 100644 --- a/src/lib/features/feature-toggle/feature-toggle-service.ts +++ b/src/lib/features/feature-toggle/feature-toggle-service.ts @@ -294,7 +294,9 @@ class FeatureToggleService { project: string, ): Promise { const toggle = await this.featureToggleStore.get(featureName); - + if (toggle === undefined) { + throw new NotFoundError(`Could not find feature ${featureName}`); + } if (toggle.archived || Boolean(toggle.archivedAt)) { throw new ArchivedFeatureError(); } @@ -840,7 +842,9 @@ class FeatureToggleService { ): Promise> { const { projectId, environment, featureName } = context; const existingStrategy = await this.featureStrategiesStore.get(id); - + if (existingStrategy === undefined) { + throw new NotFoundError(`Could not find strategy with id ${id}`); + } this.validateUpdatedProperties(context, existingStrategy); await this.validateStrategyType(updates.name); await this.validateProjectCanAccessSegments( @@ -920,6 +924,9 @@ class FeatureToggleService { const { projectId, environment, featureName } = context; const existingStrategy = await this.featureStrategiesStore.get(id); + if (existingStrategy === undefined) { + throw new NotFoundError(`Could not find strategy with id ${id}`); + } this.validateUpdatedProperties(context, existingStrategy); if (existingStrategy.id === id) { @@ -983,6 +990,10 @@ class FeatureToggleService { auditUser: IAuditUser, ): Promise { const existingStrategy = await this.featureStrategiesStore.get(id); + if (!existingStrategy) { + // If the strategy doesn't exist, do nothing. + return; + } const { featureName, projectId, environment } = context; this.validateUpdatedProperties(context, existingStrategy); @@ -1142,11 +1153,17 @@ class FeatureToggleService { featureName, environment, }); - return featureEnvironment.variants || []; + return featureEnvironment?.variants || []; } async getFeatureMetadata(featureName: string): Promise { - return this.featureToggleStore.get(featureName); + const metaData = await this.featureToggleStore.get(featureName); + if (metaData === undefined) { + throw new NotFoundError( + `Could find metadata for feature with name ${featureName}`, + ); + } + return metaData; } async getClientFeatures( @@ -1172,7 +1189,7 @@ class FeatureToggleService { type, enabled, project, - stale, + stale: stale || false, strategies, variants, description, @@ -1322,7 +1339,11 @@ class FeatureToggleService { ): Promise { try { const project = await this.projectStore.get(projectId); - + if (project === undefined) { + throw new NotFoundError( + `Could not find project with id: ${projectId}`, + ); + } const patternData = project.featureNaming; const namingPattern = patternData?.pattern; @@ -1490,7 +1511,11 @@ class FeatureToggleService { ...featureData, name: featureName, }); - + if (preData === undefined) { + throw new NotFoundError( + `Could find feature toggle with name ${featureName}`, + ); + } await this.eventService.storeEvent( new FeatureMetadataUpdateEvent({ auditUser, @@ -1601,6 +1626,9 @@ class FeatureToggleService { let msg: string; try { const feature = await this.featureToggleStore.get(name); + if (feature === undefined) { + return; + } msg = feature.archived ? 'An archived flag with that name already exists' : 'A flag with that name already exists'; @@ -1620,6 +1648,11 @@ class FeatureToggleService { auditUser: IAuditUser, ): Promise { const feature = await this.featureToggleStore.get(featureName); + if (feature === undefined) { + throw new NotFoundError( + `Could not find feature with name: ${featureName}`, + ); + } const { project } = feature; feature.stale = isStale; await this.featureToggleStore.update(project, feature); @@ -1658,7 +1691,11 @@ class FeatureToggleService { projectId?: string, ): Promise { const feature = await this.featureToggleStore.get(featureName); - + if (feature === undefined) { + throw new NotFoundError( + `Could not find feature with name ${featureName}`, + ); + } if (projectId) { await this.validateFeatureBelongsToProject({ featureName, @@ -1941,7 +1978,7 @@ class FeatureToggleService { }), ); } - return feature; + return feature!; // If we get here we know the toggle exists } async changeProject( @@ -1968,6 +2005,11 @@ class FeatureToggleService { ); } const feature = await this.featureToggleStore.get(featureName); + if (feature === undefined) { + throw new NotFoundError( + `Could not find feature with name ${featureName}`, + ); + } const oldProject = feature.project; feature.project = newProject; await this.featureToggleStore.update(newProject, feature); @@ -1989,6 +2031,9 @@ class FeatureToggleService { ): Promise { await this.validateNoChildren(featureName); const toggle = await this.featureToggleStore.get(featureName); + if (toggle === undefined) { + return; /// Do nothing, toggle is already deleted + } const tags = await this.tagStore.getAllTagsForFeature(featureName); await this.featureToggleStore.delete(featureName); @@ -2275,7 +2320,7 @@ class FeatureToggleService { featureName, environment, }) - ).variants || + )?.variants || []; await this.eventService.storeEvent( @@ -2353,7 +2398,7 @@ class FeatureToggleService { featureName, environment: env, }); - oldVariants[env] = featureEnv.variants || []; + oldVariants[env] = featureEnv?.variants || []; } await this.eventService.storeEvents( diff --git a/src/lib/features/feature-toggle/feature-toggle-store.ts b/src/lib/features/feature-toggle/feature-toggle-store.ts index 4084024119..5d355469e7 100644 --- a/src/lib/features/feature-toggle/feature-toggle-store.ts +++ b/src/lib/features/feature-toggle/feature-toggle-store.ts @@ -44,15 +44,15 @@ const FEATURE_COLUMNS = [ export interface FeaturesTable { name: string; - description: string; - type: string; - stale: boolean; + description: string | null; + type?: string; + stale?: boolean | null; project: string; last_seen_at?: Date; created_at?: Date; - impression_data: boolean; + impression_data?: boolean | null; archived?: boolean; - archived_at?: Date; + archived_at?: Date | null; created_by_user_id?: number; } @@ -309,7 +309,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore { const result = await query; return result.map((row) => ({ - type: row.type, + type: row.type!, count: Number(row.count), })); } @@ -449,11 +449,11 @@ export default class FeatureToggleStore implements IFeatureToggleStore { description: row.description, type: row.type, project: row.project, - stale: row.stale, + stale: row.stale || false, createdAt: row.created_at, lastSeenAt: row.last_seen_at, - impressionData: row.impression_data, - archivedAt: row.archived_at, + impressionData: row.impression_data || false, + archivedAt: row.archived_at || undefined, archived: row.archived_at != null, }; } @@ -472,13 +472,13 @@ export default class FeatureToggleStore implements IFeatureToggleStore { insertToRow(project: string, data: FeatureToggleInsert): FeaturesTable { const row = { name: data.name, - description: data.description, + description: data.description || null, type: data.type, project, archived_at: data.archived ? new Date() : null, - stale: data.stale, + stale: data.stale || false, created_at: data.createdAt, - impression_data: data.impressionData, + impression_data: data.impressionData || false, created_by_user_id: data.createdByUserId, }; if (!row.created_at) { @@ -494,7 +494,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore { ): Omit { const row = { name: data.name, - description: data.description, + description: data.description || null, type: data.type, project, archived_at: data.archived ? new Date() : null, diff --git a/src/lib/features/feature-toggle/types/feature-toggle-store-type.ts b/src/lib/features/feature-toggle/types/feature-toggle-store-type.ts index e083afb81d..89afef8020 100644 --- a/src/lib/features/feature-toggle/types/feature-toggle-store-type.ts +++ b/src/lib/features/feature-toggle/types/feature-toggle-store-type.ts @@ -23,7 +23,7 @@ export interface IFeatureToggleStore extends Store { setLastSeen(data: LastSeenInput[]): Promise; - getProjectId(name: string): Promise; + getProjectId(name: string | undefined): Promise; create(project: string, data: FeatureToggleInsert): Promise; diff --git a/src/lib/features/frontend-api/create-context.ts b/src/lib/features/frontend-api/create-context.ts index 286eddb32f..8fb3640906 100644 --- a/src/lib/features/frontend-api/create-context.ts +++ b/src/lib/features/frontend-api/create-context.ts @@ -1,5 +1,5 @@ // Copy of https://github.com/Unleash/unleash-proxy/blob/main/src/create-context.ts. -import crypto from 'crypto'; +import crypto from 'node:crypto'; import type { Context } from 'unleash-client'; export function createContext(contextData: any): Context { diff --git a/src/lib/features/frontend-api/frontend-api-service.ts b/src/lib/features/frontend-api/frontend-api-service.ts index 39ce6ba717..66e9a3b151 100644 --- a/src/lib/features/frontend-api/frontend-api-service.ts +++ b/src/lib/features/frontend-api/frontend-api-service.ts @@ -1,4 +1,4 @@ -import crypto from 'crypto'; +import crypto from 'node:crypto'; import type { IAuditUser, IUnleashConfig, @@ -63,7 +63,7 @@ export class FrontendApiService { private readonly clients: Map> = new Map(); - private cachedFrontendSettings?: FrontendSettings; + private cachedFrontendSettings: FrontendSettings; constructor( config: Config, @@ -228,9 +228,12 @@ export class FrontendApiService { async fetchFrontendSettings(): Promise { try { this.cachedFrontendSettings = - await this.services.settingService.get(frontendSettingsKey, { - frontendApiOrigins: this.config.frontendApiOrigins, - }); + await this.services.settingService.getWithDefault( + frontendSettingsKey, + { + frontendApiOrigins: this.config.frontendApiOrigins, + }, + ); } catch (error) { this.logger.debug('Unable to fetch frontend settings', error); } diff --git a/src/lib/features/frontend-api/global-frontend-api-cache.ts b/src/lib/features/frontend-api/global-frontend-api-cache.ts index e6784b2b61..a6dbc7bce0 100644 --- a/src/lib/features/frontend-api/global-frontend-api-cache.ts +++ b/src/lib/features/frontend-api/global-frontend-api-cache.ts @@ -154,7 +154,10 @@ export class GlobalFrontendApiCache extends EventEmitter { Object.fromEntries( Object.entries(value).map(([innerKey, innerValue]) => [ innerKey, - mapFeatureForClient(innerValue), + mapFeatureForClient({ + ...innerValue, + stale: innerValue.stale || false, + }), ]), ), ]); diff --git a/src/lib/features/metrics/client-metrics/client-metrics-service.e2e.test.ts b/src/lib/features/metrics/client-metrics/client-metrics-service.e2e.test.ts index 37aa28f1e8..eca8357571 100644 --- a/src/lib/features/metrics/client-metrics/client-metrics-service.e2e.test.ts +++ b/src/lib/features/metrics/client-metrics/client-metrics-service.e2e.test.ts @@ -2,15 +2,16 @@ import ClientInstanceService from '../instance/instance-service'; import type { IClientApp } from '../../../types/model'; import { secondsToMilliseconds } from 'date-fns'; import { createTestConfig } from '../../../../test/config/test-config'; -import type { IUnleashConfig, IUnleashStores } from '../../../types'; +import { + APPLICATION_CREATED, + type IUnleashConfig, + type IUnleashStores, +} from '../../../types'; import { FakePrivateProjectChecker } from '../../private-project/fakePrivateProjectChecker'; import type { ITestDb } from '../../../../test/e2e/helpers/database-init'; - -const faker = require('faker'); -const dbInit = require('../../../../test/e2e/helpers/database-init'); -const getLogger = require('../../../../test/fixtures/no-logger'); -const { APPLICATION_CREATED } = require('../../../types/events'); - +import dbInit from '../../../../test/e2e/helpers/database-init'; +import { noLoggerProvider as getLogger } from '../../../../test/fixtures/no-logger'; +import faker from 'faker'; let stores: IUnleashStores; let db: ITestDb; let clientInstanceService: ClientInstanceService; diff --git a/src/lib/features/metrics/client-metrics/client-metrics-store-v2-type.ts b/src/lib/features/metrics/client-metrics/client-metrics-store-v2-type.ts index b4a7c6e90e..60c853dd3c 100644 --- a/src/lib/features/metrics/client-metrics/client-metrics-store-v2-type.ts +++ b/src/lib/features/metrics/client-metrics/client-metrics-store-v2-type.ts @@ -20,7 +20,7 @@ export interface IClientMetricsEnvVariant extends IClientMetricsEnvKey { export interface IClientMetricsStoreV2 extends Store { - batchInsertMetrics(metrics: IClientMetricsEnv[]): Promise; + batchInsertMetrics(metrics: IClientMetricsEnv[] | undefined): Promise; getMetricsForFeatureToggle( featureName: string, hoursBack?: number, diff --git a/src/lib/features/metrics/client-metrics/client-metrics-store-v2.e2e.test.ts b/src/lib/features/metrics/client-metrics/client-metrics-store-v2.e2e.test.ts index 986cf420d8..d3d4b9ae69 100644 --- a/src/lib/features/metrics/client-metrics/client-metrics-store-v2.e2e.test.ts +++ b/src/lib/features/metrics/client-metrics/client-metrics-store-v2.e2e.test.ts @@ -352,12 +352,12 @@ test('Should get metric', async () => { }, ]; await clientMetricsStore.batchInsertMetrics(metrics); - const metric = await clientMetricsStore.get({ + const metric = (await clientMetricsStore.get({ featureName: 'demo4', timestamp: twoDaysAgo, appName: 'backend-api', environment: 'dev', - }); + }))!; expect(metric.featureName).toBe('demo4'); expect(metric.yes).toBe(41); diff --git a/src/lib/features/metrics/instance/instance-service.ts b/src/lib/features/metrics/instance/instance-service.ts index 541de3dacb..6518290a6d 100644 --- a/src/lib/features/metrics/instance/instance-service.ts +++ b/src/lib/features/metrics/instance/instance-service.ts @@ -26,6 +26,7 @@ import type { Logger } from '../../../logger'; import { findOutdatedSDKs, isOutdatedSdk } from './findOutdatedSdks'; import type { OutdatedSdksSchema } from '../../../openapi/spec/outdated-sdks-schema'; import { CLIENT_REGISTERED } from '../../../metric-events'; +import { NotFoundError } from '../../../error'; export default class ClientInstanceService { apps = {}; @@ -219,7 +220,11 @@ export default class ClientInstanceService { this.strategyStore.getAll(), this.featureToggleStore.getAll(), ]); - + if (application === undefined) { + throw new NotFoundError( + `Could not find application with appName ${appName}`, + ); + } return { appName: application.appName, createdAt: application.createdAt, diff --git a/src/lib/features/playground/feature-evaluator/strategy/remote-address-strategy.ts b/src/lib/features/playground/feature-evaluator/strategy/remote-address-strategy.ts index 9580895409..547a21e8c2 100644 --- a/src/lib/features/playground/feature-evaluator/strategy/remote-address-strategy.ts +++ b/src/lib/features/playground/feature-evaluator/strategy/remote-address-strategy.ts @@ -27,6 +27,7 @@ export default class RemoteAddressStrategy extends Strategy { return false; } } + return false; }, ); } diff --git a/src/lib/features/playground/feature-evaluator/strategy/user-with-id-strategy.ts b/src/lib/features/playground/feature-evaluator/strategy/user-with-id-strategy.ts index 31268fb5da..372f4f6e4e 100644 --- a/src/lib/features/playground/feature-evaluator/strategy/user-with-id-strategy.ts +++ b/src/lib/features/playground/feature-evaluator/strategy/user-with-id-strategy.ts @@ -10,6 +10,8 @@ export default class UserWithIdStrategy extends Strategy { const userIdList = parameters.userIds ? parameters.userIds.split(/\s*,\s*/) : []; - return userIdList.includes(context.userId); + return ( + context.userId !== undefined && userIdList.includes(context.userId) + ); } } diff --git a/src/lib/features/playground/offline-unleash-client.test.ts b/src/lib/features/playground/offline-unleash-client.test.ts index ce160faf85..575a6b6f80 100644 --- a/src/lib/features/playground/offline-unleash-client.test.ts +++ b/src/lib/features/playground/offline-unleash-client.test.ts @@ -453,6 +453,7 @@ describe('offline client', () => { const client = await offlineUnleashClient({ features: [ { + // @ts-expect-error: hostnames is incompatible with index signature | undefined is not assignable to type string strategies, // impressionData: false, enabled: true, diff --git a/src/lib/features/project-environments/environment-service.ts b/src/lib/features/project-environments/environment-service.ts index 51e0040193..fdfd2cd7a7 100644 --- a/src/lib/features/project-environments/environment-service.ts +++ b/src/lib/features/project-environments/environment-service.ts @@ -71,14 +71,20 @@ export default class EnvironmentService { } async get(name: string): Promise { - return this.environmentStore.get(name); + const env = await this.environmentStore.get(name); + if (env === undefined) { + throw new NotFoundError( + `Could not find environment with name ${name}`, + ); + } + return env; } async getProjectEnvironments( projectId: string, ): Promise { // This function produces an object for every environment, in that object is a boolean - // describing whether or not that environment is enabled - aka not deprecated + // describing whether that environment is enabled - aka not deprecated const environments = await this.projectStore.getEnvironmentsForProject(projectId); const environmentsOnProject = new Set( diff --git a/src/lib/features/project-environments/fake-environment-store.ts b/src/lib/features/project-environments/fake-environment-store.ts index e898a6b528..a7349b56a1 100644 --- a/src/lib/features/project-environments/fake-environment-store.ts +++ b/src/lib/features/project-environments/fake-environment-store.ts @@ -65,7 +65,7 @@ export default class FakeEnvironmentStore implements IEnvironmentStore { ): Promise { const found = this.environments.find( (en: IEnvironment) => en.name === name, - ); + )!; const idx = this.environments.findIndex( (en: IEnvironment) => en.name === name, ); @@ -78,7 +78,7 @@ export default class FakeEnvironmentStore implements IEnvironmentStore { async updateSortOrder(id: string, value: number): Promise { const environment = this.environments.find( (env: IEnvironment) => env.name === id, - ); + )!; environment.sortOrder = value; return Promise.resolve(); } @@ -90,7 +90,7 @@ export default class FakeEnvironmentStore implements IEnvironmentStore { ): Promise { const environment = this.environments.find( (env: IEnvironment) => env.name === id, - ); + )!; environment[field] = value; return Promise.resolve(); } @@ -132,8 +132,8 @@ export default class FakeEnvironmentStore implements IEnvironmentStore { destroy(): void {} - async get(key: string): Promise { - return this.environments.find((e) => e.name === key); + async get(key: string): Promise { + return Promise.resolve(this.environments.find((e) => e.name === key)); } async getAllWithCounts(): Promise { diff --git a/src/lib/features/project-insights/project-insights-service.ts b/src/lib/features/project-insights/project-insights-service.ts index e98d134e4e..6484a2437c 100644 --- a/src/lib/features/project-insights/project-insights-service.ts +++ b/src/lib/features/project-insights/project-insights-service.ts @@ -112,7 +112,7 @@ export class ProjectInsightsService { ]); return { - health: project.health || 0, + health: project?.health || 0, features: features, }; } diff --git a/src/lib/features/project/project-service.e2e.test.ts b/src/lib/features/project/project-service.e2e.test.ts index 15656830dc..bafca338ee 100644 --- a/src/lib/features/project/project-service.e2e.test.ts +++ b/src/lib/features/project/project-service.e2e.test.ts @@ -2472,7 +2472,7 @@ test('should not delete project-bound api tokens still bound to project', async await projectService.deleteProject(project1, user, auditUser); const fetchedToken = await apiTokenService.getToken(token.secret); expect(fetchedToken).not.toBeUndefined(); - expect(fetchedToken.project).toBe(project2); + expect(fetchedToken!.project).toBe(project2); }); test('should delete project-bound api tokens when all projects they belong to are deleted', async () => { diff --git a/src/lib/features/project/project-service.ts b/src/lib/features/project/project-service.ts index 8510c33438..85865a458a 100644 --- a/src/lib/features/project/project-service.ts +++ b/src/lib/features/project/project-service.ts @@ -261,7 +261,11 @@ export default class ProjectService { } async getProject(id: string): Promise { - return this.projectStore.get(id); + const project = await this.projectStore.get(id); + if (project === undefined) { + throw new NotFoundError(`Could not find project with id ${id}`); + } + return Promise.resolve(project); } private validateAndProcessFeatureNamingPattern = ( @@ -503,7 +507,9 @@ export default class ProjectService { auditUser: IAuditUser, ): Promise { const feature = await this.featureToggleStore.get(featureName); - + if (feature === undefined) { + throw new NotFoundError(`Could not find feature ${featureName}`); + } if (feature.project !== currentProjectId) { throw new PermissionError(MOVE_FEATURE_TOGGLE); } @@ -676,7 +682,7 @@ export default class ProjectService { roleId, userId, roleName: role.name, - email: user.email, + email: user?.email, }, }), ); @@ -1374,7 +1380,11 @@ export default class ProjectService { : Promise.resolve(false), this.projectStatsStore.getProjectStats(projectId), ]); - + if (project === undefined) { + throw new NotFoundError( + `Could not find project with id ${projectId}`, + ); + } return { stats: projectStats, name: project.name, @@ -1426,6 +1436,12 @@ export default class ProjectService { this.onboardingReadModel.getOnboardingStatusForProject(projectId), ]); + if (project === undefined) { + throw new NotFoundError( + `Could not find project with id: ${projectId}`, + ); + } + return { stats: projectStats, name: project.name, diff --git a/src/lib/features/project/project-store.e2e.test.ts b/src/lib/features/project/project-store.e2e.test.ts index a338c10f82..9047b35584 100644 --- a/src/lib/features/project/project-store.e2e.test.ts +++ b/src/lib/features/project/project-store.e2e.test.ts @@ -45,7 +45,7 @@ test('should exclude archived projects', async () => { test('should have default project', async () => { const project = await projectStore.get('default'); expect(project).toBeDefined(); - expect(project.id).toBe('default'); + expect(project!.id).toBe('default'); }); test('should create new project', async () => { @@ -58,11 +58,11 @@ test('should create new project', async () => { await projectStore.create(project); const ret = await projectStore.get('test'); const exists = await projectStore.exists('test'); - expect(project.id).toEqual(ret.id); - expect(project.name).toEqual(ret.name); - expect(project.description).toEqual(ret.description); - expect(ret.createdAt).toBeTruthy(); - expect(ret.updatedAt).toBeTruthy(); + expect(project.id).toEqual(ret!.id); + expect(project.name).toEqual(ret!.name); + expect(project.description).toEqual(ret!.description); + expect(ret!.createdAt).toBeTruthy(); + expect(ret!.updatedAt).toBeTruthy(); expect(exists).toBe(true); }); @@ -103,8 +103,8 @@ test('should update project', async () => { const readProject = await projectStore.get(project.id); - expect(updatedProject.name).toBe(readProject.name); - expect(updatedProject.description).toBe(readProject.description); + expect(updatedProject.name).toBe(readProject!.name); + expect(updatedProject.description).toBe(readProject!.description); }); test('should give error when getting unknown project', async () => { diff --git a/src/lib/features/segment/client-segment.e2e.test.ts b/src/lib/features/segment/client-segment.e2e.test.ts index 629d4d70d1..8055ad6955 100644 --- a/src/lib/features/segment/client-segment.e2e.test.ts +++ b/src/lib/features/segment/client-segment.e2e.test.ts @@ -355,7 +355,9 @@ test('should inline segment constraints into features by default', async () => { const clientFeatures = await fetchClientFeatures(); const clientStrategies = clientFeatures.flatMap((f) => f.strategies); - const clientConstraints = clientStrategies.flatMap((s) => s.constraints); + const clientConstraints = clientStrategies.flatMap( + (s) => s.constraints || [], + ); const clientValues = clientConstraints.flatMap((c) => c.values); const uniqueValues = [...new Set(clientValues)]; diff --git a/src/lib/features/segment/segment-service.ts b/src/lib/features/segment/segment-service.ts index 8fa3a90172..39557cde04 100644 --- a/src/lib/features/segment/segment-service.ts +++ b/src/lib/features/segment/segment-service.ts @@ -20,7 +20,7 @@ import type { ISegmentService, StrategiesUsingSegment, } from './segment-service-interface'; -import { PermissionError } from '../../error'; +import { NotFoundError, PermissionError } from '../../error'; import type { IChangeRequestAccessReadModel } from '../change-request-access-service/change-request-access-read-model'; import type { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType'; import type EventService from '../events/event-service'; @@ -74,7 +74,11 @@ export class SegmentService implements ISegmentService { } async get(id: number): Promise { - return this.segmentStore.get(id); + const segment = await this.segmentStore.get(id); + if (segment === undefined) { + throw new NotFoundError(`Could find segment with id ${id}`); + } + return segment; } async getAll(): Promise { @@ -179,7 +183,11 @@ export class SegmentService implements ISegmentService { const input = await segmentSchema.validateAsync(data); this.validateSegmentValuesLimit(input); const preData = await this.segmentStore.get(id); - + if (preData === undefined) { + throw new NotFoundError( + `Could not find segment with id ${id} to update`, + ); + } if (preData.name !== input.name) { await this.validateName(input.name); } @@ -200,6 +208,10 @@ export class SegmentService implements ISegmentService { async delete(id: number, user: User, auditUser: IAuditUser): Promise { const segment = await this.segmentStore.get(id); + if (segment === undefined) { + /// Already deleted + return; + } await this.stopWhenChangeRequestsEnabled(segment.project, user); await this.segmentStore.delete(id); await this.eventService.storeEvent( diff --git a/src/lib/features/tag-type/fake-tag-type-store.ts b/src/lib/features/tag-type/fake-tag-type-store.ts index c61da6ab3b..f66891206c 100644 --- a/src/lib/features/tag-type/fake-tag-type-store.ts +++ b/src/lib/features/tag-type/fake-tag-type-store.ts @@ -1,6 +1,6 @@ import type { ITagType, ITagTypeStore } from './tag-type-store-type'; -const NotFoundError = require('../../error/notfound-error'); +import { NotFoundError } from '../../error'; export default class FakeTagTypeStore implements ITagTypeStore { tagTypes: ITagType[] = []; diff --git a/src/lib/features/tag-type/tag-type-service.ts b/src/lib/features/tag-type/tag-type-service.ts index 6365135d53..eb0f809e41 100644 --- a/src/lib/features/tag-type/tag-type-service.ts +++ b/src/lib/features/tag-type/tag-type-service.ts @@ -14,6 +14,7 @@ import type { ITagType, ITagTypeStore } from './tag-type-store-type'; import type { IUnleashConfig } from '../../types/option'; import type EventService from '../events/event-service'; import type { IAuditUser } from '../../types'; +import { NotFoundError } from '../../error'; export default class TagTypeService { private tagTypeStore: ITagTypeStore; @@ -37,7 +38,11 @@ export default class TagTypeService { } async getTagType(name: string): Promise { - return this.tagTypeStore.get(name); + const tagType = await this.tagTypeStore.get(name); + if (tagType === undefined) { + throw new NotFoundError(`Tagtype ${name} could not be found`); + } + return tagType; } async createTagType( diff --git a/src/lib/features/traffic-data-usage/traffic-data-usage-store.test.ts b/src/lib/features/traffic-data-usage/traffic-data-usage-store.test.ts index c669443156..2986fe782f 100644 --- a/src/lib/features/traffic-data-usage/traffic-data-usage-store.test.ts +++ b/src/lib/features/traffic-data-usage/traffic-data-usage-store.test.ts @@ -44,7 +44,7 @@ test('upsert stores new entries', async () => { statusCodeSeries: data.statusCodeSeries, }); expect(data2).toBeDefined(); - expect(data2.count).toBe(1); + expect(data2!.count).toBe(1); }); test('upsert upserts', async () => { @@ -68,7 +68,7 @@ test('upsert upserts', async () => { statusCodeSeries: data.statusCodeSeries, }); expect(data2).toBeDefined(); - expect(data2.count).toBe(4); + expect(data2!.count).toBe(4); }); test('getAll returns all', async () => { diff --git a/src/lib/metrics.ts b/src/lib/metrics.ts index b63fcf1fe2..95c707d891 100644 --- a/src/lib/metrics.ts +++ b/src/lib/metrics.ts @@ -825,7 +825,11 @@ export function registerPrometheusMetrics( eventBus, events.REQUEST_ORIGIN, ({ type, method, source }) => { - requestOriginCounter.increment({ type, method, source }); + requestOriginCounter.increment({ + type, + method, + source: source || 'unknown', + }); }, ); diff --git a/src/lib/middleware/content_type_checker.ts b/src/lib/middleware/content_type_checker.ts index 800505fbaa..fd5455429a 100644 --- a/src/lib/middleware/content_type_checker.ts +++ b/src/lib/middleware/content_type_checker.ts @@ -19,7 +19,10 @@ export default function requireContentType( } return (req, res, next) => { const contentType = req.header('Content-Type'); - if (is(contentType, acceptedContentTypes)) { + if ( + contentType !== undefined && + is(contentType, acceptedContentTypes) + ) { next(); } else { const error = new ContentTypeError( diff --git a/src/lib/middleware/rbac-middleware.ts b/src/lib/middleware/rbac-middleware.ts index 88e558f0e9..deb1c5a076 100644 --- a/src/lib/middleware/rbac-middleware.ts +++ b/src/lib/middleware/rbac-middleware.ts @@ -125,8 +125,11 @@ const rbacMiddleware = ( params.id ) { const { id } = params; - const { project } = await segmentStore.get(id); - projectId = project; + const segment = await segmentStore.get(id); + if (segment === undefined) { + return false; + } + projectId = segment.project; } return accessService.hasPermission( diff --git a/src/lib/openapi/validate.ts b/src/lib/openapi/validate.ts index 9819807861..8514c46059 100644 --- a/src/lib/openapi/validate.ts +++ b/src/lib/openapi/validate.ts @@ -33,6 +33,7 @@ export const validateSchema = ( schema: S, data: unknown, ): ISchemaValidationErrors | undefined => { + // @ts-expect-error we validate that we have an $id field, AJV apparently does not think this is enough to be willing to validate. if (!ajv.validate(schema, data)) { return { schema, diff --git a/src/lib/routes/admin-api/strategy.ts b/src/lib/routes/admin-api/strategy.ts index 0e3f1d4d7c..b025fb150c 100644 --- a/src/lib/routes/admin-api/strategy.ts +++ b/src/lib/routes/admin-api/strategy.ts @@ -253,7 +253,7 @@ class StrategyController extends Controller { res, strategySchema.$id, strategy, - { location: `strategies/${strategy.name}` }, + { location: `strategies/${strategy!.name}` }, ); } diff --git a/src/lib/routes/admin-api/telemetry.ts b/src/lib/routes/admin-api/telemetry.ts index aaa26a1e54..326ad89d8f 100644 --- a/src/lib/routes/admin-api/telemetry.ts +++ b/src/lib/routes/admin-api/telemetry.ts @@ -12,8 +12,6 @@ import { } from '../../openapi/spec/telemetry-settings-schema'; class TelemetryController extends Controller { - config: IUnleashConfig; - openApiService: OpenApiService; constructor( @@ -21,7 +19,6 @@ class TelemetryController extends Controller { { openApiService }: Pick, ) { super(config); - this.config = config; this.openApiService = openApiService; this.route({ diff --git a/src/lib/routes/admin-api/user-feedback.ts b/src/lib/routes/admin-api/user-feedback.ts index 3111313581..ee8701d1f3 100644 --- a/src/lib/routes/admin-api/user-feedback.ts +++ b/src/lib/routes/admin-api/user-feedback.ts @@ -110,7 +110,7 @@ class UserFeedbackController extends Controller { feedbackId: req.params.id, userId: req.user.id, neverShow: req.body.neverShow || false, - given: req.body.given && parseISO(req.body.given), + given: (req.body.given && parseISO(req.body.given)) || new Date(), }); this.openApiService.respondWithValidation( diff --git a/src/lib/services/access-service.ts b/src/lib/services/access-service.ts index cb28076b3b..4cd15f3b11 100644 --- a/src/lib/services/access-service.ts +++ b/src/lib/services/access-service.ts @@ -48,6 +48,7 @@ import { RoleUpdatedEvent, } from '../types'; import type EventService from '../features/events/event-service'; +import { NotFoundError } from '../error'; const { ADMIN } = permissions; @@ -536,6 +537,9 @@ export class AccessService { async getRole(id: number): Promise { const role = await this.store.get(id); + if (role === undefined) { + throw new NotFoundError(`Could not find role with id ${id}`); + } const rolePermissions = await this.store.getPermissionsForRole(role.id); return { ...role, @@ -549,6 +553,9 @@ export class AccessService { this.store.getPermissionsForRole(roleId), this.getUsersForRole(roleId), ]); + if (role === undefined) { + throw new NotFoundError(`Could not find role with id ${roleId}`); + } return { role, permissions: rolePerms, users }; } @@ -873,6 +880,11 @@ export class AccessService { async validateRoleIsNotBuiltIn(roleId: number): Promise { const role = await this.store.get(roleId); + if (role === undefined) { + throw new InvalidOperationError( + 'You cannot change a non-existing role', + ); + } if ( role.type !== CUSTOM_PROJECT_ROLE_TYPE && role.type !== CUSTOM_ROOT_ROLE_TYPE diff --git a/src/lib/services/account-service.ts b/src/lib/services/account-service.ts index bc459bf173..8d85c8b1a3 100644 --- a/src/lib/services/account-service.ts +++ b/src/lib/services/account-service.ts @@ -5,6 +5,7 @@ import type { IAccountStore, IUnleashStores } from '../types/stores'; import type { AccessService } from './access-service'; import { RoleName } from '../types/model'; import type { IAdminCount } from '../types/stores/account-store'; +import { NotFoundError } from '../error'; interface IUserWithRole extends IUser { rootRole: number; @@ -46,7 +47,12 @@ export class AccountService { } async getAccountByPersonalAccessToken(secret: string): Promise { - return this.store.getAccountByPersonalAccessToken(secret); + const account = + await this.store.getAccountByPersonalAccessToken(secret); + if (account === undefined) { + throw new NotFoundError(); + } + return account; } async getAdminCount(): Promise { diff --git a/src/lib/services/addon-service.ts b/src/lib/services/addon-service.ts index 3bf4542e2c..4f5e0c77ac 100644 --- a/src/lib/services/addon-service.ts +++ b/src/lib/services/addon-service.ts @@ -27,6 +27,7 @@ import type { IAddonDefinition } from '../types/model'; import { minutesToMilliseconds } from 'date-fns'; import type EventService from '../features/events/event-service'; import { omitKeys } from '../util'; +import { NotFoundError } from '../error'; const SUPPORTED_EVENTS = Object.keys(events).map((k) => events[k]); @@ -110,7 +111,7 @@ export default class AddonService { ); return providerDefinitions.reduce((obj, definition) => { const sensitiveParams = definition.parameters - .filter((p) => p.sensitive) + ?.filter((p) => p.sensitive) .map((p) => p.name); const o = { ...obj }; @@ -183,6 +184,9 @@ export default class AddonService { async getAddon(id: number): Promise { const addonConfig = await this.addonStore.get(id); + if (addonConfig === undefined) { + throw new NotFoundError(); + } return this.filterSensitiveFields(addonConfig); } @@ -240,7 +244,10 @@ export default class AddonService { data: IAddonDto, auditUser: IAuditUser, ): Promise { - const existingConfig = await this.addonStore.get(id); // because getting an early 404 here makes more sense + const existingConfig = await this.addonStore.get(id); + if (existingConfig === undefined) { + throw new NotFoundError(); + } // because getting an early 404 here makes more sense const addonConfig = await addonSchema.validateAsync(data); await this.validateKnownProvider(addonConfig); await this.validateRequiredParameters(addonConfig); @@ -272,6 +279,10 @@ export default class AddonService { async removeAddon(id: number, auditUser: IAuditUser): Promise { const existingConfig = await this.addonStore.get(id); + if (existingConfig === undefined) { + /// No config, no need to delete + return; + } await this.addonStore.delete(id); await this.eventService.storeEvent( new AddonConfigDeletedEvent({ @@ -310,13 +321,14 @@ export default class AddonService { }): Promise { const providerDefinition = this.addonProviders[provider].definition; - const requiredParamsMissing = providerDefinition.parameters - .filter((p) => p.required) - .map((p) => p.name) - .filter( - (requiredParam) => - !Object.keys(parameters).includes(requiredParam), - ); + const requiredParamsMissing = + providerDefinition.parameters + ?.filter((p) => p.required) + .map((p) => p.name) + .filter( + (requiredParam) => + !Object.keys(parameters).includes(requiredParam), + ) || []; if (requiredParamsMissing.length > 0) { throw new ValidationError( `Missing required parameters: ${requiredParamsMissing.join( diff --git a/src/lib/services/api-token-service.ts b/src/lib/services/api-token-service.ts index 2b47b37717..f58368ea82 100644 --- a/src/lib/services/api-token-service.ts +++ b/src/lib/services/api-token-service.ts @@ -127,7 +127,7 @@ export class ApiTokenService { } } - async getToken(secret: string): Promise { + async getToken(secret: string): Promise { return this.store.get(secret); } @@ -245,7 +245,7 @@ export class ApiTokenService { auditUser: IAuditUser, ): Promise { const previous = (await this.store.get(secret))!; - const token = await this.store.setExpiry(secret, expiresAt); + const token = (await this.store.setExpiry(secret, expiresAt))!; await this.eventService.storeEvent( new ApiTokenUpdatedEvent({ auditUser, @@ -258,7 +258,7 @@ export class ApiTokenService { public async delete(secret: string, auditUser: IAuditUser): Promise { if (await this.store.exists(secret)) { - const token = await this.store.get(secret); + const token = (await this.store.get(secret))!; await this.store.delete(secret); await this.eventService.storeEvent( new ApiTokenDeletedEvent({ diff --git a/src/lib/services/favorites-service.ts b/src/lib/services/favorites-service.ts index fb5b958f7b..f4a865e29f 100644 --- a/src/lib/services/favorites-service.ts +++ b/src/lib/services/favorites-service.ts @@ -13,6 +13,7 @@ import { import type { IUser } from '../types/user'; import type { IFavoriteProjectKey } from '../types/stores/favorite-projects'; import type EventService from '../features/events/event-service'; +import { NotFoundError } from '../error'; export interface IFavoriteFeatureProps { feature: string; @@ -61,6 +62,11 @@ export class FavoritesService { feature: feature, userId: user.id, }); + if (data === undefined) { + throw new NotFoundError( + `Feature with name ${feature} did not exist`, + ); + } await this.eventService.storeEvent( new FeatureFavoritedEvent({ featureName: feature, @@ -97,10 +103,13 @@ export class FavoritesService { { project, user }: IFavoriteProjectProps, auditUser: IAuditUser, ): Promise { - const data = this.favoriteProjectsStore.addFavoriteProject({ + const data = await this.favoriteProjectsStore.addFavoriteProject({ project, userId: user.id, }); + if (data === undefined) { + throw new NotFoundError(`Project with id ${project} was not found`); + } await this.eventService.storeEvent( new ProjectFavoritedEvent({ data: { @@ -117,7 +126,7 @@ export class FavoritesService { { project, user }: IFavoriteProjectProps, auditUser: IAuditUser, ): Promise { - const data = this.favoriteProjectsStore.delete({ + const data = await this.favoriteProjectsStore.delete({ project: project, userId: user.id, }); @@ -130,7 +139,6 @@ export class FavoritesService { auditUser, }), ); - return data; } async isFavoriteProject(favorite: IFavoriteProjectKey): Promise { diff --git a/src/lib/services/feature-tag-service.ts b/src/lib/services/feature-tag-service.ts index b8323f5092..7ee26b76b3 100644 --- a/src/lib/services/feature-tag-service.ts +++ b/src/lib/services/feature-tag-service.ts @@ -65,6 +65,9 @@ class FeatureTagService { auditUser: IAuditUser, ): Promise { const featureToggle = await this.featureToggleStore.get(featureName); + if (featureToggle === undefined) { + throw new NotFoundError(); + } const validatedTag = await tagSchema.validateAsync(tag); await this.createTagIfNeeded(validatedTag, auditUser); await this.featureTagStore.tagFeature( @@ -180,6 +183,10 @@ class FeatureTagService { auditUser: IAuditUser, ): Promise { const featureToggle = await this.featureToggleStore.get(featureName); + if (featureToggle === undefined) { + /// No toggle, so no point in removing tags + return; + } const tags = await this.featureTagStore.getAllTagsForFeature(featureName); await this.featureTagStore.untagFeature(featureName, tag); diff --git a/src/lib/services/group-service.ts b/src/lib/services/group-service.ts index c8da53965f..019e702419 100644 --- a/src/lib/services/group-service.ts +++ b/src/lib/services/group-service.ts @@ -30,6 +30,7 @@ import type { IUser } from '../types/user'; import type EventService from '../features/events/event-service'; import { SSO_SYNC_USER } from '../db/group-store'; import type { IGroupWithProjectRoles } from '../types/stores/access-store'; +import { NotFoundError } from '../error'; const setsAreEqual = (firstSet, secondSet) => firstSet.size === secondSet.size && @@ -95,6 +96,9 @@ export class GroupService { async getGroup(id: number): Promise { const group = await this.groupStore.get(id); + if (group === undefined) { + throw new NotFoundError(`Could not find group with id ${id}`); + } const groupUsers = await this.groupStore.getAllUsersByGroups([id]); const users = await this.accountStore.getAllWithId( groupUsers.map((u) => u.userId), @@ -104,7 +108,7 @@ export class GroupService { async isScimGroup(id: number): Promise { const group = await this.groupStore.get(id); - return Boolean(group.scimId); + return Boolean(group?.scimId); } async createGroup( @@ -208,6 +212,10 @@ export class GroupService { async deleteGroup(id: number, auditUser: IAuditUser): Promise { const group = await this.groupStore.get(id); + if (group === undefined) { + /// Group was already deleted, or never existed, do nothing + return; + } const existingUsers = await this.groupStore.getAllUsersByGroups([ group.id, ]); diff --git a/src/lib/services/public-signup-token-service.ts b/src/lib/services/public-signup-token-service.ts index f03a8c2cda..91dde525de 100644 --- a/src/lib/services/public-signup-token-service.ts +++ b/src/lib/services/public-signup-token-service.ts @@ -1,4 +1,4 @@ -import crypto from 'crypto'; +import crypto from 'node:crypto'; import type { Logger } from '../logger'; import { type IAuditUser, @@ -23,6 +23,7 @@ import type { IUser } from '../types/user'; import { URL } from 'url'; import { add } from 'date-fns'; import type EventService from '../features/events/event-service'; +import { NotFoundError } from '../error'; export class PublicSignupTokenService { private store: IPublicSignupTokenStore; @@ -63,7 +64,11 @@ export class PublicSignupTokenService { } public async get(secret: string): Promise { - return this.store.get(secret); + const token = await this.store.get(secret); + if (token === undefined) { + throw new NotFoundError('Could not find token with that secret'); + } + return token; } public async getAllTokens(): Promise { @@ -95,6 +100,9 @@ export class PublicSignupTokenService { auditUser: IAuditUser, ): Promise { const token = await this.get(secret); + if (token === undefined) { + throw new NotFoundError('Could not find token with that secret'); + } const user = await this.userService.createUser( { ...createUser, diff --git a/src/lib/services/session-service.ts b/src/lib/services/session-service.ts index 162b7c1899..2aaa227096 100644 --- a/src/lib/services/session-service.ts +++ b/src/lib/services/session-service.ts @@ -35,7 +35,7 @@ export default class SessionService { return this.sessionStore.getSessionsForUser(userId); } - async getSession(sid: string): Promise { + async getSession(sid: string): Promise { return this.sessionStore.get(sid); } diff --git a/src/lib/services/strategy-schema.ts b/src/lib/services/strategy-schema.ts index 4c293f32cd..2f593af9b8 100644 --- a/src/lib/services/strategy-schema.ts +++ b/src/lib/services/strategy-schema.ts @@ -1,5 +1,5 @@ -const joi = require('joi'); -const { nameType } = require('../routes/util'); +import { nameType } from '../routes/util'; +import joi from 'joi'; const strategySchema = joi .object() diff --git a/src/lib/services/strategy-service.ts b/src/lib/services/strategy-service.ts index 10dfa7c181..78bb884ead 100644 --- a/src/lib/services/strategy-service.ts +++ b/src/lib/services/strategy-service.ts @@ -16,16 +16,8 @@ import { StrategyReactivatedEvent, StrategyUpdatedEvent, } from '../types'; - -const strategySchema = require('./strategy-schema'); -const NameExistsError = require('../error/name-exists-error'); -const { - STRATEGY_CREATED, - STRATEGY_DELETED, - STRATEGY_DEPRECATED, - STRATEGY_REACTIVATED, - STRATEGY_UPDATED, -} = require('../types/events'); +import strategySchema from './strategy-schema'; +import { NameExistsError } from '../error'; class StrategyService { private logger: Logger; @@ -48,7 +40,7 @@ class StrategyService { return this.strategyStore.getAll(); } - async getStrategy(name: string): Promise { + async getStrategy(name: string): Promise { return this.strategyStore.get(name); } @@ -110,7 +102,7 @@ class StrategyService { async createStrategy( value: IMinimalStrategy, auditUser: IAuditUser, - ): Promise { + ): Promise { const strategy = await strategySchema.validateAsync(value); strategy.deprecated = false; await this._validateStrategyName(strategy); @@ -158,9 +150,9 @@ class StrategyService { } // This check belongs in the store. - _validateEditable(strategy: IStrategy): void { - if (!strategy.editable) { - throw new Error(`Cannot edit strategy ${strategy.name}`); + _validateEditable(strategy: IStrategy | undefined): void { + if (!strategy?.editable) { + throw new Error(`Cannot edit strategy ${strategy?.name}`); } } } diff --git a/src/lib/services/tag-schema.test.ts b/src/lib/services/tag-schema.test.ts index 1cbd88a19b..dca6d8d5d6 100644 --- a/src/lib/services/tag-schema.test.ts +++ b/src/lib/services/tag-schema.test.ts @@ -7,5 +7,8 @@ test('should require url friendly type if defined', () => { }; const { error } = tagSchema.validate(tag); + if (error === undefined) { + fail('Did not receive an expected error'); + } expect(error.details[0].message).toEqual('"type" must be URL friendly'); }); diff --git a/src/lib/services/tag-type-schema.test.ts b/src/lib/services/tag-type-schema.test.ts index 5147c6c2b7..6b7dab2fe6 100644 --- a/src/lib/services/tag-type-schema.test.ts +++ b/src/lib/services/tag-type-schema.test.ts @@ -6,7 +6,7 @@ test('should require a URLFriendly name but allow empty description and icon', ( }; const { error } = tagTypeSchema.validate(simpleTagType); - expect(error.details[0].message).toEqual('"name" must be URL friendly'); + expect(error!.details[0].message).toEqual('"name" must be URL friendly'); }); test('should require a stringy description and icon', () => { @@ -17,8 +17,8 @@ test('should require a stringy description and icon', () => { }; const { error } = tagTypeSchema.validate(tagType); - expect(error.details[0].message).toEqual('"description" must be a string'); - expect(error.details[1].message).toEqual('"icon" must be a string'); + expect(error!.details[0].message).toEqual('"description" must be a string'); + expect(error!.details[1].message).toEqual('"icon" must be a string'); }); test('Should validate if all requirements are fulfilled', () => { diff --git a/src/lib/services/user-service.test.ts b/src/lib/services/user-service.test.ts index 2d3aac8c8f..1465769011 100644 --- a/src/lib/services/user-service.test.ts +++ b/src/lib/services/user-service.test.ts @@ -15,7 +15,6 @@ import SettingService from './setting-service'; import FakeSettingStore from '../../test/fixtures/fake-setting-store'; import { extractAuditInfoFromUser } from '../util'; import { createFakeEventsService } from '../features'; - const config: IUnleashConfig = createTestConfig(); const systemUser = new User({ id: -1, username: 'system' }); @@ -190,9 +189,6 @@ describe('Default admin initialization', () => { process.env.UNLEASH_DEFAULT_ADMIN_USERNAME = CUSTOM_ADMIN_USERNAME; process.env.UNLEASH_DEFAULT_ADMIN_PASSWORD = CUSTOM_ADMIN_PASSWORD; - const createTestConfig = - require('../../test/config/test-config').createTestConfig; - const config = createTestConfig(); expect(config.authentication.initialAdminUser).toStrictEqual({ diff --git a/src/lib/services/user-service.ts b/src/lib/services/user-service.ts index c94cec6e16..32255aabd0 100644 --- a/src/lib/services/user-service.ts +++ b/src/lib/services/user-service.ts @@ -229,8 +229,11 @@ class UserService { async getUser(id: number): Promise { const user = await this.store.get(id); + if (user === undefined) { + throw new NotFoundError(`Could not find user with id ${id}`); + } const rootRole = await this.accessService.getRootRoleForUser(id); - return { ...user, rootRole: rootRole.id }; + return { ...user, id, rootRole: rootRole.id }; } async search(query: string): Promise { @@ -416,7 +419,7 @@ class UserService { async loginUser( usernameOrEmail: string, password: string, - device?: { userAgent: string; ip: string }, + device?: { userAgent?: string; ip: string }, ): Promise { const settings = await this.settingService.get( simpleAuthSettingsKey, @@ -581,7 +584,7 @@ class UserService { return { token, createdBy, - email: user.email, + email: user.email!, name: user.name, id: user.id, role: { @@ -632,7 +635,7 @@ class UserService { const resetLink = await this.resetTokenService.createResetPasswordUrl( receiver.id, - user.username || user.email, + user.username || user.email || SYSTEM_USER_AUDIT.username, ); this.passwordResetTimeouts[receiver.id] = setTimeout(() => { @@ -640,8 +643,8 @@ class UserService { }, 1000 * 60); // 1 minute await this.emailService.sendResetMail( - receiver.name, - receiver.email, + receiver.name!, + receiverEmail, resetLink.toString(), ); return resetLink; diff --git a/src/lib/types/group.ts b/src/lib/types/group.ts index a4b387b425..32ed778bc1 100644 --- a/src/lib/types/group.ts +++ b/src/lib/types/group.ts @@ -1,5 +1,6 @@ import Joi, { ValidationError } from 'joi'; import type { IUser } from './user'; +import { SYSTEM_USER_AUDIT } from './core'; export interface IGroup { id: number; @@ -60,7 +61,7 @@ export interface IGroupModelWithAddedAt extends IGroupModel { export default class Group implements IGroup { type: string; - createdAt: Date; + createdAt?: Date; createdBy: string; @@ -95,9 +96,9 @@ export default class Group implements IGroup { this.id = id; this.name = name; this.rootRole = rootRole; - this.description = description; - this.mappingsSSO = mappingsSSO; - this.createdBy = createdBy; + this.description = description || ''; + this.mappingsSSO = mappingsSSO || []; + this.createdBy = createdBy || SYSTEM_USER_AUDIT.username; this.createdAt = createdAt; this.scimId = scimId; } diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index 8337e89834..d15867d1a4 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -61,7 +61,7 @@ export interface IFeatureStrategy { export interface FeatureToggleDTO { name: string; - description?: string; + description?: string | null; type?: string; stale?: boolean; archived?: boolean; @@ -91,7 +91,7 @@ export interface IFeatureToggleListItem extends FeatureToggle { export interface IFeatureToggleClient { name: string; - description: string; + description: string | undefined | null; type: string; project: string; stale: boolean; diff --git a/src/lib/types/stores/account-store.ts b/src/lib/types/stores/account-store.ts index 5e29ca4a2d..2c18cbfa29 100644 --- a/src/lib/types/stores/account-store.ts +++ b/src/lib/types/stores/account-store.ts @@ -19,7 +19,7 @@ export interface IAccountStore extends Store { getAllWithId(userIdList: number[]): Promise; getByQuery(idQuery: IUserLookup): Promise; count(): Promise; - getAccountByPersonalAccessToken(secret: string): Promise; + getAccountByPersonalAccessToken(secret: string): Promise; markSeenAt(secrets: string[]): Promise; getAdminCount(): Promise; getAdmins(): Promise; diff --git a/src/lib/types/stores/api-token-store.ts b/src/lib/types/stores/api-token-store.ts index 9da92337b6..f5152a98f3 100644 --- a/src/lib/types/stores/api-token-store.ts +++ b/src/lib/types/stores/api-token-store.ts @@ -4,7 +4,7 @@ import type { Store } from './store'; export interface IApiTokenStore extends Store { getAllActive(): Promise; insert(newToken: IApiTokenCreate): Promise; - setExpiry(secret: string, expiresAt: Date): Promise; + setExpiry(secret: string, expiresAt: Date): Promise; markSeenAt(secrets: string[]): Promise; count(): Promise; countByType(): Promise>; diff --git a/src/lib/types/stores/favorite-features.ts b/src/lib/types/stores/favorite-features.ts index 7f6cb66953..de3435aca3 100644 --- a/src/lib/types/stores/favorite-features.ts +++ b/src/lib/types/stores/favorite-features.ts @@ -10,5 +10,5 @@ export interface IFavoriteFeaturesStore extends Store { addFavoriteFeature( favorite: IFavoriteFeatureKey, - ): Promise; + ): Promise; } diff --git a/src/lib/types/stores/favorite-projects.ts b/src/lib/types/stores/favorite-projects.ts index 8b2f5c6b96..26266eff92 100644 --- a/src/lib/types/stores/favorite-projects.ts +++ b/src/lib/types/stores/favorite-projects.ts @@ -10,5 +10,5 @@ export interface IFavoriteProjectsStore extends Store { addFavoriteProject( favorite: IFavoriteProjectKey, - ): Promise; + ): Promise; } diff --git a/src/lib/types/stores/reset-token-store.ts b/src/lib/types/stores/reset-token-store.ts index f041d39bc9..6f541afcb1 100644 --- a/src/lib/types/stores/reset-token-store.ts +++ b/src/lib/types/stores/reset-token-store.ts @@ -4,7 +4,7 @@ export interface IResetTokenCreate { reset_token: string; user_id: number; expires_at: Date; - created_by?: string; + created_by: string; } export interface IResetToken { diff --git a/src/lib/users/inactive/fakes/fake-inactive-users-store.ts b/src/lib/users/inactive/fakes/fake-inactive-users-store.ts index 5f49adc773..bb43c6c524 100644 --- a/src/lib/users/inactive/fakes/fake-inactive-users-store.ts +++ b/src/lib/users/inactive/fakes/fake-inactive-users-store.ts @@ -27,7 +27,7 @@ export class FakeInactiveUsersStore implements IInactiveUsersStore { id: user.id, name: user.name, username: user.username, - email: user.email, + email: user.email!, seen_at: user.seenAt, created_at: user.createdAt || new Date(), }; diff --git a/src/lib/util/anyEventEmitter.test.ts b/src/lib/util/anyEventEmitter.test.ts index 268434a78d..ac7fb74834 100644 --- a/src/lib/util/anyEventEmitter.test.ts +++ b/src/lib/util/anyEventEmitter.test.ts @@ -1,8 +1,8 @@ import { AnyEventEmitter } from './anyEventEmitter'; test('AnyEventEmitter', () => { - const events = []; - const results = []; + const events: string[] = []; + const results: boolean[] = []; class MyEventEmitter extends AnyEventEmitter {} const myEventEmitter = new MyEventEmitter(); diff --git a/src/lib/util/collect-ids.ts b/src/lib/util/collect-ids.ts index f1419d5132..b055d576e0 100644 --- a/src/lib/util/collect-ids.ts +++ b/src/lib/util/collect-ids.ts @@ -1,3 +1,3 @@ -export const collectIds = (items: { id?: T }[]): T[] => { +export const collectIds = (items: { id: T }[]): T[] => { return items.map((item) => item.id); }; diff --git a/src/lib/util/extract-user.ts b/src/lib/util/extract-user.ts index a12ab17d32..4e4b58d4bb 100644 --- a/src/lib/util/extract-user.ts +++ b/src/lib/util/extract-user.ts @@ -1,4 +1,4 @@ -import { SYSTEM_USER } from '../../lib/types'; +import { SYSTEM_USER, SYSTEM_USER_AUDIT } from '../../lib/types'; import type { IApiRequest, IApiUser, @@ -8,7 +8,9 @@ import type { } from '../server-impl'; export function extractUsernameFromUser(user: IUser | IApiUser): string { - return (user as IUser)?.email || user?.username || SYSTEM_USER.username; + return ( + (user as IUser)?.email || user?.username || SYSTEM_USER_AUDIT.username + ); } export function extractUsername(req: IAuthRequest | IApiRequest): string { diff --git a/src/lib/util/snakeCase.ts b/src/lib/util/snakeCase.ts index bc5f85ea0b..3d66370af6 100644 --- a/src/lib/util/snakeCase.ts +++ b/src/lib/util/snakeCase.ts @@ -1,5 +1,5 @@ export const snakeCase = (input: string): string => { - const result = []; + const result: string[] = []; const splitString = input.split(''); for (let i = 0; i < splitString.length; i++) { const char = splitString[i]; diff --git a/src/lib/util/time-utils.ts b/src/lib/util/time-utils.ts index 7b6037dd9a..af310e3bc5 100644 --- a/src/lib/util/time-utils.ts +++ b/src/lib/util/time-utils.ts @@ -7,7 +7,7 @@ export interface HourBucket { export function generateHourBuckets(hours: number): HourBucket[] { const start = startOfHour(new Date()); - const result = []; + const result: HourBucket[] = []; for (let i = 0; i < hours; i++) { result.push({ timestamp: subHours(start, i) }); @@ -19,7 +19,7 @@ export function generateHourBuckets(hours: number): HourBucket[] { export function generateDayBuckets(days: number): HourBucket[] { const start = endOfDay(subDays(new Date(), 1)); - const result = []; + const result: HourBucket[] = []; for (let i = 0; i < days; i++) { result.push({ timestamp: subDays(start, i) }); diff --git a/src/lib/util/validateOrigin.ts b/src/lib/util/validateOrigin.ts index e9ad69705f..5b0a502103 100644 --- a/src/lib/util/validateOrigin.ts +++ b/src/lib/util/validateOrigin.ts @@ -1,4 +1,7 @@ -export const validateOrigin = (origin: string): boolean => { +export const validateOrigin = (origin: string | undefined): boolean => { + if (origin === undefined) { + return false; + } if (origin === '*') { return true; } @@ -9,7 +12,7 @@ export const validateOrigin = (origin: string): boolean => { try { const parsed = new URL(origin); - return parsed.origin && parsed.origin === origin; + return typeof parsed.origin === 'string' && parsed.origin === origin; } catch { return false; } diff --git a/src/lib/util/version.ts b/src/lib/util/version.ts index 35a283f9ec..b44041f926 100644 --- a/src/lib/util/version.ts +++ b/src/lib/util/version.ts @@ -1,6 +1,4 @@ // export module version require('pkginfo')(module, 'version'); - const { version } = module.exports; export default version; -module.exports = version; diff --git a/src/test/config/create-config.test.ts b/src/test/config/create-config.test.ts index 626f949ca4..69868a5279 100644 --- a/src/test/config/create-config.test.ts +++ b/src/test/config/create-config.test.ts @@ -61,8 +61,8 @@ test('should allow setting pool size', () => { disableMigration: false, }; const config = createConfig({ db }); - expect(config.db.pool.min).toBe(min); - expect(config.db.pool.max).toBe(max); + expect(config.db.pool!.min).toBe(min); + expect(config.db.pool!.max).toBe(max); expect(config.db.driver).toBe('postgres'); }); diff --git a/src/test/e2e/api/admin/project/api-token.e2e.test.ts b/src/test/e2e/api/admin/project/api-token.e2e.test.ts index 4bc3c5312f..1dc711d26e 100644 --- a/src/test/e2e/api/admin/project/api-token.e2e.test.ts +++ b/src/test/e2e/api/admin/project/api-token.e2e.test.ts @@ -43,7 +43,7 @@ test('Should always return token type in lowercase', async () => { }); const storedToken = await apiTokenStore.get('some-secret'); - expect(storedToken.type).toBe('frontend'); + expect(storedToken!.type).toBe('frontend'); const { body } = await app.request .get('/api/admin/projects/default/api-tokens') diff --git a/src/test/e2e/api/openapi/openapi.e2e.test.ts b/src/test/e2e/api/openapi/openapi.e2e.test.ts index ffceaa8bb4..cb4f5f4e32 100644 --- a/src/test/e2e/api/openapi/openapi.e2e.test.ts +++ b/src/test/e2e/api/openapi/openapi.e2e.test.ts @@ -187,7 +187,7 @@ test('all tags are listed in the root "tags" list', async () => { // dictionary of all invalid tags found in the spec let invalidTags = {}; for (const [path, data] of Object.entries(spec.paths)) { - for (const [operation, opData] of Object.entries(data)) { + for (const [operation, opData] of Object.entries(data!)) { // ensure that the list of tags for every operation is a subset of // the list of tags defined on the root level @@ -218,7 +218,7 @@ test('all tags are listed in the root "tags" list', async () => { if (Object.keys(invalidTags).length) { // create a human-readable list of invalid tags per operation const msgs = Object.entries(invalidTags).flatMap(([path, data]) => - Object.entries(data).map( + Object.entries(data!).map( ([operation, opData]) => `${operation.toUpperCase()} ${path} (operation id: ${ opData.operationId @@ -247,7 +247,7 @@ test('all API operations have non-empty summaries and descriptions', async () => .expect(200); const anomalies = Object.entries(spec.paths).flatMap(([path, data]) => { - return Object.entries(data) + return Object.entries(data!) .map(([verb, operationDescription]) => { if ( operationDescription.summary && @@ -260,6 +260,7 @@ test('all API operations have non-empty summaries and descriptions', async () => }) .filter(Boolean) .map( + // @ts-expect-error - requesting an iterator where none could be found ([verb, operationId]) => `${verb.toUpperCase()} ${path} (operation ID: ${operationId})`, ); diff --git a/src/test/e2e/helpers/test-helper.ts b/src/test/e2e/helpers/test-helper.ts index 43854fd755..52ede02c03 100644 --- a/src/test/e2e/helpers/test-helper.ts +++ b/src/test/e2e/helpers/test-helper.ts @@ -357,6 +357,7 @@ async function createApp( }, }); const services = createServices(stores, config, db); + // @ts-expect-error We don't have a database for sessions here. const unleashSession = sessionDb(config, undefined); const app = await getApp(config, stores, services, unleashSession, db); const request = supertest.agent(app); @@ -411,6 +412,7 @@ export async function setupAppWithoutSupertest( }, }); const services = createServices(stores, config, db); + // @ts-expect-error we don't have a db for the session here const unleashSession = sessionDb(config, undefined); const app = await getApp(config, stores, services, unleashSession, db); const server = app.listen(0); @@ -453,7 +455,7 @@ export async function setupAppWithAuth( export async function setupAppWithCustomAuth( stores: IUnleashStores, - preHook: Function, + preHook?: Function, // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types customOptions?: any, db?: Db, diff --git a/src/test/e2e/seed/segment.seed.ts b/src/test/e2e/seed/segment.seed.ts index 0c1a53c9f7..f6624641e8 100644 --- a/src/test/e2e/seed/segment.seed.ts +++ b/src/test/e2e/seed/segment.seed.ts @@ -120,7 +120,7 @@ const seedSegmentsDatabase = async ( assert(segments.length === spec.segmentsPerFeature); const addSegment = (feature: IFeatureToggleClient, segment: ISegment) => { - return addSegmentToStrategy(app, segment.id, feature.strategies[0].id); + return addSegmentToStrategy(app, segment.id, feature.strategies[0].id!); }; for (const feature of features) { diff --git a/src/test/e2e/services/playground-service.test.ts b/src/test/e2e/services/playground-service.test.ts index ba10934cb0..09df3087e3 100644 --- a/src/test/e2e/services/playground-service.test.ts +++ b/src/test/e2e/services/playground-service.test.ts @@ -80,6 +80,8 @@ const mapSegmentSchemaToISegment = ( ...segment, name: segment.name || `test-segment ${index ?? 'unnumbered'}`, createdAt: new Date(), + description: '', + project: undefined, }); export const seedDatabaseForPlaygroundTest = async ( @@ -116,11 +118,13 @@ export const seedDatabaseForPlaygroundTest = async ( // create feature const toggle = await database.stores.featureToggleStore.create( - feature.project, + feature.project!, { ...feature, createdAt: undefined, - variants: null, + variants: [], + description: undefined, + impressionData: false, createdByUserId: 9999, }, ); @@ -133,7 +137,7 @@ export const seedDatabaseForPlaygroundTest = async ( ); await database.stores.featureToggleStore.saveVariants( - feature.project, + feature.project!, feature.name, [ ...(feature.variants ?? []).map((variant) => ({ @@ -791,7 +795,7 @@ describe('the playground service (e2e)', () => { unmappedFeature.strategies?.forEach( (unmappedStrategy) => { const mappedStrategySegments: PlaygroundSegmentSchema[] = - strategies[unmappedStrategy.id] + strategies[unmappedStrategy.id!] .segments; const unmappedSegments = @@ -808,7 +812,7 @@ describe('the playground service (e2e)', () => { ).toEqual([...unmappedSegments].sort()); switch ( - strategies[unmappedStrategy.id].result + strategies[unmappedStrategy.id!].result ) { case true: // If a strategy is considered true, _all_ segments @@ -975,7 +979,7 @@ describe('the playground service (e2e)', () => { ...feature, // use a constraint that will never be true strategies: [ - ...feature.strategies.map((strategy) => ({ + ...feature.strategies!.map((strategy) => ({ ...strategy, constraints: [ { diff --git a/src/test/e2e/services/reset-token-service.e2e.test.ts b/src/test/e2e/services/reset-token-service.e2e.test.ts index 2bdafde440..a78bbcca2a 100644 --- a/src/test/e2e/services/reset-token-service.e2e.test.ts +++ b/src/test/e2e/services/reset-token-service.e2e.test.ts @@ -103,7 +103,7 @@ test('Should create a reset link with unleashUrl with context path', async () => const url = await resetToken.createResetPasswordUrl( userIdToCreateResetFor, - adminUser.username, + adminUser.username!, ); expect(url.toString().substring(0, url.toString().indexOf('='))).toBe( `${localConfig.server.unleashUrl}/reset-password?token`, @@ -113,7 +113,7 @@ test('Should create a reset link with unleashUrl with context path', async () => test('Should create a welcome link', async () => { const url = await resetTokenService.createNewUserUrl( userIdToCreateResetFor, - adminUser.username, + adminUser.username!, ); const urlS = url.toString(); expect(urlS.substring(0, urlS.indexOf('='))).toBe( @@ -124,7 +124,7 @@ test('Should create a welcome link', async () => { test('Tokens should be one-time only', async () => { const token = await resetTokenService.createToken( userIdToCreateResetFor, - adminUser.username, + adminUser.username!, ); const accessGranted = await resetTokenService.useAccessToken(token); @@ -136,11 +136,11 @@ test('Tokens should be one-time only', async () => { test('Creating a new token should expire older tokens', async () => { const firstToken = await resetTokenService.createToken( userIdToCreateResetFor, - adminUser.username, + adminUser.username!, ); const secondToken = await resetTokenService.createToken( userIdToCreateResetFor, - adminUser.username, + adminUser.username!, ); await expect(async () => resetTokenService.isValid(firstToken.token), @@ -152,7 +152,7 @@ test('Creating a new token should expire older tokens', async () => { test('Retrieving valid invitation links should retrieve an object with userid key and token value', async () => { const token = await resetTokenService.createToken( userIdToCreateResetFor, - adminUser.username, + adminUser.username!, ); expect(token).toBeTruthy(); const activeInvitations = await resetTokenService.getActiveInvitations(); diff --git a/src/test/e2e/stores/api-token-store.e2e.test.ts b/src/test/e2e/stores/api-token-store.e2e.test.ts index 5574605dbc..647ed44487 100644 --- a/src/test/e2e/stores/api-token-store.e2e.test.ts +++ b/src/test/e2e/stores/api-token-store.e2e.test.ts @@ -37,10 +37,10 @@ test('get token returns the token when exists', async () => { }); const foundToken = await stores.apiTokenStore.get('abcde321'); expect(foundToken).toBeDefined(); - expect(foundToken.secret).toBe(newToken.secret); - expect(foundToken.environment).toBe(newToken.environment); - expect(foundToken.tokenName).toBe(newToken.tokenName); - expect(foundToken.type).toBe(newToken.type); + expect(foundToken!.secret).toBe(newToken.secret); + expect(foundToken!.environment).toBe(newToken.environment); + expect(foundToken!.tokenName).toBe(newToken.tokenName); + expect(foundToken!.type).toBe(newToken.type); }); describe('count deprecated tokens', () => { diff --git a/src/test/e2e/stores/client-application-store.e2e.test.ts b/src/test/e2e/stores/client-application-store.e2e.test.ts index 4b75d35697..97bd28df00 100644 --- a/src/test/e2e/stores/client-application-store.e2e.test.ts +++ b/src/test/e2e/stores/client-application-store.e2e.test.ts @@ -138,8 +138,8 @@ test('Merge keeps value for single row in database', async () => { const stored = await clientApplicationsStore.get( clientRegistration.appName, ); - expect(stored.color).toBe(clientRegistration.color); - expect(stored.description).toBe('new description'); + expect(stored!.color).toBe(clientRegistration.color); + expect(stored!.description).toBe('new description'); }); test('Multi row merge also works', async () => { @@ -171,7 +171,7 @@ test('Multi row merge also works', async () => { clients.map(async (c) => clientApplicationsStore.get(c.appName!)), ); stored.forEach((s, i) => { - expect(s.description).toBe(clients[i].description); - expect(s.icon).toBe('red'); + expect(s!.description).toBe(clients[i].description); + expect(s!.icon).toBe('red'); }); }); diff --git a/src/test/e2e/stores/feature-environment-store.e2e.test.ts b/src/test/e2e/stores/feature-environment-store.e2e.test.ts index 680962b92a..48fb371e86 100644 --- a/src/test/e2e/stores/feature-environment-store.e2e.test.ts +++ b/src/test/e2e/stores/feature-environment-store.e2e.test.ts @@ -124,7 +124,7 @@ test('Copying features also copies variants', async () => { featureName: featureName, environment: 'clone', }); - expect(cloned.variants).toMatchObject([variant]); + expect(cloned!.variants).toMatchObject([variant]); }); test('Copying strategies also copies strategy variants', async () => { diff --git a/src/test/e2e/stores/feature-tag-store.e2e.test.ts b/src/test/e2e/stores/feature-tag-store.e2e.test.ts index 6e60ef654d..099ad6ae9d 100644 --- a/src/test/e2e/stores/feature-tag-store.e2e.test.ts +++ b/src/test/e2e/stores/feature-tag-store.e2e.test.ts @@ -46,8 +46,8 @@ test('should tag feature', async () => { }); expect(featureTags).toHaveLength(1); expect(featureTags[0]).toStrictEqual(tag); - expect(featureTag.featureName).toBe(featureName); - expect(featureTag.tagValue).toBe(tag.value); + expect(featureTag!.featureName).toBe(featureName); + expect(featureTag!.tagValue).toBe(tag.value); }); test('feature tag exists', async () => { diff --git a/src/test/e2e/stores/feature-type-store.e2e.test.ts b/src/test/e2e/stores/feature-type-store.e2e.test.ts index 51c02714e5..2ae4d1b882 100644 --- a/src/test/e2e/stores/feature-type-store.e2e.test.ts +++ b/src/test/e2e/stores/feature-type-store.e2e.test.ts @@ -52,8 +52,8 @@ describe('update lifetimes', () => { ); expect(updated?.lifetimeDays).toBe(newLifetime); - - expect(updated).toMatchObject(await featureTypeStore.get(type.id)); + const fromStore = await featureTypeStore.get(type.id); + expect(updated).toMatchObject(fromStore!); } }); diff --git a/src/test/e2e/stores/user-store.e2e.test.ts b/src/test/e2e/stores/user-store.e2e.test.ts index 230699e41f..21be706b32 100644 --- a/src/test/e2e/stores/user-store.e2e.test.ts +++ b/src/test/e2e/stores/user-store.e2e.test.ts @@ -168,7 +168,7 @@ test('should always lowercase emails on updates', async () => { await store.upsert(user); - let storedUser = await store.getByQuery({ email }); + const storedUser = await store.getByQuery({ email }); expect(storedUser.email).toEqual(user.email.toLowerCase()); @@ -178,8 +178,9 @@ test('should always lowercase emails on updates', async () => { }; await store.upsert(updatedUser); - storedUser = await store.get(storedUser.id); - expect(storedUser.email).toBe(updatedUser.email.toLowerCase()); + const newFromStore = await store.get(storedUser.id); + expect(newFromStore).toBeDefined(); + expect(newFromStore!.email).toBe(updatedUser.email.toLowerCase()); }); test('should delete user', async () => { diff --git a/src/test/fixtures/access-service-mock.ts b/src/test/fixtures/access-service-mock.ts index 323e373b45..729f732513 100644 --- a/src/test/fixtures/access-service-mock.ts +++ b/src/test/fixtures/access-service-mock.ts @@ -13,9 +13,13 @@ class AccessServiceMock extends AccessService { constructor() { super( { + // @ts-expect-error - We're mocking the service so we don't need the store accessStore: undefined, + // @ts-expect-error - We're mocking the service so we don't need the store accountStore: undefined, + // @ts-expect-error - We're mocking the service so we don't need the store roleStore: undefined, + // @ts-expect-error - We're mocking the service so we don't need the store environmentStore: undefined, }, { getLogger: noLoggerProvider }, diff --git a/src/test/fixtures/fake-access-store.ts b/src/test/fixtures/fake-access-store.ts index 737cd24a8a..c6b81dd983 100644 --- a/src/test/fixtures/fake-access-store.ts +++ b/src/test/fixtures/fake-access-store.ts @@ -313,7 +313,7 @@ export class FakeAccessStore implements IAccessStore { getUserAccessOverview(): Promise { throw new Error('Method not implemented.'); } - getRootRoleForUser(userId: number): Promise { + getRootRoleForUser(userId: number): Promise { const roleId = this.userToRoleMap.get(userId); if (roleId !== undefined) { return Promise.resolve(this.fakeRolesStore.get(roleId)); diff --git a/src/test/fixtures/fake-account-store.ts b/src/test/fixtures/fake-account-store.ts index c08a8d5d41..90a2df5ed3 100644 --- a/src/test/fixtures/fake-account-store.ts +++ b/src/test/fixtures/fake-account-store.ts @@ -42,7 +42,7 @@ export class FakeAccountStore implements IAccountStore { return this.data.length; } - async get(key: number): Promise { + async get(key: number): Promise { return this.data.find((u) => u.id === key); } @@ -85,7 +85,9 @@ export class FakeAccountStore implements IAccountStore { } // eslint-disable-next-line @typescript-eslint/no-unused-vars - getAccountByPersonalAccessToken(secret: string): Promise { + getAccountByPersonalAccessToken( + secret: string, + ): Promise { return Promise.resolve(undefined); } diff --git a/src/test/fixtures/fake-api-token-store.ts b/src/test/fixtures/fake-api-token-store.ts index 6c7624c820..e17ad819f5 100644 --- a/src/test/fixtures/fake-api-token-store.ts +++ b/src/test/fixtures/fake-api-token-store.ts @@ -74,7 +74,10 @@ export default class FakeApiTokenStore }); } - async setExpiry(secret: string, expiresAt: Date): Promise { + async setExpiry( + secret: string, + expiresAt: Date, + ): Promise { const found = this.tokens.find((t) => t.secret === secret); if (!found) { return undefined; diff --git a/src/test/fixtures/fake-client-applications-store.ts b/src/test/fixtures/fake-client-applications-store.ts index 1de0822097..41998bded6 100644 --- a/src/test/fixtures/fake-client-applications-store.ts +++ b/src/test/fixtures/fake-client-applications-store.ts @@ -75,7 +75,7 @@ export default class FakeClientApplicationsStore } async upsert(details: Partial): Promise { - await this.delete(details.appName); + await this.delete(details.appName!!); return this.bulkUpsert([details]); } diff --git a/src/test/fixtures/fake-favorite-features-store.ts b/src/test/fixtures/fake-favorite-features-store.ts index d6043ad942..962777d952 100644 --- a/src/test/fixtures/fake-favorite-features-store.ts +++ b/src/test/fixtures/fake-favorite-features-store.ts @@ -7,7 +7,7 @@ export default class FakeFavoriteFeaturesStore { addFavoriteFeature( favorite: IFavoriteFeatureKey, - ): Promise { + ): Promise { return Promise.resolve(undefined); } @@ -25,7 +25,7 @@ export default class FakeFavoriteFeaturesStore return Promise.resolve(false); } - get(key: IFavoriteFeatureKey): Promise { + get(key: IFavoriteFeatureKey): Promise { return Promise.resolve(undefined); } diff --git a/src/test/fixtures/fake-favorite-projects-store.ts b/src/test/fixtures/fake-favorite-projects-store.ts index d3508b6db3..d094f909b5 100644 --- a/src/test/fixtures/fake-favorite-projects-store.ts +++ b/src/test/fixtures/fake-favorite-projects-store.ts @@ -7,7 +7,7 @@ export default class FakeFavoriteProjectsStore { addFavoriteProject( favorite: IFavoriteProjectKey, - ): Promise { + ): Promise { return Promise.resolve(undefined); } @@ -25,7 +25,7 @@ export default class FakeFavoriteProjectsStore return Promise.resolve(false); } - get(key: IFavoriteProjectKey): Promise { + get(key: IFavoriteProjectKey): Promise { return Promise.resolve(undefined); } diff --git a/src/test/fixtures/fake-feature-tag-store.ts b/src/test/fixtures/fake-feature-tag-store.ts index 8bd781f49f..3216dec388 100644 --- a/src/test/fixtures/fake-feature-tag-store.ts +++ b/src/test/fixtures/fake-feature-tag-store.ts @@ -38,7 +38,7 @@ export default class FakeFeatureTagStore implements IFeatureTagStore { return this.featureTags.some((t) => t === key); } - async get(key: IFeatureTag): Promise { + async get(key: IFeatureTag): Promise { return this.featureTags.find((t) => t === key); } @@ -78,7 +78,7 @@ export default class FakeFeatureTagStore implements IFeatureTagStore { value: fT.tagValue, type: fT.tagType, }, - fT.createdByUserId, + fT.createdByUserId || -1337, ); return { featureName: fT.featureName, diff --git a/src/test/fixtures/fake-group-store.ts b/src/test/fixtures/fake-group-store.ts index 8b64d8c696..6cfc62d3f8 100644 --- a/src/test/fixtures/fake-group-store.ts +++ b/src/test/fixtures/fake-group-store.ts @@ -39,7 +39,7 @@ export default class FakeGroupStore implements IGroupStore { return this.data.some((u) => u.id === key); } - async get(key: number): Promise { + async get(key: number): Promise { return this.data.find((u) => u.id === key); } diff --git a/src/test/fixtures/fake-project-store.ts b/src/test/fixtures/fake-project-store.ts index 7338bc6575..178fa0b01c 100644 --- a/src/test/fixtures/fake-project-store.ts +++ b/src/test/fixtures/fake-project-store.ts @@ -17,7 +17,9 @@ import type { ProjectEnvironment, } from '../../lib/features/project/project-store-type'; -type ArchivableProject = IProject & { archivedAt: null | Date }; +type ArchivableProject = Omit & { + archivedAt: null | Date; +}; export default class FakeProjectStore implements IProjectStore { projects: ArchivableProject[] = []; @@ -56,7 +58,7 @@ export default class FakeProjectStore implements IProjectStore { archivedAt: null, }; this.projects.push(newProj); - return newProj; + return newProj as IProject; } async create(project: IProjectInsert): Promise { @@ -95,13 +97,15 @@ export default class FakeProjectStore implements IProjectStore { async get(key: string): Promise { const project = this.projects.find((p) => p.id === key); if (project) { - return project; + return project as IProject; } throw new NotFoundError(`Could not find project with id: ${key}`); } async getAll(): Promise { - return this.projects.filter((project) => project.archivedAt === null); + return this.projects + .filter((project) => project.archivedAt === null) + .map((p) => p as IProject); } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/test/fixtures/fake-public-signup-store.ts b/src/test/fixtures/fake-public-signup-store.ts index e6c3624442..52e6be7248 100644 --- a/src/test/fixtures/fake-public-signup-store.ts +++ b/src/test/fixtures/fake-public-signup-store.ts @@ -1,24 +1,34 @@ import type { IPublicSignupTokenStore } from '../../lib/types/stores/public-signup-token-store'; import type { PublicSignupTokenSchema } from '../../lib/openapi/spec/public-signup-token-schema'; import type { IPublicSignupTokenCreate } from '../../lib/types/models/public-signup-token'; +import { NotFoundError } from '../../lib/error'; export default class FakePublicSignupStore implements IPublicSignupTokenStore { tokens: PublicSignupTokenSchema[] = []; async addTokenUser(secret: string, userId: number): Promise { - this.get(secret).then((token) => token.users.push({ id: userId })); + this.get(secret).then((token) => { + if (token !== undefined) { + token.users?.push({ id: userId }); + } + }); return Promise.resolve(); } async get(secret: string): Promise { const token = this.tokens.find((t) => t.secret === secret); + if (!token) { + throw new NotFoundError('Could not find token'); + } return Promise.resolve(token); } async isValid(secret: string): Promise { const token = this.tokens.find((t) => t.secret === secret); return Promise.resolve( - token && new Date(token.expiresAt) > new Date() && token.enabled, + token !== undefined && + new Date(token.expiresAt) > new Date() && + token.enabled, ); } diff --git a/src/test/fixtures/fake-reset-token-store.ts b/src/test/fixtures/fake-reset-token-store.ts index 349c9984c1..89e8cd8f04 100644 --- a/src/test/fixtures/fake-reset-token-store.ts +++ b/src/test/fixtures/fake-reset-token-store.ts @@ -27,7 +27,7 @@ export default class FakeResetTokenStore implements IResetTokenStore { userId: newToken.user_id, token: newToken.reset_token, expiresAt: newToken.expires_at, - createdBy: newToken.created_by, + createdBy: newToken.created_by || 'system-user', createdAt: new Date(), }; this.data.push(token); @@ -72,7 +72,11 @@ export default class FakeResetTokenStore implements IResetTokenStore { } async get(token: string): Promise { - return this.data.find((t) => t.token === token); + const foundToken = this.data.find((t) => t.token === token); + if (foundToken === undefined) { + throw new NotFoundError('Could find token'); + } + return Promise.resolve(foundToken); } async getActiveTokens(): Promise { @@ -85,11 +89,11 @@ export default class FakeResetTokenStore implements IResetTokenStore { } async useToken(token: IResetQuery): Promise { - if (this.exists(token.token)) { + if (await this.exists(token.token)) { const d = this.data.find( (t) => t.usedAt === null && t.token === token.token, ); - d.usedAt = new Date(); + d!!.usedAt = new Date(); return true; } return false; diff --git a/src/test/fixtures/fake-session-store.ts b/src/test/fixtures/fake-session-store.ts index f28bac3c6e..e0f5251723 100644 --- a/src/test/fixtures/fake-session-store.ts +++ b/src/test/fixtures/fake-session-store.ts @@ -43,8 +43,8 @@ export default class FakeSessionStore implements ISessionStore { ); } - async get(sid: string): Promise { - return this.sessions.find((s) => s.sid === sid); + async get(sid: string): Promise { + return Promise.resolve(this.sessions.find((s) => s.sid === sid)); } async insertSession(data: Omit): Promise { diff --git a/src/test/fixtures/fake-user-feedback-store.ts b/src/test/fixtures/fake-user-feedback-store.ts index dcc97604c2..e37dee8aa0 100644 --- a/src/test/fixtures/fake-user-feedback-store.ts +++ b/src/test/fixtures/fake-user-feedback-store.ts @@ -1,3 +1,4 @@ +import NotImplementedError from '../../lib/error/not-implemented-error'; import type { IUserFeedback, IUserFeedbackKey, @@ -23,7 +24,7 @@ export default class FakeUserFeedbackStore implements IUserFeedbackStore { // eslint-disable-next-line @typescript-eslint/no-unused-vars get(key: IUserFeedbackKey): Promise { - return Promise.resolve(undefined); + throw new NotImplementedError('This is not implemented yet'); } getAll(): Promise { @@ -37,11 +38,11 @@ export default class FakeUserFeedbackStore implements IUserFeedbackStore { // eslint-disable-next-line @typescript-eslint/no-unused-vars getFeedback(userId: number, feedbackId: string): Promise { - return Promise.resolve(undefined); + throw new NotImplementedError('This is not implemented yet'); } // eslint-disable-next-line @typescript-eslint/no-unused-vars updateFeedback(feedback: IUserFeedback): Promise { - return Promise.resolve(undefined); + throw new NotImplementedError('This is not implemented yet'); } } diff --git a/src/test/fixtures/fake-user-splash-store.ts b/src/test/fixtures/fake-user-splash-store.ts index e782c4cc6b..4cfbe16851 100644 --- a/src/test/fixtures/fake-user-splash-store.ts +++ b/src/test/fixtures/fake-user-splash-store.ts @@ -1,3 +1,4 @@ +import NotImplementedError from '../../lib/error/not-implemented-error'; import type { IUserSplashKey, IUserSplash, @@ -12,12 +13,12 @@ export default class FakeUserSplashStore implements IUserSplashStore { // eslint-disable-next-line @typescript-eslint/no-unused-vars getSplash(userId: number, splashId: string): Promise { - return Promise.resolve(undefined); + throw new NotImplementedError('This is not implemented yet'); } // eslint-disable-next-line @typescript-eslint/no-unused-vars updateSplash(splash: IUserSplash): Promise { - return Promise.resolve(undefined); + throw new NotImplementedError('This is not implemented yet'); } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -27,7 +28,7 @@ export default class FakeUserSplashStore implements IUserSplashStore { // eslint-disable-next-line @typescript-eslint/no-unused-vars get(key: IUserSplashKey): Promise { - return Promise.resolve(undefined); + throw new NotImplementedError('This is not implemented yet'); } // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/test/fixtures/no-logger.ts b/src/test/fixtures/no-logger.ts index d09cd7d0b7..151335fec6 100644 --- a/src/test/fixtures/no-logger.ts +++ b/src/test/fixtures/no-logger.ts @@ -3,7 +3,7 @@ import type { Logger } from '../../lib/logger'; let muteError = false; let verbose = false; -function noLoggerProvider(): Logger { +export function noLoggerProvider(): Logger { // do something with the name return { debug: verbose ? console.log : () => {}, diff --git a/yarn.lock b/yarn.lock index fe8969bfa9..afccfb1602 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9224,23 +9224,23 @@ __metadata: languageName: node linkType: hard -"typescript@npm:5.4.5": - version: 5.4.5 - resolution: "typescript@npm:5.4.5" +"typescript@npm:5.8.2": + version: 5.8.2 + resolution: "typescript@npm:5.8.2" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/2954022ada340fd3d6a9e2b8e534f65d57c92d5f3989a263754a78aba549f7e6529acc1921913560a4b816c46dce7df4a4d29f9f11a3dc0d4213bb76d043251e + checksum: 10c0/5c4f6fbf1c6389b6928fe7b8fcd5dc73bb2d58cd4e3883f1d774ed5bd83b151cbac6b7ecf11723de56d4676daeba8713894b1e9af56174f2f9780ae7848ec3c6 languageName: node linkType: hard -"typescript@patch:typescript@npm%3A5.4.5#optional!builtin": - version: 5.4.5 - resolution: "typescript@patch:typescript@npm%3A5.4.5#optional!builtin::version=5.4.5&hash=5adc0c" +"typescript@patch:typescript@npm%3A5.8.2#optional!builtin": + version: 5.8.2 + resolution: "typescript@patch:typescript@npm%3A5.8.2#optional!builtin::version=5.8.2&hash=5786d5" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/db2ad2a16ca829f50427eeb1da155e7a45e598eec7b086d8b4e8ba44e5a235f758e606d681c66992230d3fc3b8995865e5fd0b22a2c95486d0b3200f83072ec9 + checksum: 10c0/5448a08e595cc558ab321e49d4cac64fb43d1fa106584f6ff9a8d8e592111b373a995a1d5c7f3046211c8a37201eb6d0f1566f15cdb7a62a5e3be01d087848e2 languageName: node linkType: hard @@ -9426,7 +9426,7 @@ __metadata: ts-toolbelt: "npm:^9.6.0" tsc-watch: "npm:6.2.1" type-is: "npm:^1.6.18" - typescript: "npm:5.4.5" + typescript: "npm:5.8.2" unleash-client: "npm:^6.6.0" uuid: "npm:^9.0.0" wait-on: "npm:^7.2.0"