mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	chore: make it build with strict null checks set to true (#9554)
As part of preparation for ESM and node/TSC updates, this PR will make Unleash build with strictNullChecks set to true, since that's what's in our tsconfig file. Hence, this PR also removes the `--strictNullChecks false` flag in our compile tasks in package.json. TL;DR - Clean up your code rather than turning off compiler security features :)
This commit is contained in:
		
							parent
							
								
									d082e5eb25
								
							
						
					
					
						commit
						efcf04487d
					
				
							
								
								
									
										12
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								package.json
									
									
									
									
									
								
							@ -35,21 +35,21 @@
 | 
				
			|||||||
  "scripts": {
 | 
					  "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": {
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
				
			||||||
 | 
				
			|||||||
@ -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')) {
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
				
			|||||||
@ -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') {
 | 
				
			||||||
 | 
				
			|||||||
@ -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 = () => {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
        demoAllowAdminLogin: parseEnvVarBoolean(
 | 
					        demoAllowAdminLogin: parseEnvVarBoolean(
 | 
				
			||||||
            process.env.AUTH_DEMO_ALLOW_ADMIN_LOGIN,
 | 
					            process.env.AUTH_DEMO_ALLOW_ADMIN_LOGIN,
 | 
				
			||||||
            false,
 | 
					            false,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    enableApiToken: parseEnvVarBoolean(process.env.AUTH_ENABLE_API_TOKEN, true),
 | 
					        enableApiToken: parseEnvVarBoolean(
 | 
				
			||||||
 | 
					            process.env.AUTH_ENABLE_API_TOKEN,
 | 
				
			||||||
 | 
					            true,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
        type: authTypeFromString(process.env.AUTH_TYPE),
 | 
					        type: authTypeFromString(process.env.AUTH_TYPE),
 | 
				
			||||||
        customAuthHandler: defaultCustomAuthDenyAll,
 | 
					        customAuthHandler: defaultCustomAuthDenyAll,
 | 
				
			||||||
        createAdminUser: true,
 | 
					        createAdminUser: true,
 | 
				
			||||||
        initialAdminUser: parseEnvVarInitialAdminUser(),
 | 
					        initialAdminUser: parseEnvVarInitialAdminUser(),
 | 
				
			||||||
        initApiTokens: [],
 | 
					        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) || {},
 | 
				
			||||||
 | 
				
			|||||||
@ -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',
 | 
				
			||||||
 | 
				
			|||||||
@ -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'],
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
				
			|||||||
@ -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> {
 | 
				
			||||||
 | 
				
			|||||||
@ -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> {
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
 | 
				
			|||||||
@ -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: [],
 | 
				
			||||||
 | 
				
			|||||||
@ -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 || []),
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -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,
 | 
				
			||||||
 | 
				
			|||||||
@ -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,
 | 
				
			||||||
 | 
				
			|||||||
@ -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,
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
 | 
				
			|||||||
@ -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 (
 | 
				
			||||||
 | 
					            dto.data.contextFields?.filter(
 | 
				
			||||||
                (contextField) =>
 | 
					                (contextField) =>
 | 
				
			||||||
                    !availableContextFields.some(
 | 
					                    !availableContextFields.some(
 | 
				
			||||||
                        (availableField) =>
 | 
					                        (availableField) =>
 | 
				
			||||||
                            availableField.name === contextField.name,
 | 
					                            availableField.name === contextField.name,
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
 | 
					            ) || []
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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,9 +705,11 @@ 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
 | 
				
			||||||
 | 
					            .filter((v) => typeof v === 'string')
 | 
				
			||||||
 | 
					            .map((val) =>
 | 
				
			||||||
                (Array.isArray(fields)
 | 
					                (Array.isArray(fields)
 | 
				
			||||||
                ? val.split(/:(.+)/).filter(Boolean)
 | 
					                    ? val!.split(/:(.+)/).filter(Boolean)
 | 
				
			||||||
                    : [val]
 | 
					                    : [val]
 | 
				
			||||||
                ).map((s) => s.trim()),
 | 
					                ).map((s) => s.trim()),
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
 | 
				
			|||||||
@ -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!,
 | 
				
			||||||
 | 
					                        };
 | 
				
			||||||
 | 
					                    }),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
 | 
				
			|||||||
@ -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(
 | 
				
			||||||
 | 
				
			|||||||
@ -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,6 +296,7 @@ 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()
 | 
				
			||||||
            ) {
 | 
					            ) {
 | 
				
			||||||
@ -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()
 | 
				
			||||||
            ) {
 | 
					            ) {
 | 
				
			||||||
 | 
				
			|||||||
@ -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 }),
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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(
 | 
				
			||||||
 | 
				
			|||||||
@ -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,
 | 
				
			||||||
 | 
				
			|||||||
@ -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>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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 {
 | 
				
			||||||
 | 
				
			|||||||
@ -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(
 | 
				
			||||||
 | 
					                    frontendSettingsKey,
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
                        frontendApiOrigins: this.config.frontendApiOrigins,
 | 
					                        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);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -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,
 | 
				
			||||||
 | 
					                    }),
 | 
				
			||||||
                ]),
 | 
					                ]),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
 | 
				
			|||||||
@ -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,
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
				
			||||||
 | 
				
			|||||||
@ -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,
 | 
				
			||||||
 | 
				
			|||||||
@ -27,6 +27,7 @@ export default class RemoteAddressStrategy extends Strategy {
 | 
				
			|||||||
                        return false;
 | 
					                        return false;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -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)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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,
 | 
				
			||||||
 | 
				
			|||||||
@ -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(
 | 
				
			||||||
 | 
				
			|||||||
@ -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[]> {
 | 
				
			||||||
 | 
				
			|||||||
@ -112,7 +112,7 @@ export class ProjectInsightsService {
 | 
				
			|||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
            health: project.health || 0,
 | 
					            health: project?.health || 0,
 | 
				
			||||||
            features: features,
 | 
					            features: features,
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -2472,7 +2472,7 @@ test('should not delete project-bound api tokens still bound to project', async
 | 
				
			|||||||
    await projectService.deleteProject(project1, user, auditUser);
 | 
					    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 () => {
 | 
				
			||||||
 | 
				
			|||||||
@ -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,
 | 
				
			||||||
 | 
				
			|||||||
@ -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 () => {
 | 
				
			||||||
 | 
				
			|||||||
@ -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)];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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(
 | 
				
			||||||
 | 
				
			|||||||
@ -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[] = [];
 | 
				
			||||||
 | 
				
			|||||||
@ -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(
 | 
				
			||||||
 | 
				
			|||||||
@ -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 () => {
 | 
				
			||||||
 | 
				
			|||||||
@ -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',
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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(
 | 
				
			||||||
 | 
				
			|||||||
@ -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(
 | 
				
			||||||
 | 
				
			|||||||
@ -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,
 | 
				
			||||||
 | 
				
			|||||||
@ -253,7 +253,7 @@ class StrategyController extends Controller {
 | 
				
			|||||||
            res,
 | 
					            res,
 | 
				
			||||||
            strategySchema.$id,
 | 
					            strategySchema.$id,
 | 
				
			||||||
            strategy,
 | 
					            strategy,
 | 
				
			||||||
            { location: `strategies/${strategy.name}` },
 | 
					            { location: `strategies/${strategy!.name}` },
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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({
 | 
				
			||||||
 | 
				
			|||||||
@ -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(
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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> {
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
					                ?.filter((p) => p.required)
 | 
				
			||||||
                .map((p) => p.name)
 | 
					                .map((p) => p.name)
 | 
				
			||||||
                .filter(
 | 
					                .filter(
 | 
				
			||||||
                    (requiredParam) =>
 | 
					                    (requiredParam) =>
 | 
				
			||||||
                        !Object.keys(parameters).includes(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(
 | 
				
			||||||
 | 
				
			|||||||
@ -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({
 | 
				
			||||||
 | 
				
			|||||||
@ -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> {
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
				
			||||||
 | 
				
			|||||||
@ -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,
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
 | 
				
			|||||||
@ -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,
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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()
 | 
				
			||||||
 | 
				
			|||||||
@ -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}`);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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');
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -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', () => {
 | 
				
			||||||
 | 
				
			|||||||
@ -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({
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
 | 
				
			|||||||
@ -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[]>;
 | 
				
			||||||
 | 
				
			|||||||
@ -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>>;
 | 
				
			||||||
 | 
				
			|||||||
@ -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>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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 {
 | 
				
			||||||
 | 
				
			|||||||
@ -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(),
 | 
				
			||||||
                    };
 | 
					                    };
 | 
				
			||||||
 | 
				
			|||||||
@ -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();
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -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 {
 | 
				
			||||||
 | 
				
			|||||||
@ -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];
 | 
				
			||||||
 | 
				
			|||||||
@ -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) });
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -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');
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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')
 | 
				
			||||||
 | 
				
			|||||||
@ -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})`,
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
 | 
				
			|||||||
@ -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,
 | 
				
			||||||
 | 
				
			|||||||
@ -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) {
 | 
				
			||||||
 | 
				
			|||||||
@ -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: [
 | 
				
			||||||
                                            {
 | 
					                                            {
 | 
				
			||||||
 | 
				
			|||||||
@ -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();
 | 
				
			||||||
 | 
				
			|||||||
@ -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', () => {
 | 
				
			||||||
 | 
				
			|||||||
@ -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');
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -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 () => {
 | 
				
			||||||
 | 
				
			|||||||
@ -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 () => {
 | 
				
			||||||
 | 
				
			|||||||
@ -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
		Loading…
	
		Reference in New Issue
	
	Block a user