1
0
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:
Christopher Kolstad 2025-03-19 10:01:49 +01:00 committed by GitHub
parent d082e5eb25
commit efcf04487d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
118 changed files with 577 additions and 310 deletions

View File

@ -35,21 +35,21 @@
"scripts": { "scripts": {
"start": "TZ=UTC node ./dist/server.js", "start": "TZ=UTC node ./dist/server.js",
"copy-templates": "copyfiles -u 1 src/mailtemplates/**/* dist/", "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": "yarn --cwd ./frontend run build",
"build:frontend:if-needed": "./scripts/build-frontend-if-needed.sh", "build:frontend:if-needed": "./scripts/build-frontend-if-needed.sh",
"build": "yarn run clean && concurrently \"yarn:copy-templates\" \"yarn:build:frontend\" \"yarn:build:backend\"", "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": "wait-on tcp:4242 && yarn --cwd ./frontend run dev",
"dev:frontend:cloud": "UNLEASH_BASE_PATH=/demo/ yarn run dev:frontend", "dev:frontend:cloud": "UNLEASH_BASE_PATH=/demo/ yarn run dev:frontend",
"dev": "concurrently \"yarn:dev:backend\" \"yarn:dev:frontend\"", "dev": "concurrently \"yarn:dev:backend\" \"yarn:dev:frontend\"",
"prepare:backend": "concurrently \"yarn:copy-templates\" \"yarn:build:backend\"", "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", "db-migrate": "db-migrate --migrations-dir ./src/migrations",
"lint": "biome check .", "lint": "biome check .",
"lint:fix": "biome check . --write", "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", "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", "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": "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", "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": "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: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", "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", "seed:serve": "UNLEASH_DATABASE_NAME=unleash_test UNLEASH_DATABASE_SCHEMA=seed yarn run start:dev",
"clean": "del-cli --force dist", "clean": "del-cli --force dist",
"heroku-postbuild": "cd frontend && yarn && yarn build", "heroku-postbuild": "cd frontend && yarn && yarn build",
@ -220,7 +220,7 @@
"supertest": "7.0.0", "supertest": "7.0.0",
"ts-node": "10.9.2", "ts-node": "10.9.2",
"tsc-watch": "6.2.1", "tsc-watch": "6.2.1",
"typescript": "5.4.5", "typescript": "5.8.2",
"wait-on": "^7.2.0" "wait-on": "^7.2.0"
}, },
"resolutions": { "resolutions": {

View File

@ -32,7 +32,7 @@ interface DDRequestBody {
export default class DatadogAddon extends Addon { export default class DatadogAddon extends Addon {
private msgFormatter: FeatureEventFormatter; private msgFormatter: FeatureEventFormatter;
flagResolver: IFlagResolver; declare flagResolver: IFlagResolver;
constructor(config: IAddonConfig) { constructor(config: IAddonConfig) {
super(definition, config); super(definition, config);

View File

@ -39,7 +39,7 @@ interface INewRelicRequestBody {
export default class NewRelicAddon extends Addon { export default class NewRelicAddon extends Addon {
private msgFormatter: FeatureEventFormatter; private msgFormatter: FeatureEventFormatter;
flagResolver: IFlagResolver; declare flagResolver: IFlagResolver;
constructor(config: IAddonConfig) { constructor(config: IAddonConfig) {
super(definition, config); super(definition, config);

View File

@ -34,7 +34,7 @@ interface ISlackAppAddonParameters {
export default class SlackAppAddon extends Addon { export default class SlackAppAddon extends Addon {
private msgFormatter: FeatureEventFormatter; private msgFormatter: FeatureEventFormatter;
flagResolver: IFlagResolver; declare flagResolver: IFlagResolver;
private accessToken?: string; private accessToken?: string;

View File

@ -25,7 +25,7 @@ interface ISlackAddonParameters {
export default class SlackAddon extends Addon { export default class SlackAddon extends Addon {
private msgFormatter: FeatureEventFormatter; private msgFormatter: FeatureEventFormatter;
flagResolver: IFlagResolver; declare flagResolver: IFlagResolver;
constructor(args: IAddonConfig) { constructor(args: IAddonConfig) {
super(slackDefinition, args); super(slackDefinition, args);

View File

@ -38,7 +38,7 @@ interface ITeamsParameters {
export default class TeamsAddon extends Addon { export default class TeamsAddon extends Addon {
private msgFormatter: FeatureEventFormatter; private msgFormatter: FeatureEventFormatter;
flagResolver: IFlagResolver; declare flagResolver: IFlagResolver;
constructor(args: IAddonConfig) { constructor(args: IAddonConfig) {
if (args.flagResolver.isEnabled('teamsIntegrationChangeRequests')) { if (args.flagResolver.isEnabled('teamsIntegrationChangeRequests')) {

View File

@ -25,7 +25,7 @@ interface IParameters {
export default class Webhook extends Addon { export default class Webhook extends Addon {
private msgFormatter: FeatureEventFormatter; private msgFormatter: FeatureEventFormatter;
flagResolver: IFlagResolver; declare flagResolver: IFlagResolver;
constructor(args: IAddonConfig) { constructor(args: IAddonConfig) {
super(definition, args); super(definition, args);

View File

@ -18,10 +18,10 @@ jest.mock(
}, },
); );
const getApp = require('./app').default; import getApp from './app';
test('should not throw when valid config', async () => { test('should not throw when valid config', async () => {
const config = createTestConfig(); const config = createTestConfig();
// @ts-expect-error - We're passing in empty stores and services
const app = await getApp(config, {}, {}); const app = await getApp(config, {}, {});
expect(typeof app.listen).toBe('function'); expect(typeof app.listen).toBe('function');
}); });
@ -33,6 +33,7 @@ test('should call preHook', async () => {
called++; called++;
}, },
}); });
// @ts-expect-error - We're passing in empty stores and services
await getApp(config, {}, {}); await getApp(config, {}, {});
expect(called).toBe(1); expect(called).toBe(1);
}); });
@ -44,6 +45,7 @@ test('should call preRouterHook', async () => {
called++; called++;
}, },
}); });
// @ts-expect-error - We're passing in empty stores and services
await getApp(config, {}, {}); await getApp(config, {}, {});
expect(called).toBe(1); expect(called).toBe(1);
}); });
@ -82,6 +84,7 @@ describe('compression middleware', () => {
disableCompression: disableCompression as any, disableCompression: disableCompression as any,
}, },
}); });
// @ts-expect-error - We're passing in empty stores and services
await getApp(config, {}, {}); await getApp(config, {}, {});
expect(compression).toBeCalledTimes(+expectCompressionEnabled); expect(compression).toBeCalledTimes(+expectCompressionEnabled);
}, },

View File

@ -195,6 +195,7 @@ export default async function getApp(
} }
// Setup API routes // 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); app.use(`${baseUriPath}/`, new IndexRouter(config, services, db).router);
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {

View File

@ -335,17 +335,22 @@ const parseEnvVarInitialAdminUser = (): UsernameAdminUser | undefined => {
return username && password ? { username, password } : undefined; return username && password ? { username, password } : undefined;
}; };
const defaultAuthentication: IAuthOption = { const buildDefaultAuthOption = () => {
demoAllowAdminLogin: parseEnvVarBoolean( return {
process.env.AUTH_DEMO_ALLOW_ADMIN_LOGIN, demoAllowAdminLogin: parseEnvVarBoolean(
false, process.env.AUTH_DEMO_ALLOW_ADMIN_LOGIN,
), false,
enableApiToken: parseEnvVarBoolean(process.env.AUTH_ENABLE_API_TOKEN, true), ),
type: authTypeFromString(process.env.AUTH_TYPE), enableApiToken: parseEnvVarBoolean(
customAuthHandler: defaultCustomAuthDenyAll, process.env.AUTH_ENABLE_API_TOKEN,
createAdminUser: true, true,
initialAdminUser: parseEnvVarInitialAdminUser(), ),
initApiTokens: [], type: authTypeFromString(process.env.AUTH_TYPE),
customAuthHandler: defaultCustomAuthDenyAll,
createAdminUser: true,
initialAdminUser: parseEnvVarInitialAdminUser(),
initApiTokens: [],
};
}; };
const defaultImport: WithOptional<IImportOption, 'file'> = { const defaultImport: WithOptional<IImportOption, 'file'> = {
@ -563,7 +568,7 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
const initApiTokens = loadInitApiTokens(); const initApiTokens = loadInitApiTokens();
const authentication: IAuthOption = mergeAll([ const authentication: IAuthOption = mergeAll([
defaultAuthentication, buildDefaultAuthOption(),
(options.authentication (options.authentication
? removeUndefinedKeys(options.authentication) ? removeUndefinedKeys(options.authentication)
: options.authentication) || {}, : options.authentication) || {},

View File

@ -7,9 +7,8 @@ import type {
} from '../types/stores/client-instance-store'; } from '../types/stores/client-instance-store';
import { subDays } from 'date-fns'; import { subDays } from 'date-fns';
import type { Db } from './db'; import type { Db } from './db';
import metricsHelper from '../util/metrics-helper';
const metricsHelper = require('../util/metrics-helper'); import { DB_TIME } from '../metric-events';
const { DB_TIME } = require('../metric-events');
const COLUMNS = [ const COLUMNS = [
'app_name', 'app_name',

View File

@ -153,6 +153,7 @@ export class PublicSignupTokenStore implements IPublicSignupTokenStore {
newToken: IPublicSignupTokenCreate, newToken: IPublicSignupTokenCreate,
): Promise<PublicSignupTokenSchema> { ): Promise<PublicSignupTokenSchema> {
const response = await this.db<ITokenRow>(TABLE).insert( 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), toRow(newToken),
['secret'], ['secret'],
); );

View File

@ -25,7 +25,7 @@ const fieldToRow = (fields: IUserFeedback): IUserFeedbackTable => ({
}); });
const rowToField = (row: IUserFeedbackTable): IUserFeedback => ({ const rowToField = (row: IUserFeedbackTable): IUserFeedback => ({
neverShow: row.nevershow, neverShow: row.nevershow || false,
feedbackId: row.feedback_id, feedbackId: row.feedback_id,
given: row.given, given: row.given,
userId: row.user_id, userId: row.user_id,
@ -71,7 +71,7 @@ export default class UserFeedbackStore implements IUserFeedbackStore {
.merge() .merge()
.returning(COLUMNS); .returning(COLUMNS);
return rowToField(insertedFeedback[0]); return rowToField(insertedFeedback[0] as IUserFeedbackTable);
} }
async delete({ userId, feedbackId }: IUserFeedbackKey): Promise<void> { async delete({ userId, feedbackId }: IUserFeedbackKey): Promise<void> {

View File

@ -23,7 +23,7 @@ const fieldToRow = (fields: IUserSplash): IUserSplashTable => ({
}); });
const rowToField = (row: IUserSplashTable): IUserSplash => ({ const rowToField = (row: IUserSplashTable): IUserSplash => ({
seen: row.seen, seen: row.seen || false,
splashId: row.splash_id, splashId: row.splash_id,
userId: row.user_id, userId: row.user_id,
}); });
@ -65,7 +65,7 @@ export default class UserSplashStore implements IUserSplashStore {
.merge() .merge()
.returning(COLUMNS); .returning(COLUMNS);
return rowToField(insertedSplash[0]); return rowToField(insertedSplash[0] as IUserSplashTable);
} }
async delete({ userId, splashId }: IUserSplashKey): Promise<void> { async delete({ userId, splashId }: IUserSplashKey): Promise<void> {

View File

@ -22,7 +22,9 @@ const getPotentiallyStaleCount = (
const today = new Date().valueOf(); const today = new Date().valueOf();
return features.filter((feature) => { return features.filter((feature) => {
const diff = today - feature.createdAt?.valueOf(); const diff = feature.createdAt
? today - feature.createdAt.valueOf()
: 0;
const featureTypeExpectedLifetime = featureTypes.find( const featureTypeExpectedLifetime = featureTypes.find(
(t) => t.id === feature.type, (t) => t.id === feature.type,
)?.lifetimeDays; )?.lifetimeDays;
@ -30,6 +32,7 @@ const getPotentiallyStaleCount = (
return ( return (
!feature.stale && !feature.stale &&
featureTypeExpectedLifetime !== null && featureTypeExpectedLifetime !== null &&
featureTypeExpectedLifetime !== undefined &&
diff >= featureTypeExpectedLifetime * hoursToMilliseconds(24) diff >= featureTypeExpectedLifetime * hoursToMilliseconds(24)
); );
}).length; }).length;

View File

@ -38,7 +38,7 @@ export default class FakeClientFeatureToggleStore
...t, ...t,
enabled: true, enabled: true,
strategies: [], strategies: [],
description: t.description || '', description: t.description,
type: t.type || 'Release', type: t.type || 'Release',
stale: t.stale || false, stale: t.stale || false,
variants: [], variants: [],

View File

@ -50,7 +50,7 @@ const mapRow = (row: ContextFieldDB): IContextField => ({
interface ICreateContextField { interface ICreateContextField {
name: string; name: string;
description: string; description?: string | null;
stickiness: boolean; stickiness: boolean;
sort_order: number; sort_order: number;
legal_values?: string; legal_values?: string;
@ -80,8 +80,8 @@ class ContextFieldStore implements IContextFieldStore {
return { return {
name: data.name, name: data.name,
description: data.description, description: data.description,
stickiness: data.stickiness, stickiness: data.stickiness || false,
sort_order: data.sortOrder, // eslint-disable-line sort_order: data.sortOrder || 0,
legal_values: JSON.stringify(data.legalValues || []), legal_values: JSON.stringify(data.legalValues || []),
}; };
} }

View File

@ -21,7 +21,7 @@ import {
import type { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType'; import type { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType';
import type EventService from '../events/event-service'; import type EventService from '../events/event-service';
import { contextSchema, legalValueSchema } from '../../services/context-schema'; import { contextSchema, legalValueSchema } from '../../services/context-schema';
import { NameExistsError } from '../../error'; import { NameExistsError, NotFoundError } from '../../error';
import { nameSchema } from '../../schema/feature-schema'; import { nameSchema } from '../../schema/feature-schema';
import type { LegalValueSchema } from '../../openapi'; import type { LegalValueSchema } from '../../openapi';
@ -63,7 +63,13 @@ class ContextService {
} }
async getContextField(name: string): Promise<IContextField> { 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( async getStrategiesByContextField(
@ -125,6 +131,11 @@ class ContextService {
const contextField = await this.contextFieldStore.get( const contextField = await this.contextFieldStore.get(
updatedContextField.name, updatedContextField.name,
); );
if (contextField === undefined) {
throw new NotFoundError(
`Could not find context field with name: ${updatedContextField.name}`,
);
}
const value = await contextSchema.validateAsync(updatedContextField); const value = await contextSchema.validateAsync(updatedContextField);
await this.contextFieldStore.update(value); await this.contextFieldStore.update(value);
@ -147,6 +158,11 @@ class ContextService {
const contextField = await this.contextFieldStore.get( const contextField = await this.contextFieldStore.get(
contextFieldLegalValue.name, contextFieldLegalValue.name,
); );
if (contextField === undefined) {
throw new NotFoundError(
`Context field with name ${contextFieldLegalValue.name} was not found`,
);
}
const validatedLegalValue = await legalValueSchema.validateAsync( const validatedLegalValue = await legalValueSchema.validateAsync(
contextFieldLegalValue.legalValue, contextFieldLegalValue.legalValue,
); );
@ -186,6 +202,11 @@ class ContextService {
const contextField = await this.contextFieldStore.get( const contextField = await this.contextFieldStore.get(
contextFieldLegalValue.name, contextFieldLegalValue.name,
); );
if (contextField === undefined) {
throw new NotFoundError(
`Could not find context field with name ${contextFieldLegalValue.name}`,
);
}
const newContextField = { const newContextField = {
...contextField, ...contextField,

View File

@ -262,7 +262,7 @@ beforeAll(async () => {
contextFieldStore = db.stores.contextFieldStore; contextFieldStore = db.stores.contextFieldStore;
const roles = await accessService.getRootRoles(); const roles = await accessService.getRootRoles();
adminRole = roles.find((role) => role.name === RoleName.ADMIN); adminRole = roles.find((role) => role.name === RoleName.ADMIN)!;
await createUserEditorAccess( await createUserEditorAccess(
regularUserName, regularUserName,

View File

@ -463,7 +463,7 @@ export default class ExportImportService
this.contextService.createContextField( this.contextService.createContextField(
{ {
name: contextField.name, name: contextField.name,
description: contextField.description || '', description: contextField.description,
legalValues: contextField.legalValues, legalValues: contextField.legalValues,
stickiness: contextField.stickiness, stickiness: contextField.stickiness,
}, },

View File

@ -48,12 +48,14 @@ export class ImportPermissionsService {
): Promise<ContextFieldSchema[]> { ): Promise<ContextFieldSchema[]> {
const availableContextFields = await this.contextService.getAll(); const availableContextFields = await this.contextService.getAll();
return dto.data.contextFields?.filter( return (
(contextField) => dto.data.contextFields?.filter(
!availableContextFields.some( (contextField) =>
(availableField) => !availableContextFields.some(
availableField.name === contextField.name, (availableField) =>
), availableField.name === contextField.name,
),
) || []
); );
} }

View File

@ -253,7 +253,7 @@ class FeatureSearchStore implements IFeatureSearchStore {
const rankingSql = this.buildRankingSql( const rankingSql = this.buildRankingSql(
favoritesFirst, favoritesFirst,
sortBy, sortBy || '',
validatedSortOrder, validatedSortOrder,
lastSeenQuery, lastSeenQuery,
); );
@ -705,12 +705,14 @@ const applyMultiQueryParams = (
) => (dbSubQuery: Knex.QueryBuilder) => Knex.QueryBuilder, ) => (dbSubQuery: Knex.QueryBuilder) => Knex.QueryBuilder,
): void => { ): void => {
queryParams.forEach((param) => { queryParams.forEach((param) => {
const values = param.values.map((val) => const values = param.values
(Array.isArray(fields) .filter((v) => typeof v === 'string')
? val.split(/:(.+)/).filter(Boolean) .map((val) =>
: [val] (Array.isArray(fields)
).map((s) => s.trim()), ? val!.split(/:(.+)/).filter(Boolean)
); : [val]
).map((s) => s.trim()),
);
const baseSubQuery = createBaseQuery(values); const baseSubQuery = createBaseQuery(values);
switch (param.operator) { switch (param.operator) {

View File

@ -150,11 +150,23 @@ export default class ArchiveController extends Controller {
true, true,
extractUserIdFromUser(user), extractUserIdFromUser(user),
); );
this.openApiService.respondWithValidation( this.openApiService.respondWithValidation(
200, 200,
res, res,
archivedFeaturesSchema.$id, archivedFeaturesSchema.$id,
{ version: 2, features: serializeDates(features) }, {
version: 2,
features: serializeDates(
features.map((feature) => {
return {
...feature,
stale: feature.stale || false,
archivedAt: feature.archivedAt!,
};
}),
),
},
); );
} }

View File

@ -147,7 +147,7 @@ export class FeatureToggleRowConverter {
feature.name = row.name; feature.name = row.name;
feature.description = row.description; feature.description = row.description;
feature.project = row.project; feature.project = row.project;
feature.stale = row.stale; feature.stale = row.stale || false;
feature.type = row.type; feature.type = row.type;
feature.lastSeenAt = row.last_seen_at; feature.lastSeenAt = row.last_seen_at;
feature.variants = row.variants || []; feature.variants = row.variants || [];
@ -176,13 +176,13 @@ export class FeatureToggleRowConverter {
const result = rows.reduce((acc, r) => { const result = rows.reduce((acc, r) => {
let feature: PartialDeep<IFeatureToggleListItem> = acc[r.name] ?? { let feature: PartialDeep<IFeatureToggleListItem> = acc[r.name] ?? {
strategies: [], strategies: [],
stale: r.stale || false,
}; };
feature = this.createBaseFeature(r, feature, featureQuery); feature = this.createBaseFeature(r, feature, featureQuery);
feature.createdAt = r.created_at; feature.createdAt = r.created_at;
feature.favorite = r.favorite; feature.favorite = r.favorite;
this.addLastSeenByEnvironment(feature, r); this.addLastSeenByEnvironment(feature, r);
acc[r.name] = feature; acc[r.name] = feature;

View File

@ -1,4 +1,4 @@
import { randomUUID } from 'crypto'; import { randomUUID } from 'node:crypto';
import type { import type {
FeatureToggleWithEnvironment, FeatureToggleWithEnvironment,
IFeatureOverview, IFeatureOverview,
@ -67,7 +67,7 @@ export default class FakeFeatureStrategiesStore
return this.featureStrategies.some((s) => s.id === id); 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); return this.featureStrategies.find((s) => s.id === id);
} }
@ -180,8 +180,8 @@ export default class FakeFeatureStrategiesStore
archived: boolean = false, archived: boolean = false,
): Promise<IFeatureToggleClient[]> { ): Promise<IFeatureToggleClient[]> {
const rows = this.featureToggles.filter((toggle) => { const rows = this.featureToggles.filter((toggle) => {
if (featureQuery.namePrefix) { if (featureQuery?.namePrefix) {
if (featureQuery.project) { if (featureQuery?.project) {
return ( return (
(toggle.name.startsWith(featureQuery.namePrefix) && (toggle.name.startsWith(featureQuery.namePrefix) &&
featureQuery.project.some((project) => featureQuery.project.some((project) =>
@ -192,7 +192,7 @@ export default class FakeFeatureStrategiesStore
} }
return toggle.name.startsWith(featureQuery.namePrefix); return toggle.name.startsWith(featureQuery.namePrefix);
} }
if (featureQuery.project) { if (featureQuery?.project) {
return ( return (
featureQuery.project.some((project) => featureQuery.project.some((project) =>
project.includes(toggle.project), project.includes(toggle.project),
@ -205,7 +205,7 @@ export default class FakeFeatureStrategiesStore
...t, ...t,
enabled: true, enabled: true,
strategies: [], strategies: [],
description: t.description || '', description: t.description || undefined,
type: t.type || 'Release', type: t.type || 'Release',
stale: t.stale || false, stale: t.stale || false,
variants: [], variants: [],
@ -233,7 +233,7 @@ export default class FakeFeatureStrategiesStore
this.environmentAndFeature.set(environment, []); this.environmentAndFeature.set(environment, []);
} }
this.environmentAndFeature this.environmentAndFeature
.get(environment) .get(environment)!
.push({ feature: feature_name, enabled }); .push({ feature: feature_name, enabled });
return Promise.resolve(); return Promise.resolve();
} }
@ -245,7 +245,7 @@ export default class FakeFeatureStrategiesStore
this.environmentAndFeature.set( this.environmentAndFeature.set(
environment, environment,
this.environmentAndFeature this.environmentAndFeature
.get(environment) .get(environment)!
.filter((e) => e.featureName !== feature_name), .filter((e) => e.featureName !== feature_name),
); );
return Promise.resolve(); return Promise.resolve();
@ -271,7 +271,9 @@ export default class FakeFeatureStrategiesStore
} }
return f; return f;
}); });
return Promise.resolve(this.featureStrategies.find((f) => f.id === id)); return Promise.resolve(
this.featureStrategies.find((f) => f.id === id)!,
);
} }
async deleteConfigurationsForProjectAndEnvironment( async deleteConfigurationsForProjectAndEnvironment(

View File

@ -87,8 +87,11 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
return this.features.filter((f) => names.includes(f.name)); return this.features.filter((f) => names.includes(f.name));
} }
async getProjectId(name: string): Promise<string> { async getProjectId(name: string | undefined): Promise<string | undefined> {
return this.get(name).then((f) => f.project); if (name === undefined) {
return Promise.resolve(undefined);
}
return Promise.resolve(this.get(name).then((f) => f.project));
} }
private getFilterQuery(query: Partial<IFeatureToggleStoreQuery>) { private getFilterQuery(query: Partial<IFeatureToggleStoreQuery>) {
@ -164,7 +167,7 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
if (revive) { if (revive) {
revive.archived = false; revive.archived = false;
} }
return this.update(revive.project, revive); return this.update(revive!.project, revive!);
} }
async getFeatureToggleList( async getFeatureToggleList(
@ -195,7 +198,7 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
if (exists) { if (exists) {
const id = this.features.findIndex((f) => f.name === data.name); const id = this.features.findIndex((f) => f.name === data.name);
const old = this.features.find((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.splice(id, 1);
this.features.push(updated); this.features.push(updated);
return updated; return updated;
@ -293,8 +296,9 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
} }
if ( if (
queryModifiers.date &&
new Date(feature[queryModifiers.dateAccessor]).getTime() >= new Date(feature[queryModifiers.dateAccessor]).getTime() >=
new Date(queryModifiers.date).getTime() new Date(queryModifiers.date).getTime()
) { ) {
return true; return true;
} }
@ -303,6 +307,7 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
feature[queryModifiers.dateAccessor], feature[queryModifiers.dateAccessor],
).getTime(); ).getTime();
if ( if (
queryModifiers.range &&
featureDate >= new Date(queryModifiers.range[0]).getTime() && featureDate >= new Date(queryModifiers.range[0]).getTime() &&
featureDate <= new Date(queryModifiers.range[1]).getTime() featureDate <= new Date(queryModifiers.range[1]).getTime()
) { ) {

View File

@ -670,7 +670,7 @@ export default class ProjectFeaturesController extends Controller {
201, 201,
res, res,
featureSchema.$id, featureSchema.$id,
serializeDates(created), serializeDates({ ...created, stale: created.stale || false }),
); );
} }
@ -693,7 +693,7 @@ export default class ProjectFeaturesController extends Controller {
201, 201,
res, res,
featureSchema.$id, featureSchema.$id,
serializeDates(created), serializeDates({ ...created, stale: created.stale || false }),
); );
} }
@ -740,8 +740,13 @@ export default class ProjectFeaturesController extends Controller {
environmentVariants: variantEnvironments === 'true', environmentVariants: variantEnvironments === 'true',
userId: user.id, userId: user.id,
}); });
const maybeAnonymized = this.maybeAnonymise(feature);
res.status(200).json(serializeDates(this.maybeAnonymise(feature))); res.status(200).json(
serializeDates({
...maybeAnonymized,
stale: maybeAnonymized.stale || false,
}),
);
} }
async updateFeature( async updateFeature(
@ -771,7 +776,7 @@ export default class ProjectFeaturesController extends Controller {
200, 200,
res, res,
featureSchema.$id, featureSchema.$id,
serializeDates(created), serializeDates({ ...created, stale: created.stale || false }),
); );
} }
@ -795,7 +800,7 @@ export default class ProjectFeaturesController extends Controller {
200, 200,
res, res,
featureSchema.$id, featureSchema.$id,
serializeDates(updated), serializeDates({ ...updated, stale: updated.stale || false }),
); );
} }

View File

@ -294,7 +294,9 @@ class FeatureToggleService {
project: string, project: string,
): Promise<void> { ): Promise<void> {
const toggle = await this.featureToggleStore.get(featureName); const toggle = await this.featureToggleStore.get(featureName);
if (toggle === undefined) {
throw new NotFoundError(`Could not find feature ${featureName}`);
}
if (toggle.archived || Boolean(toggle.archivedAt)) { if (toggle.archived || Boolean(toggle.archivedAt)) {
throw new ArchivedFeatureError(); throw new ArchivedFeatureError();
} }
@ -840,7 +842,9 @@ class FeatureToggleService {
): Promise<Saved<IStrategyConfig>> { ): Promise<Saved<IStrategyConfig>> {
const { projectId, environment, featureName } = context; const { projectId, environment, featureName } = context;
const existingStrategy = await this.featureStrategiesStore.get(id); const existingStrategy = await this.featureStrategiesStore.get(id);
if (existingStrategy === undefined) {
throw new NotFoundError(`Could not find strategy with id ${id}`);
}
this.validateUpdatedProperties(context, existingStrategy); this.validateUpdatedProperties(context, existingStrategy);
await this.validateStrategyType(updates.name); await this.validateStrategyType(updates.name);
await this.validateProjectCanAccessSegments( await this.validateProjectCanAccessSegments(
@ -920,6 +924,9 @@ class FeatureToggleService {
const { projectId, environment, featureName } = context; const { projectId, environment, featureName } = context;
const existingStrategy = await this.featureStrategiesStore.get(id); const existingStrategy = await this.featureStrategiesStore.get(id);
if (existingStrategy === undefined) {
throw new NotFoundError(`Could not find strategy with id ${id}`);
}
this.validateUpdatedProperties(context, existingStrategy); this.validateUpdatedProperties(context, existingStrategy);
if (existingStrategy.id === id) { if (existingStrategy.id === id) {
@ -983,6 +990,10 @@ class FeatureToggleService {
auditUser: IAuditUser, auditUser: IAuditUser,
): Promise<void> { ): Promise<void> {
const existingStrategy = await this.featureStrategiesStore.get(id); const existingStrategy = await this.featureStrategiesStore.get(id);
if (!existingStrategy) {
// If the strategy doesn't exist, do nothing.
return;
}
const { featureName, projectId, environment } = context; const { featureName, projectId, environment } = context;
this.validateUpdatedProperties(context, existingStrategy); this.validateUpdatedProperties(context, existingStrategy);
@ -1142,11 +1153,17 @@ class FeatureToggleService {
featureName, featureName,
environment, environment,
}); });
return featureEnvironment.variants || []; return featureEnvironment?.variants || [];
} }
async getFeatureMetadata(featureName: string): Promise<FeatureToggle> { 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( async getClientFeatures(
@ -1172,7 +1189,7 @@ class FeatureToggleService {
type, type,
enabled, enabled,
project, project,
stale, stale: stale || false,
strategies, strategies,
variants, variants,
description, description,
@ -1322,7 +1339,11 @@ class FeatureToggleService {
): Promise<FeatureNameCheckResultWithFeaturePattern> { ): Promise<FeatureNameCheckResultWithFeaturePattern> {
try { try {
const project = await this.projectStore.get(projectId); 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 patternData = project.featureNaming;
const namingPattern = patternData?.pattern; const namingPattern = patternData?.pattern;
@ -1490,7 +1511,11 @@ class FeatureToggleService {
...featureData, ...featureData,
name: featureName, name: featureName,
}); });
if (preData === undefined) {
throw new NotFoundError(
`Could find feature toggle with name ${featureName}`,
);
}
await this.eventService.storeEvent( await this.eventService.storeEvent(
new FeatureMetadataUpdateEvent({ new FeatureMetadataUpdateEvent({
auditUser, auditUser,
@ -1601,6 +1626,9 @@ class FeatureToggleService {
let msg: string; let msg: string;
try { try {
const feature = await this.featureToggleStore.get(name); const feature = await this.featureToggleStore.get(name);
if (feature === undefined) {
return;
}
msg = feature.archived msg = feature.archived
? 'An archived flag with that name already exists' ? 'An archived flag with that name already exists'
: 'A flag with that name already exists'; : 'A flag with that name already exists';
@ -1620,6 +1648,11 @@ class FeatureToggleService {
auditUser: IAuditUser, auditUser: IAuditUser,
): Promise<any> { ): Promise<any> {
const feature = await this.featureToggleStore.get(featureName); const feature = await this.featureToggleStore.get(featureName);
if (feature === undefined) {
throw new NotFoundError(
`Could not find feature with name: ${featureName}`,
);
}
const { project } = feature; const { project } = feature;
feature.stale = isStale; feature.stale = isStale;
await this.featureToggleStore.update(project, feature); await this.featureToggleStore.update(project, feature);
@ -1658,7 +1691,11 @@ class FeatureToggleService {
projectId?: string, projectId?: string,
): Promise<void> { ): Promise<void> {
const feature = await this.featureToggleStore.get(featureName); const feature = await this.featureToggleStore.get(featureName);
if (feature === undefined) {
throw new NotFoundError(
`Could not find feature with name ${featureName}`,
);
}
if (projectId) { if (projectId) {
await this.validateFeatureBelongsToProject({ await this.validateFeatureBelongsToProject({
featureName, featureName,
@ -1941,7 +1978,7 @@ class FeatureToggleService {
}), }),
); );
} }
return feature; return feature!; // If we get here we know the toggle exists
} }
async changeProject( async changeProject(
@ -1968,6 +2005,11 @@ class FeatureToggleService {
); );
} }
const feature = await this.featureToggleStore.get(featureName); const feature = await this.featureToggleStore.get(featureName);
if (feature === undefined) {
throw new NotFoundError(
`Could not find feature with name ${featureName}`,
);
}
const oldProject = feature.project; const oldProject = feature.project;
feature.project = newProject; feature.project = newProject;
await this.featureToggleStore.update(newProject, feature); await this.featureToggleStore.update(newProject, feature);
@ -1989,6 +2031,9 @@ class FeatureToggleService {
): Promise<void> { ): Promise<void> {
await this.validateNoChildren(featureName); await this.validateNoChildren(featureName);
const toggle = await this.featureToggleStore.get(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); const tags = await this.tagStore.getAllTagsForFeature(featureName);
await this.featureToggleStore.delete(featureName); await this.featureToggleStore.delete(featureName);
@ -2275,7 +2320,7 @@ class FeatureToggleService {
featureName, featureName,
environment, environment,
}) })
).variants || )?.variants ||
[]; [];
await this.eventService.storeEvent( await this.eventService.storeEvent(
@ -2353,7 +2398,7 @@ class FeatureToggleService {
featureName, featureName,
environment: env, environment: env,
}); });
oldVariants[env] = featureEnv.variants || []; oldVariants[env] = featureEnv?.variants || [];
} }
await this.eventService.storeEvents( await this.eventService.storeEvents(

View File

@ -44,15 +44,15 @@ const FEATURE_COLUMNS = [
export interface FeaturesTable { export interface FeaturesTable {
name: string; name: string;
description: string; description: string | null;
type: string; type?: string;
stale: boolean; stale?: boolean | null;
project: string; project: string;
last_seen_at?: Date; last_seen_at?: Date;
created_at?: Date; created_at?: Date;
impression_data: boolean; impression_data?: boolean | null;
archived?: boolean; archived?: boolean;
archived_at?: Date; archived_at?: Date | null;
created_by_user_id?: number; created_by_user_id?: number;
} }
@ -309,7 +309,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
const result = await query; const result = await query;
return result.map((row) => ({ return result.map((row) => ({
type: row.type, type: row.type!,
count: Number(row.count), count: Number(row.count),
})); }));
} }
@ -449,11 +449,11 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
description: row.description, description: row.description,
type: row.type, type: row.type,
project: row.project, project: row.project,
stale: row.stale, stale: row.stale || false,
createdAt: row.created_at, createdAt: row.created_at,
lastSeenAt: row.last_seen_at, lastSeenAt: row.last_seen_at,
impressionData: row.impression_data, impressionData: row.impression_data || false,
archivedAt: row.archived_at, archivedAt: row.archived_at || undefined,
archived: row.archived_at != null, archived: row.archived_at != null,
}; };
} }
@ -472,13 +472,13 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
insertToRow(project: string, data: FeatureToggleInsert): FeaturesTable { insertToRow(project: string, data: FeatureToggleInsert): FeaturesTable {
const row = { const row = {
name: data.name, name: data.name,
description: data.description, description: data.description || null,
type: data.type, type: data.type,
project, project,
archived_at: data.archived ? new Date() : null, archived_at: data.archived ? new Date() : null,
stale: data.stale, stale: data.stale || false,
created_at: data.createdAt, created_at: data.createdAt,
impression_data: data.impressionData, impression_data: data.impressionData || false,
created_by_user_id: data.createdByUserId, created_by_user_id: data.createdByUserId,
}; };
if (!row.created_at) { if (!row.created_at) {
@ -494,7 +494,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
): Omit<FeaturesTable, 'created_by_user_id'> { ): Omit<FeaturesTable, 'created_by_user_id'> {
const row = { const row = {
name: data.name, name: data.name,
description: data.description, description: data.description || null,
type: data.type, type: data.type,
project, project,
archived_at: data.archived ? new Date() : null, archived_at: data.archived ? new Date() : null,

View File

@ -23,7 +23,7 @@ export interface IFeatureToggleStore extends Store<FeatureToggle, string> {
setLastSeen(data: LastSeenInput[]): Promise<void>; 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>; create(project: string, data: FeatureToggleInsert): Promise<FeatureToggle>;

View File

@ -1,5 +1,5 @@
// Copy of https://github.com/Unleash/unleash-proxy/blob/main/src/create-context.ts. // 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'; import type { Context } from 'unleash-client';
export function createContext(contextData: any): Context { export function createContext(contextData: any): Context {

View File

@ -1,4 +1,4 @@
import crypto from 'crypto'; import crypto from 'node:crypto';
import type { import type {
IAuditUser, IAuditUser,
IUnleashConfig, IUnleashConfig,
@ -63,7 +63,7 @@ export class FrontendApiService {
private readonly clients: Map<ApiUser['secret'], Promise<Unleash>> = private readonly clients: Map<ApiUser['secret'], Promise<Unleash>> =
new Map(); new Map();
private cachedFrontendSettings?: FrontendSettings; private cachedFrontendSettings: FrontendSettings;
constructor( constructor(
config: Config, config: Config,
@ -228,9 +228,12 @@ export class FrontendApiService {
async fetchFrontendSettings(): Promise<FrontendSettings> { async fetchFrontendSettings(): Promise<FrontendSettings> {
try { try {
this.cachedFrontendSettings = this.cachedFrontendSettings =
await this.services.settingService.get(frontendSettingsKey, { await this.services.settingService.getWithDefault(
frontendApiOrigins: this.config.frontendApiOrigins, frontendSettingsKey,
}); {
frontendApiOrigins: this.config.frontendApiOrigins,
},
);
} catch (error) { } catch (error) {
this.logger.debug('Unable to fetch frontend settings', error); this.logger.debug('Unable to fetch frontend settings', error);
} }

View File

@ -154,7 +154,10 @@ export class GlobalFrontendApiCache extends EventEmitter {
Object.fromEntries( Object.fromEntries(
Object.entries(value).map(([innerKey, innerValue]) => [ Object.entries(value).map(([innerKey, innerValue]) => [
innerKey, innerKey,
mapFeatureForClient(innerValue), mapFeatureForClient({
...innerValue,
stale: innerValue.stale || false,
}),
]), ]),
), ),
]); ]);

View File

@ -2,15 +2,16 @@ import ClientInstanceService from '../instance/instance-service';
import type { IClientApp } from '../../../types/model'; import type { IClientApp } from '../../../types/model';
import { secondsToMilliseconds } from 'date-fns'; import { secondsToMilliseconds } from 'date-fns';
import { createTestConfig } from '../../../../test/config/test-config'; 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 { FakePrivateProjectChecker } from '../../private-project/fakePrivateProjectChecker';
import type { ITestDb } from '../../../../test/e2e/helpers/database-init'; import type { ITestDb } from '../../../../test/e2e/helpers/database-init';
import dbInit from '../../../../test/e2e/helpers/database-init';
const faker = require('faker'); import { noLoggerProvider as getLogger } from '../../../../test/fixtures/no-logger';
const dbInit = require('../../../../test/e2e/helpers/database-init'); import faker from 'faker';
const getLogger = require('../../../../test/fixtures/no-logger');
const { APPLICATION_CREATED } = require('../../../types/events');
let stores: IUnleashStores; let stores: IUnleashStores;
let db: ITestDb; let db: ITestDb;
let clientInstanceService: ClientInstanceService; let clientInstanceService: ClientInstanceService;

View File

@ -20,7 +20,7 @@ export interface IClientMetricsEnvVariant extends IClientMetricsEnvKey {
export interface IClientMetricsStoreV2 export interface IClientMetricsStoreV2
extends Store<IClientMetricsEnv, IClientMetricsEnvKey> { extends Store<IClientMetricsEnv, IClientMetricsEnvKey> {
batchInsertMetrics(metrics: IClientMetricsEnv[]): Promise<void>; batchInsertMetrics(metrics: IClientMetricsEnv[] | undefined): Promise<void>;
getMetricsForFeatureToggle( getMetricsForFeatureToggle(
featureName: string, featureName: string,
hoursBack?: number, hoursBack?: number,

View File

@ -352,12 +352,12 @@ test('Should get metric', async () => {
}, },
]; ];
await clientMetricsStore.batchInsertMetrics(metrics); await clientMetricsStore.batchInsertMetrics(metrics);
const metric = await clientMetricsStore.get({ const metric = (await clientMetricsStore.get({
featureName: 'demo4', featureName: 'demo4',
timestamp: twoDaysAgo, timestamp: twoDaysAgo,
appName: 'backend-api', appName: 'backend-api',
environment: 'dev', environment: 'dev',
}); }))!;
expect(metric.featureName).toBe('demo4'); expect(metric.featureName).toBe('demo4');
expect(metric.yes).toBe(41); expect(metric.yes).toBe(41);

View File

@ -26,6 +26,7 @@ import type { Logger } from '../../../logger';
import { findOutdatedSDKs, isOutdatedSdk } from './findOutdatedSdks'; import { findOutdatedSDKs, isOutdatedSdk } from './findOutdatedSdks';
import type { OutdatedSdksSchema } from '../../../openapi/spec/outdated-sdks-schema'; import type { OutdatedSdksSchema } from '../../../openapi/spec/outdated-sdks-schema';
import { CLIENT_REGISTERED } from '../../../metric-events'; import { CLIENT_REGISTERED } from '../../../metric-events';
import { NotFoundError } from '../../../error';
export default class ClientInstanceService { export default class ClientInstanceService {
apps = {}; apps = {};
@ -219,7 +220,11 @@ export default class ClientInstanceService {
this.strategyStore.getAll(), this.strategyStore.getAll(),
this.featureToggleStore.getAll(), this.featureToggleStore.getAll(),
]); ]);
if (application === undefined) {
throw new NotFoundError(
`Could not find application with appName ${appName}`,
);
}
return { return {
appName: application.appName, appName: application.appName,
createdAt: application.createdAt, createdAt: application.createdAt,

View File

@ -27,6 +27,7 @@ export default class RemoteAddressStrategy extends Strategy {
return false; return false;
} }
} }
return false;
}, },
); );
} }

View File

@ -10,6 +10,8 @@ export default class UserWithIdStrategy extends Strategy {
const userIdList = parameters.userIds const userIdList = parameters.userIds
? parameters.userIds.split(/\s*,\s*/) ? parameters.userIds.split(/\s*,\s*/)
: []; : [];
return userIdList.includes(context.userId); return (
context.userId !== undefined && userIdList.includes(context.userId)
);
} }
} }

View File

@ -453,6 +453,7 @@ describe('offline client', () => {
const client = await offlineUnleashClient({ const client = await offlineUnleashClient({
features: [ features: [
{ {
// @ts-expect-error: hostnames is incompatible with index signature | undefined is not assignable to type string
strategies, strategies,
// impressionData: false, // impressionData: false,
enabled: true, enabled: true,

View File

@ -71,14 +71,20 @@ export default class EnvironmentService {
} }
async get(name: string): Promise<IEnvironment> { 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( async getProjectEnvironments(
projectId: string, projectId: string,
): Promise<IProjectsAvailableOnEnvironment[]> { ): Promise<IProjectsAvailableOnEnvironment[]> {
// This function produces an object for every environment, in that object is a boolean // 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 = const environments =
await this.projectStore.getEnvironmentsForProject(projectId); await this.projectStore.getEnvironmentsForProject(projectId);
const environmentsOnProject = new Set( const environmentsOnProject = new Set(

View File

@ -65,7 +65,7 @@ export default class FakeEnvironmentStore implements IEnvironmentStore {
): Promise<IEnvironment> { ): Promise<IEnvironment> {
const found = this.environments.find( const found = this.environments.find(
(en: IEnvironment) => en.name === name, (en: IEnvironment) => en.name === name,
); )!;
const idx = this.environments.findIndex( const idx = this.environments.findIndex(
(en: IEnvironment) => en.name === name, (en: IEnvironment) => en.name === name,
); );
@ -78,7 +78,7 @@ export default class FakeEnvironmentStore implements IEnvironmentStore {
async updateSortOrder(id: string, value: number): Promise<void> { async updateSortOrder(id: string, value: number): Promise<void> {
const environment = this.environments.find( const environment = this.environments.find(
(env: IEnvironment) => env.name === id, (env: IEnvironment) => env.name === id,
); )!;
environment.sortOrder = value; environment.sortOrder = value;
return Promise.resolve(); return Promise.resolve();
} }
@ -90,7 +90,7 @@ export default class FakeEnvironmentStore implements IEnvironmentStore {
): Promise<void> { ): Promise<void> {
const environment = this.environments.find( const environment = this.environments.find(
(env: IEnvironment) => env.name === id, (env: IEnvironment) => env.name === id,
); )!;
environment[field] = value; environment[field] = value;
return Promise.resolve(); return Promise.resolve();
} }
@ -132,8 +132,8 @@ export default class FakeEnvironmentStore implements IEnvironmentStore {
destroy(): void {} destroy(): void {}
async get(key: string): Promise<IEnvironment> { async get(key: string): Promise<IEnvironment | undefined> {
return this.environments.find((e) => e.name === key); return Promise.resolve(this.environments.find((e) => e.name === key));
} }
async getAllWithCounts(): Promise<IEnvironment[]> { async getAllWithCounts(): Promise<IEnvironment[]> {

View File

@ -112,7 +112,7 @@ export class ProjectInsightsService {
]); ]);
return { return {
health: project.health || 0, health: project?.health || 0,
features: features, features: features,
}; };
} }

View File

@ -2472,7 +2472,7 @@ test('should not delete project-bound api tokens still bound to project', async
await projectService.deleteProject(project1, user, auditUser); await projectService.deleteProject(project1, user, auditUser);
const fetchedToken = await apiTokenService.getToken(token.secret); const fetchedToken = await apiTokenService.getToken(token.secret);
expect(fetchedToken).not.toBeUndefined(); 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 () => { test('should delete project-bound api tokens when all projects they belong to are deleted', async () => {

View File

@ -261,7 +261,11 @@ export default class ProjectService {
} }
async getProject(id: string): Promise<IProject> { 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 = ( private validateAndProcessFeatureNamingPattern = (
@ -503,7 +507,9 @@ export default class ProjectService {
auditUser: IAuditUser, auditUser: IAuditUser,
): Promise<any> { ): Promise<any> {
const feature = await this.featureToggleStore.get(featureName); const feature = await this.featureToggleStore.get(featureName);
if (feature === undefined) {
throw new NotFoundError(`Could not find feature ${featureName}`);
}
if (feature.project !== currentProjectId) { if (feature.project !== currentProjectId) {
throw new PermissionError(MOVE_FEATURE_TOGGLE); throw new PermissionError(MOVE_FEATURE_TOGGLE);
} }
@ -676,7 +682,7 @@ export default class ProjectService {
roleId, roleId,
userId, userId,
roleName: role.name, roleName: role.name,
email: user.email, email: user?.email,
}, },
}), }),
); );
@ -1374,7 +1380,11 @@ export default class ProjectService {
: Promise.resolve(false), : Promise.resolve(false),
this.projectStatsStore.getProjectStats(projectId), this.projectStatsStore.getProjectStats(projectId),
]); ]);
if (project === undefined) {
throw new NotFoundError(
`Could not find project with id ${projectId}`,
);
}
return { return {
stats: projectStats, stats: projectStats,
name: project.name, name: project.name,
@ -1426,6 +1436,12 @@ export default class ProjectService {
this.onboardingReadModel.getOnboardingStatusForProject(projectId), this.onboardingReadModel.getOnboardingStatusForProject(projectId),
]); ]);
if (project === undefined) {
throw new NotFoundError(
`Could not find project with id: ${projectId}`,
);
}
return { return {
stats: projectStats, stats: projectStats,
name: project.name, name: project.name,

View File

@ -45,7 +45,7 @@ test('should exclude archived projects', async () => {
test('should have default project', async () => { test('should have default project', async () => {
const project = await projectStore.get('default'); const project = await projectStore.get('default');
expect(project).toBeDefined(); expect(project).toBeDefined();
expect(project.id).toBe('default'); expect(project!.id).toBe('default');
}); });
test('should create new project', async () => { test('should create new project', async () => {
@ -58,11 +58,11 @@ test('should create new project', async () => {
await projectStore.create(project); await projectStore.create(project);
const ret = await projectStore.get('test'); const ret = await projectStore.get('test');
const exists = await projectStore.exists('test'); const exists = await projectStore.exists('test');
expect(project.id).toEqual(ret.id); expect(project.id).toEqual(ret!.id);
expect(project.name).toEqual(ret.name); expect(project.name).toEqual(ret!.name);
expect(project.description).toEqual(ret.description); expect(project.description).toEqual(ret!.description);
expect(ret.createdAt).toBeTruthy(); expect(ret!.createdAt).toBeTruthy();
expect(ret.updatedAt).toBeTruthy(); expect(ret!.updatedAt).toBeTruthy();
expect(exists).toBe(true); expect(exists).toBe(true);
}); });
@ -103,8 +103,8 @@ test('should update project', async () => {
const readProject = await projectStore.get(project.id); const readProject = await projectStore.get(project.id);
expect(updatedProject.name).toBe(readProject.name); expect(updatedProject.name).toBe(readProject!.name);
expect(updatedProject.description).toBe(readProject.description); expect(updatedProject.description).toBe(readProject!.description);
}); });
test('should give error when getting unknown project', async () => { test('should give error when getting unknown project', async () => {

View File

@ -355,7 +355,9 @@ test('should inline segment constraints into features by default', async () => {
const clientFeatures = await fetchClientFeatures(); const clientFeatures = await fetchClientFeatures();
const clientStrategies = clientFeatures.flatMap((f) => f.strategies); 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 clientValues = clientConstraints.flatMap((c) => c.values);
const uniqueValues = [...new Set(clientValues)]; const uniqueValues = [...new Set(clientValues)];

View File

@ -20,7 +20,7 @@ import type {
ISegmentService, ISegmentService,
StrategiesUsingSegment, StrategiesUsingSegment,
} from './segment-service-interface'; } 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 { IChangeRequestAccessReadModel } from '../change-request-access-service/change-request-access-read-model';
import type { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType'; import type { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType';
import type EventService from '../events/event-service'; import type EventService from '../events/event-service';
@ -74,7 +74,11 @@ export class SegmentService implements ISegmentService {
} }
async get(id: number): Promise<ISegment> { 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[]> { async getAll(): Promise<ISegment[]> {
@ -179,7 +183,11 @@ export class SegmentService implements ISegmentService {
const input = await segmentSchema.validateAsync(data); const input = await segmentSchema.validateAsync(data);
this.validateSegmentValuesLimit(input); this.validateSegmentValuesLimit(input);
const preData = await this.segmentStore.get(id); 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) { if (preData.name !== input.name) {
await this.validateName(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> { async delete(id: number, user: User, auditUser: IAuditUser): Promise<void> {
const segment = await this.segmentStore.get(id); const segment = await this.segmentStore.get(id);
if (segment === undefined) {
/// Already deleted
return;
}
await this.stopWhenChangeRequestsEnabled(segment.project, user); await this.stopWhenChangeRequestsEnabled(segment.project, user);
await this.segmentStore.delete(id); await this.segmentStore.delete(id);
await this.eventService.storeEvent( await this.eventService.storeEvent(

View File

@ -1,6 +1,6 @@
import type { ITagType, ITagTypeStore } from './tag-type-store-type'; 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 { export default class FakeTagTypeStore implements ITagTypeStore {
tagTypes: ITagType[] = []; tagTypes: ITagType[] = [];

View File

@ -14,6 +14,7 @@ import type { ITagType, ITagTypeStore } from './tag-type-store-type';
import type { IUnleashConfig } from '../../types/option'; import type { IUnleashConfig } from '../../types/option';
import type EventService from '../events/event-service'; import type EventService from '../events/event-service';
import type { IAuditUser } from '../../types'; import type { IAuditUser } from '../../types';
import { NotFoundError } from '../../error';
export default class TagTypeService { export default class TagTypeService {
private tagTypeStore: ITagTypeStore; private tagTypeStore: ITagTypeStore;
@ -37,7 +38,11 @@ export default class TagTypeService {
} }
async getTagType(name: string): Promise<ITagType> { 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( async createTagType(

View File

@ -44,7 +44,7 @@ test('upsert stores new entries', async () => {
statusCodeSeries: data.statusCodeSeries, statusCodeSeries: data.statusCodeSeries,
}); });
expect(data2).toBeDefined(); expect(data2).toBeDefined();
expect(data2.count).toBe(1); expect(data2!.count).toBe(1);
}); });
test('upsert upserts', async () => { test('upsert upserts', async () => {
@ -68,7 +68,7 @@ test('upsert upserts', async () => {
statusCodeSeries: data.statusCodeSeries, statusCodeSeries: data.statusCodeSeries,
}); });
expect(data2).toBeDefined(); expect(data2).toBeDefined();
expect(data2.count).toBe(4); expect(data2!.count).toBe(4);
}); });
test('getAll returns all', async () => { test('getAll returns all', async () => {

View File

@ -825,7 +825,11 @@ export function registerPrometheusMetrics(
eventBus, eventBus,
events.REQUEST_ORIGIN, events.REQUEST_ORIGIN,
({ type, method, source }) => { ({ type, method, source }) => {
requestOriginCounter.increment({ type, method, source }); requestOriginCounter.increment({
type,
method,
source: source || 'unknown',
});
}, },
); );

View File

@ -19,7 +19,10 @@ export default function requireContentType(
} }
return (req, res, next) => { return (req, res, next) => {
const contentType = req.header('Content-Type'); const contentType = req.header('Content-Type');
if (is(contentType, acceptedContentTypes)) { if (
contentType !== undefined &&
is(contentType, acceptedContentTypes)
) {
next(); next();
} else { } else {
const error = new ContentTypeError( const error = new ContentTypeError(

View File

@ -125,8 +125,11 @@ const rbacMiddleware = (
params.id params.id
) { ) {
const { id } = params; const { id } = params;
const { project } = await segmentStore.get(id); const segment = await segmentStore.get(id);
projectId = project; if (segment === undefined) {
return false;
}
projectId = segment.project;
} }
return accessService.hasPermission( return accessService.hasPermission(

View File

@ -33,6 +33,7 @@ export const validateSchema = <S = SchemaId>(
schema: S, schema: S,
data: unknown, data: unknown,
): ISchemaValidationErrors<S> | undefined => { ): 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)) { if (!ajv.validate(schema, data)) {
return { return {
schema, schema,

View File

@ -253,7 +253,7 @@ class StrategyController extends Controller {
res, res,
strategySchema.$id, strategySchema.$id,
strategy, strategy,
{ location: `strategies/${strategy.name}` }, { location: `strategies/${strategy!.name}` },
); );
} }

View File

@ -12,8 +12,6 @@ import {
} from '../../openapi/spec/telemetry-settings-schema'; } from '../../openapi/spec/telemetry-settings-schema';
class TelemetryController extends Controller { class TelemetryController extends Controller {
config: IUnleashConfig;
openApiService: OpenApiService; openApiService: OpenApiService;
constructor( constructor(
@ -21,7 +19,6 @@ class TelemetryController extends Controller {
{ openApiService }: Pick<IUnleashServices, 'openApiService'>, { openApiService }: Pick<IUnleashServices, 'openApiService'>,
) { ) {
super(config); super(config);
this.config = config;
this.openApiService = openApiService; this.openApiService = openApiService;
this.route({ this.route({

View File

@ -110,7 +110,7 @@ class UserFeedbackController extends Controller {
feedbackId: req.params.id, feedbackId: req.params.id,
userId: req.user.id, userId: req.user.id,
neverShow: req.body.neverShow || false, 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( this.openApiService.respondWithValidation(

View File

@ -48,6 +48,7 @@ import {
RoleUpdatedEvent, RoleUpdatedEvent,
} from '../types'; } from '../types';
import type EventService from '../features/events/event-service'; import type EventService from '../features/events/event-service';
import { NotFoundError } from '../error';
const { ADMIN } = permissions; const { ADMIN } = permissions;
@ -536,6 +537,9 @@ export class AccessService {
async getRole(id: number): Promise<IRoleWithPermissions> { async getRole(id: number): Promise<IRoleWithPermissions> {
const role = await this.store.get(id); 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); const rolePermissions = await this.store.getPermissionsForRole(role.id);
return { return {
...role, ...role,
@ -549,6 +553,9 @@ export class AccessService {
this.store.getPermissionsForRole(roleId), this.store.getPermissionsForRole(roleId),
this.getUsersForRole(roleId), this.getUsersForRole(roleId),
]); ]);
if (role === undefined) {
throw new NotFoundError(`Could not find role with id ${roleId}`);
}
return { role, permissions: rolePerms, users }; return { role, permissions: rolePerms, users };
} }
@ -873,6 +880,11 @@ export class AccessService {
async validateRoleIsNotBuiltIn(roleId: number): Promise<void> { async validateRoleIsNotBuiltIn(roleId: number): Promise<void> {
const role = await this.store.get(roleId); const role = await this.store.get(roleId);
if (role === undefined) {
throw new InvalidOperationError(
'You cannot change a non-existing role',
);
}
if ( if (
role.type !== CUSTOM_PROJECT_ROLE_TYPE && role.type !== CUSTOM_PROJECT_ROLE_TYPE &&
role.type !== CUSTOM_ROOT_ROLE_TYPE role.type !== CUSTOM_ROOT_ROLE_TYPE

View File

@ -5,6 +5,7 @@ import type { IAccountStore, IUnleashStores } from '../types/stores';
import type { AccessService } from './access-service'; import type { AccessService } from './access-service';
import { RoleName } from '../types/model'; import { RoleName } from '../types/model';
import type { IAdminCount } from '../types/stores/account-store'; import type { IAdminCount } from '../types/stores/account-store';
import { NotFoundError } from '../error';
interface IUserWithRole extends IUser { interface IUserWithRole extends IUser {
rootRole: number; rootRole: number;
@ -46,7 +47,12 @@ export class AccountService {
} }
async getAccountByPersonalAccessToken(secret: string): Promise<IUser> { 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> { async getAdminCount(): Promise<IAdminCount> {

View File

@ -27,6 +27,7 @@ import type { IAddonDefinition } from '../types/model';
import { minutesToMilliseconds } from 'date-fns'; import { minutesToMilliseconds } from 'date-fns';
import type EventService from '../features/events/event-service'; import type EventService from '../features/events/event-service';
import { omitKeys } from '../util'; import { omitKeys } from '../util';
import { NotFoundError } from '../error';
const SUPPORTED_EVENTS = Object.keys(events).map((k) => events[k]); const SUPPORTED_EVENTS = Object.keys(events).map((k) => events[k]);
@ -110,7 +111,7 @@ export default class AddonService {
); );
return providerDefinitions.reduce((obj, definition) => { return providerDefinitions.reduce((obj, definition) => {
const sensitiveParams = definition.parameters const sensitiveParams = definition.parameters
.filter((p) => p.sensitive) ?.filter((p) => p.sensitive)
.map((p) => p.name); .map((p) => p.name);
const o = { ...obj }; const o = { ...obj };
@ -183,6 +184,9 @@ export default class AddonService {
async getAddon(id: number): Promise<IAddon> { async getAddon(id: number): Promise<IAddon> {
const addonConfig = await this.addonStore.get(id); const addonConfig = await this.addonStore.get(id);
if (addonConfig === undefined) {
throw new NotFoundError();
}
return this.filterSensitiveFields(addonConfig); return this.filterSensitiveFields(addonConfig);
} }
@ -240,7 +244,10 @@ export default class AddonService {
data: IAddonDto, data: IAddonDto,
auditUser: IAuditUser, auditUser: IAuditUser,
): Promise<IAddon> { ): 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); const addonConfig = await addonSchema.validateAsync(data);
await this.validateKnownProvider(addonConfig); await this.validateKnownProvider(addonConfig);
await this.validateRequiredParameters(addonConfig); await this.validateRequiredParameters(addonConfig);
@ -272,6 +279,10 @@ export default class AddonService {
async removeAddon(id: number, auditUser: IAuditUser): Promise<void> { async removeAddon(id: number, auditUser: IAuditUser): Promise<void> {
const existingConfig = await this.addonStore.get(id); const existingConfig = await this.addonStore.get(id);
if (existingConfig === undefined) {
/// No config, no need to delete
return;
}
await this.addonStore.delete(id); await this.addonStore.delete(id);
await this.eventService.storeEvent( await this.eventService.storeEvent(
new AddonConfigDeletedEvent({ new AddonConfigDeletedEvent({
@ -310,13 +321,14 @@ export default class AddonService {
}): Promise<boolean> { }): Promise<boolean> {
const providerDefinition = this.addonProviders[provider].definition; const providerDefinition = this.addonProviders[provider].definition;
const requiredParamsMissing = providerDefinition.parameters const requiredParamsMissing =
.filter((p) => p.required) providerDefinition.parameters
.map((p) => p.name) ?.filter((p) => p.required)
.filter( .map((p) => p.name)
(requiredParam) => .filter(
!Object.keys(parameters).includes(requiredParam), (requiredParam) =>
); !Object.keys(parameters).includes(requiredParam),
) || [];
if (requiredParamsMissing.length > 0) { if (requiredParamsMissing.length > 0) {
throw new ValidationError( throw new ValidationError(
`Missing required parameters: ${requiredParamsMissing.join( `Missing required parameters: ${requiredParamsMissing.join(

View File

@ -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); return this.store.get(secret);
} }
@ -245,7 +245,7 @@ export class ApiTokenService {
auditUser: IAuditUser, auditUser: IAuditUser,
): Promise<IApiToken> { ): Promise<IApiToken> {
const previous = (await this.store.get(secret))!; 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( await this.eventService.storeEvent(
new ApiTokenUpdatedEvent({ new ApiTokenUpdatedEvent({
auditUser, auditUser,
@ -258,7 +258,7 @@ export class ApiTokenService {
public async delete(secret: string, auditUser: IAuditUser): Promise<void> { public async delete(secret: string, auditUser: IAuditUser): Promise<void> {
if (await this.store.exists(secret)) { 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.store.delete(secret);
await this.eventService.storeEvent( await this.eventService.storeEvent(
new ApiTokenDeletedEvent({ new ApiTokenDeletedEvent({

View File

@ -13,6 +13,7 @@ import {
import type { IUser } from '../types/user'; import type { IUser } from '../types/user';
import type { IFavoriteProjectKey } from '../types/stores/favorite-projects'; import type { IFavoriteProjectKey } from '../types/stores/favorite-projects';
import type EventService from '../features/events/event-service'; import type EventService from '../features/events/event-service';
import { NotFoundError } from '../error';
export interface IFavoriteFeatureProps { export interface IFavoriteFeatureProps {
feature: string; feature: string;
@ -61,6 +62,11 @@ export class FavoritesService {
feature: feature, feature: feature,
userId: user.id, userId: user.id,
}); });
if (data === undefined) {
throw new NotFoundError(
`Feature with name ${feature} did not exist`,
);
}
await this.eventService.storeEvent( await this.eventService.storeEvent(
new FeatureFavoritedEvent({ new FeatureFavoritedEvent({
featureName: feature, featureName: feature,
@ -97,10 +103,13 @@ export class FavoritesService {
{ project, user }: IFavoriteProjectProps, { project, user }: IFavoriteProjectProps,
auditUser: IAuditUser, auditUser: IAuditUser,
): Promise<IFavoriteProject> { ): Promise<IFavoriteProject> {
const data = this.favoriteProjectsStore.addFavoriteProject({ const data = await this.favoriteProjectsStore.addFavoriteProject({
project, project,
userId: user.id, userId: user.id,
}); });
if (data === undefined) {
throw new NotFoundError(`Project with id ${project} was not found`);
}
await this.eventService.storeEvent( await this.eventService.storeEvent(
new ProjectFavoritedEvent({ new ProjectFavoritedEvent({
data: { data: {
@ -117,7 +126,7 @@ export class FavoritesService {
{ project, user }: IFavoriteProjectProps, { project, user }: IFavoriteProjectProps,
auditUser: IAuditUser, auditUser: IAuditUser,
): Promise<void> { ): Promise<void> {
const data = this.favoriteProjectsStore.delete({ const data = await this.favoriteProjectsStore.delete({
project: project, project: project,
userId: user.id, userId: user.id,
}); });
@ -130,7 +139,6 @@ export class FavoritesService {
auditUser, auditUser,
}), }),
); );
return data;
} }
async isFavoriteProject(favorite: IFavoriteProjectKey): Promise<boolean> { async isFavoriteProject(favorite: IFavoriteProjectKey): Promise<boolean> {

View File

@ -65,6 +65,9 @@ class FeatureTagService {
auditUser: IAuditUser, auditUser: IAuditUser,
): Promise<ITag> { ): Promise<ITag> {
const featureToggle = await this.featureToggleStore.get(featureName); const featureToggle = await this.featureToggleStore.get(featureName);
if (featureToggle === undefined) {
throw new NotFoundError();
}
const validatedTag = await tagSchema.validateAsync(tag); const validatedTag = await tagSchema.validateAsync(tag);
await this.createTagIfNeeded(validatedTag, auditUser); await this.createTagIfNeeded(validatedTag, auditUser);
await this.featureTagStore.tagFeature( await this.featureTagStore.tagFeature(
@ -180,6 +183,10 @@ class FeatureTagService {
auditUser: IAuditUser, auditUser: IAuditUser,
): Promise<void> { ): Promise<void> {
const featureToggle = await this.featureToggleStore.get(featureName); const featureToggle = await this.featureToggleStore.get(featureName);
if (featureToggle === undefined) {
/// No toggle, so no point in removing tags
return;
}
const tags = const tags =
await this.featureTagStore.getAllTagsForFeature(featureName); await this.featureTagStore.getAllTagsForFeature(featureName);
await this.featureTagStore.untagFeature(featureName, tag); await this.featureTagStore.untagFeature(featureName, tag);

View File

@ -30,6 +30,7 @@ import type { IUser } from '../types/user';
import type EventService from '../features/events/event-service'; import type EventService from '../features/events/event-service';
import { SSO_SYNC_USER } from '../db/group-store'; import { SSO_SYNC_USER } from '../db/group-store';
import type { IGroupWithProjectRoles } from '../types/stores/access-store'; import type { IGroupWithProjectRoles } from '../types/stores/access-store';
import { NotFoundError } from '../error';
const setsAreEqual = (firstSet, secondSet) => const setsAreEqual = (firstSet, secondSet) =>
firstSet.size === secondSet.size && firstSet.size === secondSet.size &&
@ -95,6 +96,9 @@ export class GroupService {
async getGroup(id: number): Promise<IGroupModel> { async getGroup(id: number): Promise<IGroupModel> {
const group = await this.groupStore.get(id); 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 groupUsers = await this.groupStore.getAllUsersByGroups([id]);
const users = await this.accountStore.getAllWithId( const users = await this.accountStore.getAllWithId(
groupUsers.map((u) => u.userId), groupUsers.map((u) => u.userId),
@ -104,7 +108,7 @@ export class GroupService {
async isScimGroup(id: number): Promise<boolean> { async isScimGroup(id: number): Promise<boolean> {
const group = await this.groupStore.get(id); const group = await this.groupStore.get(id);
return Boolean(group.scimId); return Boolean(group?.scimId);
} }
async createGroup( async createGroup(
@ -208,6 +212,10 @@ export class GroupService {
async deleteGroup(id: number, auditUser: IAuditUser): Promise<void> { async deleteGroup(id: number, auditUser: IAuditUser): Promise<void> {
const group = await this.groupStore.get(id); 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([ const existingUsers = await this.groupStore.getAllUsersByGroups([
group.id, group.id,
]); ]);

View File

@ -1,4 +1,4 @@
import crypto from 'crypto'; import crypto from 'node:crypto';
import type { Logger } from '../logger'; import type { Logger } from '../logger';
import { import {
type IAuditUser, type IAuditUser,
@ -23,6 +23,7 @@ import type { IUser } from '../types/user';
import { URL } from 'url'; import { URL } from 'url';
import { add } from 'date-fns'; import { add } from 'date-fns';
import type EventService from '../features/events/event-service'; import type EventService from '../features/events/event-service';
import { NotFoundError } from '../error';
export class PublicSignupTokenService { export class PublicSignupTokenService {
private store: IPublicSignupTokenStore; private store: IPublicSignupTokenStore;
@ -63,7 +64,11 @@ export class PublicSignupTokenService {
} }
public async get(secret: string): Promise<PublicSignupTokenSchema> { 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[]> { public async getAllTokens(): Promise<PublicSignupTokenSchema[]> {
@ -95,6 +100,9 @@ export class PublicSignupTokenService {
auditUser: IAuditUser, auditUser: IAuditUser,
): Promise<IUser> { ): Promise<IUser> {
const token = await this.get(secret); 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( const user = await this.userService.createUser(
{ {
...createUser, ...createUser,

View File

@ -35,7 +35,7 @@ export default class SessionService {
return this.sessionStore.getSessionsForUser(userId); return this.sessionStore.getSessionsForUser(userId);
} }
async getSession(sid: string): Promise<ISession> { async getSession(sid: string): Promise<ISession | undefined> {
return this.sessionStore.get(sid); return this.sessionStore.get(sid);
} }

View File

@ -1,5 +1,5 @@
const joi = require('joi'); import { nameType } from '../routes/util';
const { nameType } = require('../routes/util'); import joi from 'joi';
const strategySchema = joi const strategySchema = joi
.object() .object()

View File

@ -16,16 +16,8 @@ import {
StrategyReactivatedEvent, StrategyReactivatedEvent,
StrategyUpdatedEvent, StrategyUpdatedEvent,
} from '../types'; } from '../types';
import strategySchema from './strategy-schema';
const strategySchema = require('./strategy-schema'); import { NameExistsError } from '../error';
const NameExistsError = require('../error/name-exists-error');
const {
STRATEGY_CREATED,
STRATEGY_DELETED,
STRATEGY_DEPRECATED,
STRATEGY_REACTIVATED,
STRATEGY_UPDATED,
} = require('../types/events');
class StrategyService { class StrategyService {
private logger: Logger; private logger: Logger;
@ -48,7 +40,7 @@ class StrategyService {
return this.strategyStore.getAll(); return this.strategyStore.getAll();
} }
async getStrategy(name: string): Promise<IStrategy> { async getStrategy(name: string): Promise<IStrategy | undefined> {
return this.strategyStore.get(name); return this.strategyStore.get(name);
} }
@ -110,7 +102,7 @@ class StrategyService {
async createStrategy( async createStrategy(
value: IMinimalStrategy, value: IMinimalStrategy,
auditUser: IAuditUser, auditUser: IAuditUser,
): Promise<IStrategy> { ): Promise<IStrategy | undefined> {
const strategy = await strategySchema.validateAsync(value); const strategy = await strategySchema.validateAsync(value);
strategy.deprecated = false; strategy.deprecated = false;
await this._validateStrategyName(strategy); await this._validateStrategyName(strategy);
@ -158,9 +150,9 @@ class StrategyService {
} }
// This check belongs in the store. // This check belongs in the store.
_validateEditable(strategy: IStrategy): void { _validateEditable(strategy: IStrategy | undefined): void {
if (!strategy.editable) { if (!strategy?.editable) {
throw new Error(`Cannot edit strategy ${strategy.name}`); throw new Error(`Cannot edit strategy ${strategy?.name}`);
} }
} }
} }

View File

@ -7,5 +7,8 @@ test('should require url friendly type if defined', () => {
}; };
const { error } = tagSchema.validate(tag); 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'); expect(error.details[0].message).toEqual('"type" must be URL friendly');
}); });

View File

@ -6,7 +6,7 @@ test('should require a URLFriendly name but allow empty description and icon', (
}; };
const { error } = tagTypeSchema.validate(simpleTagType); 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', () => { 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); const { error } = tagTypeSchema.validate(tagType);
expect(error.details[0].message).toEqual('"description" 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'); expect(error!.details[1].message).toEqual('"icon" must be a string');
}); });
test('Should validate if all requirements are fulfilled', () => { test('Should validate if all requirements are fulfilled', () => {

View File

@ -15,7 +15,6 @@ import SettingService from './setting-service';
import FakeSettingStore from '../../test/fixtures/fake-setting-store'; import FakeSettingStore from '../../test/fixtures/fake-setting-store';
import { extractAuditInfoFromUser } from '../util'; import { extractAuditInfoFromUser } from '../util';
import { createFakeEventsService } from '../features'; import { createFakeEventsService } from '../features';
const config: IUnleashConfig = createTestConfig(); const config: IUnleashConfig = createTestConfig();
const systemUser = new User({ id: -1, username: 'system' }); 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_USERNAME = CUSTOM_ADMIN_USERNAME;
process.env.UNLEASH_DEFAULT_ADMIN_PASSWORD = CUSTOM_ADMIN_PASSWORD; process.env.UNLEASH_DEFAULT_ADMIN_PASSWORD = CUSTOM_ADMIN_PASSWORD;
const createTestConfig =
require('../../test/config/test-config').createTestConfig;
const config = createTestConfig(); const config = createTestConfig();
expect(config.authentication.initialAdminUser).toStrictEqual({ expect(config.authentication.initialAdminUser).toStrictEqual({

View File

@ -229,8 +229,11 @@ class UserService {
async getUser(id: number): Promise<IUserWithRootRole> { async getUser(id: number): Promise<IUserWithRootRole> {
const user = await this.store.get(id); 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); const rootRole = await this.accessService.getRootRoleForUser(id);
return { ...user, rootRole: rootRole.id }; return { ...user, id, rootRole: rootRole.id };
} }
async search(query: string): Promise<IUser[]> { async search(query: string): Promise<IUser[]> {
@ -416,7 +419,7 @@ class UserService {
async loginUser( async loginUser(
usernameOrEmail: string, usernameOrEmail: string,
password: string, password: string,
device?: { userAgent: string; ip: string }, device?: { userAgent?: string; ip: string },
): Promise<IUser> { ): Promise<IUser> {
const settings = await this.settingService.get<SimpleAuthSettings>( const settings = await this.settingService.get<SimpleAuthSettings>(
simpleAuthSettingsKey, simpleAuthSettingsKey,
@ -581,7 +584,7 @@ class UserService {
return { return {
token, token,
createdBy, createdBy,
email: user.email, email: user.email!,
name: user.name, name: user.name,
id: user.id, id: user.id,
role: { role: {
@ -632,7 +635,7 @@ class UserService {
const resetLink = await this.resetTokenService.createResetPasswordUrl( const resetLink = await this.resetTokenService.createResetPasswordUrl(
receiver.id, receiver.id,
user.username || user.email, user.username || user.email || SYSTEM_USER_AUDIT.username,
); );
this.passwordResetTimeouts[receiver.id] = setTimeout(() => { this.passwordResetTimeouts[receiver.id] = setTimeout(() => {
@ -640,8 +643,8 @@ class UserService {
}, 1000 * 60); // 1 minute }, 1000 * 60); // 1 minute
await this.emailService.sendResetMail( await this.emailService.sendResetMail(
receiver.name, receiver.name!,
receiver.email, receiverEmail,
resetLink.toString(), resetLink.toString(),
); );
return resetLink; return resetLink;

View File

@ -1,5 +1,6 @@
import Joi, { ValidationError } from 'joi'; import Joi, { ValidationError } from 'joi';
import type { IUser } from './user'; import type { IUser } from './user';
import { SYSTEM_USER_AUDIT } from './core';
export interface IGroup { export interface IGroup {
id: number; id: number;
@ -60,7 +61,7 @@ export interface IGroupModelWithAddedAt extends IGroupModel {
export default class Group implements IGroup { export default class Group implements IGroup {
type: string; type: string;
createdAt: Date; createdAt?: Date;
createdBy: string; createdBy: string;
@ -95,9 +96,9 @@ export default class Group implements IGroup {
this.id = id; this.id = id;
this.name = name; this.name = name;
this.rootRole = rootRole; this.rootRole = rootRole;
this.description = description; this.description = description || '';
this.mappingsSSO = mappingsSSO; this.mappingsSSO = mappingsSSO || [];
this.createdBy = createdBy; this.createdBy = createdBy || SYSTEM_USER_AUDIT.username;
this.createdAt = createdAt; this.createdAt = createdAt;
this.scimId = scimId; this.scimId = scimId;
} }

View File

@ -61,7 +61,7 @@ export interface IFeatureStrategy {
export interface FeatureToggleDTO { export interface FeatureToggleDTO {
name: string; name: string;
description?: string; description?: string | null;
type?: string; type?: string;
stale?: boolean; stale?: boolean;
archived?: boolean; archived?: boolean;
@ -91,7 +91,7 @@ export interface IFeatureToggleListItem extends FeatureToggle {
export interface IFeatureToggleClient { export interface IFeatureToggleClient {
name: string; name: string;
description: string; description: string | undefined | null;
type: string; type: string;
project: string; project: string;
stale: boolean; stale: boolean;

View File

@ -19,7 +19,7 @@ export interface IAccountStore extends Store<IUser, number> {
getAllWithId(userIdList: number[]): Promise<IUser[]>; getAllWithId(userIdList: number[]): Promise<IUser[]>;
getByQuery(idQuery: IUserLookup): Promise<IUser>; getByQuery(idQuery: IUserLookup): Promise<IUser>;
count(): Promise<number>; count(): Promise<number>;
getAccountByPersonalAccessToken(secret: string): Promise<IUser>; getAccountByPersonalAccessToken(secret: string): Promise<IUser | undefined>;
markSeenAt(secrets: string[]): Promise<void>; markSeenAt(secrets: string[]): Promise<void>;
getAdminCount(): Promise<IAdminCount>; getAdminCount(): Promise<IAdminCount>;
getAdmins(): Promise<MinimalUser[]>; getAdmins(): Promise<MinimalUser[]>;

View File

@ -4,7 +4,7 @@ import type { Store } from './store';
export interface IApiTokenStore extends Store<IApiToken, string> { export interface IApiTokenStore extends Store<IApiToken, string> {
getAllActive(): Promise<IApiToken[]>; getAllActive(): Promise<IApiToken[]>;
insert(newToken: IApiTokenCreate): 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>; markSeenAt(secrets: string[]): Promise<void>;
count(): Promise<number>; count(): Promise<number>;
countByType(): Promise<Map<string, number>>; countByType(): Promise<Map<string, number>>;

View File

@ -10,5 +10,5 @@ export interface IFavoriteFeaturesStore
extends Store<IFavoriteFeature, IFavoriteFeatureKey> { extends Store<IFavoriteFeature, IFavoriteFeatureKey> {
addFavoriteFeature( addFavoriteFeature(
favorite: IFavoriteFeatureKey, favorite: IFavoriteFeatureKey,
): Promise<IFavoriteFeature>; ): Promise<IFavoriteFeature | undefined>;
} }

View File

@ -10,5 +10,5 @@ export interface IFavoriteProjectsStore
extends Store<IFavoriteProject, IFavoriteProjectKey> { extends Store<IFavoriteProject, IFavoriteProjectKey> {
addFavoriteProject( addFavoriteProject(
favorite: IFavoriteProjectKey, favorite: IFavoriteProjectKey,
): Promise<IFavoriteProject>; ): Promise<IFavoriteProject | undefined>;
} }

View File

@ -4,7 +4,7 @@ export interface IResetTokenCreate {
reset_token: string; reset_token: string;
user_id: number; user_id: number;
expires_at: Date; expires_at: Date;
created_by?: string; created_by: string;
} }
export interface IResetToken { export interface IResetToken {

View File

@ -27,7 +27,7 @@ export class FakeInactiveUsersStore implements IInactiveUsersStore {
id: user.id, id: user.id,
name: user.name, name: user.name,
username: user.username, username: user.username,
email: user.email, email: user.email!,
seen_at: user.seenAt, seen_at: user.seenAt,
created_at: user.createdAt || new Date(), created_at: user.createdAt || new Date(),
}; };

View File

@ -1,8 +1,8 @@
import { AnyEventEmitter } from './anyEventEmitter'; import { AnyEventEmitter } from './anyEventEmitter';
test('AnyEventEmitter', () => { test('AnyEventEmitter', () => {
const events = []; const events: string[] = [];
const results = []; const results: boolean[] = [];
class MyEventEmitter extends AnyEventEmitter {} class MyEventEmitter extends AnyEventEmitter {}
const myEventEmitter = new MyEventEmitter(); const myEventEmitter = new MyEventEmitter();

View File

@ -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); return items.map((item) => item.id);
}; };

View File

@ -1,4 +1,4 @@
import { SYSTEM_USER } from '../../lib/types'; import { SYSTEM_USER, SYSTEM_USER_AUDIT } from '../../lib/types';
import type { import type {
IApiRequest, IApiRequest,
IApiUser, IApiUser,
@ -8,7 +8,9 @@ import type {
} from '../server-impl'; } from '../server-impl';
export function extractUsernameFromUser(user: IUser | IApiUser): string { 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 { export function extractUsername(req: IAuthRequest | IApiRequest): string {

View File

@ -1,5 +1,5 @@
export const snakeCase = (input: string): string => { export const snakeCase = (input: string): string => {
const result = []; const result: string[] = [];
const splitString = input.split(''); const splitString = input.split('');
for (let i = 0; i < splitString.length; i++) { for (let i = 0; i < splitString.length; i++) {
const char = splitString[i]; const char = splitString[i];

View File

@ -7,7 +7,7 @@ export interface HourBucket {
export function generateHourBuckets(hours: number): HourBucket[] { export function generateHourBuckets(hours: number): HourBucket[] {
const start = startOfHour(new Date()); const start = startOfHour(new Date());
const result = []; const result: HourBucket[] = [];
for (let i = 0; i < hours; i++) { for (let i = 0; i < hours; i++) {
result.push({ timestamp: subHours(start, i) }); result.push({ timestamp: subHours(start, i) });
@ -19,7 +19,7 @@ export function generateHourBuckets(hours: number): HourBucket[] {
export function generateDayBuckets(days: number): HourBucket[] { export function generateDayBuckets(days: number): HourBucket[] {
const start = endOfDay(subDays(new Date(), 1)); const start = endOfDay(subDays(new Date(), 1));
const result = []; const result: HourBucket[] = [];
for (let i = 0; i < days; i++) { for (let i = 0; i < days; i++) {
result.push({ timestamp: subDays(start, i) }); result.push({ timestamp: subDays(start, i) });

View File

@ -1,4 +1,7 @@
export const validateOrigin = (origin: string): boolean => { export const validateOrigin = (origin: string | undefined): boolean => {
if (origin === undefined) {
return false;
}
if (origin === '*') { if (origin === '*') {
return true; return true;
} }
@ -9,7 +12,7 @@ export const validateOrigin = (origin: string): boolean => {
try { try {
const parsed = new URL(origin); const parsed = new URL(origin);
return parsed.origin && parsed.origin === origin; return typeof parsed.origin === 'string' && parsed.origin === origin;
} catch { } catch {
return false; return false;
} }

View File

@ -1,6 +1,4 @@
// export module version // export module version
require('pkginfo')(module, 'version'); require('pkginfo')(module, 'version');
const { version } = module.exports; const { version } = module.exports;
export default version; export default version;
module.exports = version;

View File

@ -61,8 +61,8 @@ test('should allow setting pool size', () => {
disableMigration: false, disableMigration: false,
}; };
const config = createConfig({ db }); const config = createConfig({ db });
expect(config.db.pool.min).toBe(min); expect(config.db.pool!.min).toBe(min);
expect(config.db.pool.max).toBe(max); expect(config.db.pool!.max).toBe(max);
expect(config.db.driver).toBe('postgres'); expect(config.db.driver).toBe('postgres');
}); });

View File

@ -43,7 +43,7 @@ test('Should always return token type in lowercase', async () => {
}); });
const storedToken = await apiTokenStore.get('some-secret'); const storedToken = await apiTokenStore.get('some-secret');
expect(storedToken.type).toBe('frontend'); expect(storedToken!.type).toBe('frontend');
const { body } = await app.request const { body } = await app.request
.get('/api/admin/projects/default/api-tokens') .get('/api/admin/projects/default/api-tokens')

View File

@ -187,7 +187,7 @@ test('all tags are listed in the root "tags" list', async () => {
// dictionary of all invalid tags found in the spec // dictionary of all invalid tags found in the spec
let invalidTags = {}; let invalidTags = {};
for (const [path, data] of Object.entries(spec.paths)) { 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 // ensure that the list of tags for every operation is a subset of
// the list of tags defined on the root level // 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) { if (Object.keys(invalidTags).length) {
// create a human-readable list of invalid tags per operation // create a human-readable list of invalid tags per operation
const msgs = Object.entries(invalidTags).flatMap(([path, data]) => const msgs = Object.entries(invalidTags).flatMap(([path, data]) =>
Object.entries(data).map( Object.entries(data!).map(
([operation, opData]) => ([operation, opData]) =>
`${operation.toUpperCase()} ${path} (operation id: ${ `${operation.toUpperCase()} ${path} (operation id: ${
opData.operationId opData.operationId
@ -247,7 +247,7 @@ test('all API operations have non-empty summaries and descriptions', async () =>
.expect(200); .expect(200);
const anomalies = Object.entries(spec.paths).flatMap(([path, data]) => { const anomalies = Object.entries(spec.paths).flatMap(([path, data]) => {
return Object.entries(data) return Object.entries(data!)
.map(([verb, operationDescription]) => { .map(([verb, operationDescription]) => {
if ( if (
operationDescription.summary && operationDescription.summary &&
@ -260,6 +260,7 @@ test('all API operations have non-empty summaries and descriptions', async () =>
}) })
.filter(Boolean) .filter(Boolean)
.map( .map(
// @ts-expect-error - requesting an iterator where none could be found
([verb, operationId]) => ([verb, operationId]) =>
`${verb.toUpperCase()} ${path} (operation ID: ${operationId})`, `${verb.toUpperCase()} ${path} (operation ID: ${operationId})`,
); );

View File

@ -357,6 +357,7 @@ async function createApp(
}, },
}); });
const services = createServices(stores, config, db); const services = createServices(stores, config, db);
// @ts-expect-error We don't have a database for sessions here.
const unleashSession = sessionDb(config, undefined); const unleashSession = sessionDb(config, undefined);
const app = await getApp(config, stores, services, unleashSession, db); const app = await getApp(config, stores, services, unleashSession, db);
const request = supertest.agent(app); const request = supertest.agent(app);
@ -411,6 +412,7 @@ export async function setupAppWithoutSupertest(
}, },
}); });
const services = createServices(stores, config, db); 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 unleashSession = sessionDb(config, undefined);
const app = await getApp(config, stores, services, unleashSession, db); const app = await getApp(config, stores, services, unleashSession, db);
const server = app.listen(0); const server = app.listen(0);
@ -453,7 +455,7 @@ export async function setupAppWithAuth(
export async function setupAppWithCustomAuth( export async function setupAppWithCustomAuth(
stores: IUnleashStores, stores: IUnleashStores,
preHook: Function, preHook?: Function,
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
customOptions?: any, customOptions?: any,
db?: Db, db?: Db,

View File

@ -120,7 +120,7 @@ const seedSegmentsDatabase = async (
assert(segments.length === spec.segmentsPerFeature); assert(segments.length === spec.segmentsPerFeature);
const addSegment = (feature: IFeatureToggleClient, segment: ISegment) => { 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) { for (const feature of features) {

View File

@ -80,6 +80,8 @@ const mapSegmentSchemaToISegment = (
...segment, ...segment,
name: segment.name || `test-segment ${index ?? 'unnumbered'}`, name: segment.name || `test-segment ${index ?? 'unnumbered'}`,
createdAt: new Date(), createdAt: new Date(),
description: '',
project: undefined,
}); });
export const seedDatabaseForPlaygroundTest = async ( export const seedDatabaseForPlaygroundTest = async (
@ -116,11 +118,13 @@ export const seedDatabaseForPlaygroundTest = async (
// create feature // create feature
const toggle = await database.stores.featureToggleStore.create( const toggle = await database.stores.featureToggleStore.create(
feature.project, feature.project!,
{ {
...feature, ...feature,
createdAt: undefined, createdAt: undefined,
variants: null, variants: [],
description: undefined,
impressionData: false,
createdByUserId: 9999, createdByUserId: 9999,
}, },
); );
@ -133,7 +137,7 @@ export const seedDatabaseForPlaygroundTest = async (
); );
await database.stores.featureToggleStore.saveVariants( await database.stores.featureToggleStore.saveVariants(
feature.project, feature.project!,
feature.name, feature.name,
[ [
...(feature.variants ?? []).map((variant) => ({ ...(feature.variants ?? []).map((variant) => ({
@ -791,7 +795,7 @@ describe('the playground service (e2e)', () => {
unmappedFeature.strategies?.forEach( unmappedFeature.strategies?.forEach(
(unmappedStrategy) => { (unmappedStrategy) => {
const mappedStrategySegments: PlaygroundSegmentSchema[] = const mappedStrategySegments: PlaygroundSegmentSchema[] =
strategies[unmappedStrategy.id] strategies[unmappedStrategy.id!]
.segments; .segments;
const unmappedSegments = const unmappedSegments =
@ -808,7 +812,7 @@ describe('the playground service (e2e)', () => {
).toEqual([...unmappedSegments].sort()); ).toEqual([...unmappedSegments].sort());
switch ( switch (
strategies[unmappedStrategy.id].result strategies[unmappedStrategy.id!].result
) { ) {
case true: case true:
// If a strategy is considered true, _all_ segments // If a strategy is considered true, _all_ segments
@ -975,7 +979,7 @@ describe('the playground service (e2e)', () => {
...feature, ...feature,
// use a constraint that will never be true // use a constraint that will never be true
strategies: [ strategies: [
...feature.strategies.map((strategy) => ({ ...feature.strategies!.map((strategy) => ({
...strategy, ...strategy,
constraints: [ constraints: [
{ {

View File

@ -103,7 +103,7 @@ test('Should create a reset link with unleashUrl with context path', async () =>
const url = await resetToken.createResetPasswordUrl( const url = await resetToken.createResetPasswordUrl(
userIdToCreateResetFor, userIdToCreateResetFor,
adminUser.username, adminUser.username!,
); );
expect(url.toString().substring(0, url.toString().indexOf('='))).toBe( expect(url.toString().substring(0, url.toString().indexOf('='))).toBe(
`${localConfig.server.unleashUrl}/reset-password?token`, `${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 () => { test('Should create a welcome link', async () => {
const url = await resetTokenService.createNewUserUrl( const url = await resetTokenService.createNewUserUrl(
userIdToCreateResetFor, userIdToCreateResetFor,
adminUser.username, adminUser.username!,
); );
const urlS = url.toString(); const urlS = url.toString();
expect(urlS.substring(0, urlS.indexOf('='))).toBe( 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 () => { test('Tokens should be one-time only', async () => {
const token = await resetTokenService.createToken( const token = await resetTokenService.createToken(
userIdToCreateResetFor, userIdToCreateResetFor,
adminUser.username, adminUser.username!,
); );
const accessGranted = await resetTokenService.useAccessToken(token); 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 () => { test('Creating a new token should expire older tokens', async () => {
const firstToken = await resetTokenService.createToken( const firstToken = await resetTokenService.createToken(
userIdToCreateResetFor, userIdToCreateResetFor,
adminUser.username, adminUser.username!,
); );
const secondToken = await resetTokenService.createToken( const secondToken = await resetTokenService.createToken(
userIdToCreateResetFor, userIdToCreateResetFor,
adminUser.username, adminUser.username!,
); );
await expect(async () => await expect(async () =>
resetTokenService.isValid(firstToken.token), 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 () => { test('Retrieving valid invitation links should retrieve an object with userid key and token value', async () => {
const token = await resetTokenService.createToken( const token = await resetTokenService.createToken(
userIdToCreateResetFor, userIdToCreateResetFor,
adminUser.username, adminUser.username!,
); );
expect(token).toBeTruthy(); expect(token).toBeTruthy();
const activeInvitations = await resetTokenService.getActiveInvitations(); const activeInvitations = await resetTokenService.getActiveInvitations();

View File

@ -37,10 +37,10 @@ test('get token returns the token when exists', async () => {
}); });
const foundToken = await stores.apiTokenStore.get('abcde321'); const foundToken = await stores.apiTokenStore.get('abcde321');
expect(foundToken).toBeDefined(); expect(foundToken).toBeDefined();
expect(foundToken.secret).toBe(newToken.secret); expect(foundToken!.secret).toBe(newToken.secret);
expect(foundToken.environment).toBe(newToken.environment); expect(foundToken!.environment).toBe(newToken.environment);
expect(foundToken.tokenName).toBe(newToken.tokenName); expect(foundToken!.tokenName).toBe(newToken.tokenName);
expect(foundToken.type).toBe(newToken.type); expect(foundToken!.type).toBe(newToken.type);
}); });
describe('count deprecated tokens', () => { describe('count deprecated tokens', () => {

View File

@ -138,8 +138,8 @@ test('Merge keeps value for single row in database', async () => {
const stored = await clientApplicationsStore.get( const stored = await clientApplicationsStore.get(
clientRegistration.appName, clientRegistration.appName,
); );
expect(stored.color).toBe(clientRegistration.color); expect(stored!.color).toBe(clientRegistration.color);
expect(stored.description).toBe('new description'); expect(stored!.description).toBe('new description');
}); });
test('Multi row merge also works', async () => { 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!)), clients.map(async (c) => clientApplicationsStore.get(c.appName!)),
); );
stored.forEach((s, i) => { stored.forEach((s, i) => {
expect(s.description).toBe(clients[i].description); expect(s!.description).toBe(clients[i].description);
expect(s.icon).toBe('red'); expect(s!.icon).toBe('red');
}); });
}); });

View File

@ -124,7 +124,7 @@ test('Copying features also copies variants', async () => {
featureName: featureName, featureName: featureName,
environment: 'clone', environment: 'clone',
}); });
expect(cloned.variants).toMatchObject([variant]); expect(cloned!.variants).toMatchObject([variant]);
}); });
test('Copying strategies also copies strategy variants', async () => { test('Copying strategies also copies strategy variants', async () => {

View File

@ -46,8 +46,8 @@ test('should tag feature', async () => {
}); });
expect(featureTags).toHaveLength(1); expect(featureTags).toHaveLength(1);
expect(featureTags[0]).toStrictEqual(tag); expect(featureTags[0]).toStrictEqual(tag);
expect(featureTag.featureName).toBe(featureName); expect(featureTag!.featureName).toBe(featureName);
expect(featureTag.tagValue).toBe(tag.value); expect(featureTag!.tagValue).toBe(tag.value);
}); });
test('feature tag exists', async () => { test('feature tag exists', async () => {

View File

@ -52,8 +52,8 @@ describe('update lifetimes', () => {
); );
expect(updated?.lifetimeDays).toBe(newLifetime); expect(updated?.lifetimeDays).toBe(newLifetime);
const fromStore = await featureTypeStore.get(type.id);
expect(updated).toMatchObject(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