mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	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 :)
This commit is contained in:
		
							parent
							
								
									d082e5eb25
								
							
						
					
					
						commit
						efcf04487d
					
				
							
								
								
									
										12
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								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": { | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -34,7 +34,7 @@ interface ISlackAppAddonParameters { | ||||
| export default class SlackAppAddon extends Addon { | ||||
|     private msgFormatter: FeatureEventFormatter; | ||||
| 
 | ||||
|     flagResolver: IFlagResolver; | ||||
|     declare flagResolver: IFlagResolver; | ||||
| 
 | ||||
|     private accessToken?: string; | ||||
| 
 | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -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')) { | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -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); | ||||
|         }, | ||||
|  | ||||
| @ -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') { | ||||
|  | ||||
| @ -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<IImportOption, 'file'> = { | ||||
| @ -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) || {}, | ||||
|  | ||||
| @ -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', | ||||
|  | ||||
| @ -153,6 +153,7 @@ export class PublicSignupTokenStore implements IPublicSignupTokenStore { | ||||
|         newToken: IPublicSignupTokenCreate, | ||||
|     ): Promise<PublicSignupTokenSchema> { | ||||
|         const response = await this.db<ITokenRow>(TABLE).insert( | ||||
|             // @ts-expect-error - knex expects us to return a DbRecordArr<OurType>, we return OurType, which works fine.
 | ||||
|             toRow(newToken), | ||||
|             ['secret'], | ||||
|         ); | ||||
|  | ||||
| @ -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<void> { | ||||
|  | ||||
| @ -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<void> { | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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: [], | ||||
|  | ||||
| @ -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 || []), | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| @ -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<IContextField> { | ||||
|         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, | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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, | ||||
|                     }, | ||||
|  | ||||
| @ -48,12 +48,14 @@ export class ImportPermissionsService { | ||||
|     ): Promise<ContextFieldSchema[]> { | ||||
|         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, | ||||
|                     ), | ||||
|             ) || [] | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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) { | ||||
|  | ||||
| @ -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!, | ||||
|                         }; | ||||
|                     }), | ||||
|                 ), | ||||
|             }, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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<IFeatureToggleListItem> = 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; | ||||
|  | ||||
| @ -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<IFeatureStrategy> { | ||||
|     async get(id: string): Promise<IFeatureStrategy | undefined> { | ||||
|         return this.featureStrategies.find((s) => s.id === id); | ||||
|     } | ||||
| 
 | ||||
| @ -180,8 +180,8 @@ export default class FakeFeatureStrategiesStore | ||||
|         archived: boolean = false, | ||||
|     ): Promise<IFeatureToggleClient[]> { | ||||
|         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( | ||||
|  | ||||
| @ -87,8 +87,11 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore { | ||||
|         return this.features.filter((f) => names.includes(f.name)); | ||||
|     } | ||||
| 
 | ||||
|     async getProjectId(name: string): Promise<string> { | ||||
|         return this.get(name).then((f) => f.project); | ||||
|     async getProjectId(name: string | undefined): Promise<string | undefined> { | ||||
|         if (name === undefined) { | ||||
|             return Promise.resolve(undefined); | ||||
|         } | ||||
|         return Promise.resolve(this.get(name).then((f) => f.project)); | ||||
|     } | ||||
| 
 | ||||
|     private getFilterQuery(query: Partial<IFeatureToggleStoreQuery>) { | ||||
| @ -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() | ||||
|             ) { | ||||
|  | ||||
| @ -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 }), | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -294,7 +294,9 @@ class FeatureToggleService { | ||||
|         project: string, | ||||
|     ): Promise<void> { | ||||
|         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<Saved<IStrategyConfig>> { | ||||
|         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<void> { | ||||
|         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<FeatureToggle> { | ||||
|         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<FeatureNameCheckResultWithFeaturePattern> { | ||||
|         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<any> { | ||||
|         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<void> { | ||||
|         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<void> { | ||||
|         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( | ||||
|  | ||||
| @ -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<FeaturesTable, 'created_by_user_id'> { | ||||
|         const row = { | ||||
|             name: data.name, | ||||
|             description: data.description, | ||||
|             description: data.description || null, | ||||
|             type: data.type, | ||||
|             project, | ||||
|             archived_at: data.archived ? new Date() : null, | ||||
|  | ||||
| @ -23,7 +23,7 @@ export interface IFeatureToggleStore extends Store<FeatureToggle, string> { | ||||
| 
 | ||||
|     setLastSeen(data: LastSeenInput[]): Promise<void>; | ||||
| 
 | ||||
|     getProjectId(name: string): Promise<string | undefined>; | ||||
|     getProjectId(name: string | undefined): Promise<string | undefined>; | ||||
| 
 | ||||
|     create(project: string, data: FeatureToggleInsert): Promise<FeatureToggle>; | ||||
| 
 | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
| @ -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<ApiUser['secret'], Promise<Unleash>> = | ||||
|         new Map(); | ||||
| 
 | ||||
|     private cachedFrontendSettings?: FrontendSettings; | ||||
|     private cachedFrontendSettings: FrontendSettings; | ||||
| 
 | ||||
|     constructor( | ||||
|         config: Config, | ||||
| @ -228,9 +228,12 @@ export class FrontendApiService { | ||||
|     async fetchFrontendSettings(): Promise<FrontendSettings> { | ||||
|         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); | ||||
|         } | ||||
|  | ||||
| @ -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, | ||||
|                     }), | ||||
|                 ]), | ||||
|             ), | ||||
|         ]); | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -20,7 +20,7 @@ export interface IClientMetricsEnvVariant extends IClientMetricsEnvKey { | ||||
| 
 | ||||
| export interface IClientMetricsStoreV2 | ||||
|     extends Store<IClientMetricsEnv, IClientMetricsEnvKey> { | ||||
|     batchInsertMetrics(metrics: IClientMetricsEnv[]): Promise<void>; | ||||
|     batchInsertMetrics(metrics: IClientMetricsEnv[] | undefined): Promise<void>; | ||||
|     getMetricsForFeatureToggle( | ||||
|         featureName: string, | ||||
|         hoursBack?: number, | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -27,6 +27,7 @@ export default class RemoteAddressStrategy extends Strategy { | ||||
|                         return false; | ||||
|                     } | ||||
|                 } | ||||
|                 return false; | ||||
|             }, | ||||
|         ); | ||||
|     } | ||||
|  | ||||
| @ -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) | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -71,14 +71,20 @@ export default class EnvironmentService { | ||||
|     } | ||||
| 
 | ||||
|     async get(name: string): Promise<IEnvironment> { | ||||
|         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<IProjectsAvailableOnEnvironment[]> { | ||||
|         // 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( | ||||
|  | ||||
| @ -65,7 +65,7 @@ export default class FakeEnvironmentStore implements IEnvironmentStore { | ||||
|     ): Promise<IEnvironment> { | ||||
|         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<void> { | ||||
|         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<void> { | ||||
|         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<IEnvironment> { | ||||
|         return this.environments.find((e) => e.name === key); | ||||
|     async get(key: string): Promise<IEnvironment | undefined> { | ||||
|         return Promise.resolve(this.environments.find((e) => e.name === key)); | ||||
|     } | ||||
| 
 | ||||
|     async getAllWithCounts(): Promise<IEnvironment[]> { | ||||
|  | ||||
| @ -112,7 +112,7 @@ export class ProjectInsightsService { | ||||
|         ]); | ||||
| 
 | ||||
|         return { | ||||
|             health: project.health || 0, | ||||
|             health: project?.health || 0, | ||||
|             features: features, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| @ -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 () => { | ||||
|  | ||||
| @ -261,7 +261,11 @@ export default class ProjectService { | ||||
|     } | ||||
| 
 | ||||
|     async getProject(id: string): Promise<IProject> { | ||||
|         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<any> { | ||||
|         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, | ||||
|  | ||||
| @ -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 () => { | ||||
|  | ||||
| @ -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)]; | ||||
| 
 | ||||
|  | ||||
| @ -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<ISegment> { | ||||
|         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<ISegment[]> { | ||||
| @ -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<void> { | ||||
|         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( | ||||
|  | ||||
| @ -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[] = []; | ||||
|  | ||||
| @ -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<ITagType> { | ||||
|         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( | ||||
|  | ||||
| @ -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 () => { | ||||
|  | ||||
| @ -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', | ||||
|             }); | ||||
|         }, | ||||
|     ); | ||||
| 
 | ||||
|  | ||||
| @ -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( | ||||
|  | ||||
| @ -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( | ||||
|  | ||||
| @ -33,6 +33,7 @@ export const validateSchema = <S = SchemaId>( | ||||
|     schema: S, | ||||
|     data: unknown, | ||||
| ): ISchemaValidationErrors<S> | 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, | ||||
|  | ||||
| @ -253,7 +253,7 @@ class StrategyController extends Controller { | ||||
|             res, | ||||
|             strategySchema.$id, | ||||
|             strategy, | ||||
|             { location: `strategies/${strategy.name}` }, | ||||
|             { location: `strategies/${strategy!.name}` }, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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<IUnleashServices, 'openApiService'>, | ||||
|     ) { | ||||
|         super(config); | ||||
|         this.config = config; | ||||
|         this.openApiService = openApiService; | ||||
| 
 | ||||
|         this.route({ | ||||
|  | ||||
| @ -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( | ||||
|  | ||||
| @ -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<IRoleWithPermissions> { | ||||
|         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<void> { | ||||
|         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 | ||||
|  | ||||
| @ -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<IUser> { | ||||
|         return this.store.getAccountByPersonalAccessToken(secret); | ||||
|         const account = | ||||
|             await this.store.getAccountByPersonalAccessToken(secret); | ||||
|         if (account === undefined) { | ||||
|             throw new NotFoundError(); | ||||
|         } | ||||
|         return account; | ||||
|     } | ||||
| 
 | ||||
|     async getAdminCount(): Promise<IAdminCount> { | ||||
|  | ||||
| @ -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<IAddon> { | ||||
|         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<IAddon> { | ||||
|         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<void> { | ||||
|         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<boolean> { | ||||
|         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( | ||||
|  | ||||
| @ -127,7 +127,7 @@ export class ApiTokenService { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async getToken(secret: string): Promise<IApiToken> { | ||||
|     async getToken(secret: string): Promise<IApiToken | undefined> { | ||||
|         return this.store.get(secret); | ||||
|     } | ||||
| 
 | ||||
| @ -245,7 +245,7 @@ export class ApiTokenService { | ||||
|         auditUser: IAuditUser, | ||||
|     ): Promise<IApiToken> { | ||||
|         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<void> { | ||||
|         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({ | ||||
|  | ||||
| @ -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<IFavoriteProject> { | ||||
|         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<void> { | ||||
|         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<boolean> { | ||||
|  | ||||
| @ -65,6 +65,9 @@ class FeatureTagService { | ||||
|         auditUser: IAuditUser, | ||||
|     ): Promise<ITag> { | ||||
|         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<void> { | ||||
|         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); | ||||
|  | ||||
| @ -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<IGroupModel> { | ||||
|         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<boolean> { | ||||
|         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<void> { | ||||
|         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, | ||||
|         ]); | ||||
|  | ||||
| @ -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<PublicSignupTokenSchema> { | ||||
|         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<PublicSignupTokenSchema[]> { | ||||
| @ -95,6 +100,9 @@ export class PublicSignupTokenService { | ||||
|         auditUser: IAuditUser, | ||||
|     ): Promise<IUser> { | ||||
|         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, | ||||
|  | ||||
| @ -35,7 +35,7 @@ export default class SessionService { | ||||
|         return this.sessionStore.getSessionsForUser(userId); | ||||
|     } | ||||
| 
 | ||||
|     async getSession(sid: string): Promise<ISession> { | ||||
|     async getSession(sid: string): Promise<ISession | undefined> { | ||||
|         return this.sessionStore.get(sid); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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() | ||||
|  | ||||
| @ -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<IStrategy> { | ||||
|     async getStrategy(name: string): Promise<IStrategy | undefined> { | ||||
|         return this.strategyStore.get(name); | ||||
|     } | ||||
| 
 | ||||
| @ -110,7 +102,7 @@ class StrategyService { | ||||
|     async createStrategy( | ||||
|         value: IMinimalStrategy, | ||||
|         auditUser: IAuditUser, | ||||
|     ): Promise<IStrategy> { | ||||
|     ): Promise<IStrategy | undefined> { | ||||
|         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}`); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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'); | ||||
| }); | ||||
|  | ||||
| @ -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', () => { | ||||
|  | ||||
| @ -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({ | ||||
|  | ||||
| @ -229,8 +229,11 @@ class UserService { | ||||
| 
 | ||||
|     async getUser(id: number): Promise<IUserWithRootRole> { | ||||
|         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<IUser[]> { | ||||
| @ -416,7 +419,7 @@ class UserService { | ||||
|     async loginUser( | ||||
|         usernameOrEmail: string, | ||||
|         password: string, | ||||
|         device?: { userAgent: string; ip: string }, | ||||
|         device?: { userAgent?: string; ip: string }, | ||||
|     ): Promise<IUser> { | ||||
|         const settings = await this.settingService.get<SimpleAuthSettings>( | ||||
|             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; | ||||
|  | ||||
| @ -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; | ||||
|     } | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -19,7 +19,7 @@ export interface IAccountStore extends Store<IUser, number> { | ||||
|     getAllWithId(userIdList: number[]): Promise<IUser[]>; | ||||
|     getByQuery(idQuery: IUserLookup): Promise<IUser>; | ||||
|     count(): Promise<number>; | ||||
|     getAccountByPersonalAccessToken(secret: string): Promise<IUser>; | ||||
|     getAccountByPersonalAccessToken(secret: string): Promise<IUser | undefined>; | ||||
|     markSeenAt(secrets: string[]): Promise<void>; | ||||
|     getAdminCount(): Promise<IAdminCount>; | ||||
|     getAdmins(): Promise<MinimalUser[]>; | ||||
|  | ||||
| @ -4,7 +4,7 @@ import type { Store } from './store'; | ||||
| export interface IApiTokenStore extends Store<IApiToken, string> { | ||||
|     getAllActive(): Promise<IApiToken[]>; | ||||
|     insert(newToken: IApiTokenCreate): Promise<IApiToken>; | ||||
|     setExpiry(secret: string, expiresAt: Date): Promise<IApiToken>; | ||||
|     setExpiry(secret: string, expiresAt: Date): Promise<IApiToken | undefined>; | ||||
|     markSeenAt(secrets: string[]): Promise<void>; | ||||
|     count(): Promise<number>; | ||||
|     countByType(): Promise<Map<string, number>>; | ||||
|  | ||||
| @ -10,5 +10,5 @@ export interface IFavoriteFeaturesStore | ||||
|     extends Store<IFavoriteFeature, IFavoriteFeatureKey> { | ||||
|     addFavoriteFeature( | ||||
|         favorite: IFavoriteFeatureKey, | ||||
|     ): Promise<IFavoriteFeature>; | ||||
|     ): Promise<IFavoriteFeature | undefined>; | ||||
| } | ||||
|  | ||||
| @ -10,5 +10,5 @@ export interface IFavoriteProjectsStore | ||||
|     extends Store<IFavoriteProject, IFavoriteProjectKey> { | ||||
|     addFavoriteProject( | ||||
|         favorite: IFavoriteProjectKey, | ||||
|     ): Promise<IFavoriteProject>; | ||||
|     ): Promise<IFavoriteProject | undefined>; | ||||
| } | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
| @ -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(), | ||||
|                     }; | ||||
|  | ||||
| @ -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(); | ||||
|  | ||||
| @ -1,3 +1,3 @@ | ||||
| export const collectIds = <T>(items: { id?: T }[]): T[] => { | ||||
| export const collectIds = <T>(items: { id: T }[]): T[] => { | ||||
|     return items.map((item) => item.id); | ||||
| }; | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
| @ -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]; | ||||
|  | ||||
| @ -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) }); | ||||
|  | ||||
| @ -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; | ||||
|     } | ||||
|  | ||||
| @ -1,6 +1,4 @@ | ||||
| // export module version
 | ||||
| require('pkginfo')(module, 'version'); | ||||
| 
 | ||||
| const { version } = module.exports; | ||||
| export default version; | ||||
| module.exports = version; | ||||
|  | ||||
| @ -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'); | ||||
| }); | ||||
| 
 | ||||
|  | ||||
| @ -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') | ||||
|  | ||||
| @ -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})`, | ||||
|             ); | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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) { | ||||
|  | ||||
| @ -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: [ | ||||
|                                             { | ||||
|  | ||||
| @ -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(); | ||||
|  | ||||
| @ -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', () => { | ||||
|  | ||||
| @ -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'); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| @ -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 () => { | ||||
|  | ||||
| @ -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 () => { | ||||
|  | ||||
| @ -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!); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|  | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue
	
	Block a user