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 = () => {
|
||||||
demoAllowAdminLogin: parseEnvVarBoolean(
|
return {
|
||||||
process.env.AUTH_DEMO_ALLOW_ADMIN_LOGIN,
|
demoAllowAdminLogin: parseEnvVarBoolean(
|
||||||
false,
|
process.env.AUTH_DEMO_ALLOW_ADMIN_LOGIN,
|
||||||
),
|
false,
|
||||||
enableApiToken: parseEnvVarBoolean(process.env.AUTH_ENABLE_API_TOKEN, true),
|
),
|
||||||
type: authTypeFromString(process.env.AUTH_TYPE),
|
enableApiToken: parseEnvVarBoolean(
|
||||||
customAuthHandler: defaultCustomAuthDenyAll,
|
process.env.AUTH_ENABLE_API_TOKEN,
|
||||||
createAdminUser: true,
|
true,
|
||||||
initialAdminUser: parseEnvVarInitialAdminUser(),
|
),
|
||||||
initApiTokens: [],
|
type: authTypeFromString(process.env.AUTH_TYPE),
|
||||||
|
customAuthHandler: defaultCustomAuthDenyAll,
|
||||||
|
createAdminUser: true,
|
||||||
|
initialAdminUser: parseEnvVarInitialAdminUser(),
|
||||||
|
initApiTokens: [],
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultImport: WithOptional<IImportOption, 'file'> = {
|
const defaultImport: WithOptional<IImportOption, 'file'> = {
|
||||||
@ -563,7 +568,7 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
|||||||
const initApiTokens = loadInitApiTokens();
|
const initApiTokens = loadInitApiTokens();
|
||||||
|
|
||||||
const authentication: IAuthOption = mergeAll([
|
const authentication: IAuthOption = mergeAll([
|
||||||
defaultAuthentication,
|
buildDefaultAuthOption(),
|
||||||
(options.authentication
|
(options.authentication
|
||||||
? removeUndefinedKeys(options.authentication)
|
? removeUndefinedKeys(options.authentication)
|
||||||
: options.authentication) || {},
|
: options.authentication) || {},
|
||||||
|
|||||||
@ -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 (
|
||||||
(contextField) =>
|
dto.data.contextFields?.filter(
|
||||||
!availableContextFields.some(
|
(contextField) =>
|
||||||
(availableField) =>
|
!availableContextFields.some(
|
||||||
availableField.name === contextField.name,
|
(availableField) =>
|
||||||
),
|
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,12 +705,14 @@ const applyMultiQueryParams = (
|
|||||||
) => (dbSubQuery: Knex.QueryBuilder) => Knex.QueryBuilder,
|
) => (dbSubQuery: Knex.QueryBuilder) => Knex.QueryBuilder,
|
||||||
): void => {
|
): void => {
|
||||||
queryParams.forEach((param) => {
|
queryParams.forEach((param) => {
|
||||||
const values = param.values.map((val) =>
|
const values = param.values
|
||||||
(Array.isArray(fields)
|
.filter((v) => typeof v === 'string')
|
||||||
? val.split(/:(.+)/).filter(Boolean)
|
.map((val) =>
|
||||||
: [val]
|
(Array.isArray(fields)
|
||||||
).map((s) => s.trim()),
|
? val!.split(/:(.+)/).filter(Boolean)
|
||||||
);
|
: [val]
|
||||||
|
).map((s) => s.trim()),
|
||||||
|
);
|
||||||
const baseSubQuery = createBaseQuery(values);
|
const baseSubQuery = createBaseQuery(values);
|
||||||
|
|
||||||
switch (param.operator) {
|
switch (param.operator) {
|
||||||
|
|||||||
@ -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,8 +296,9 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
queryModifiers.date &&
|
||||||
new Date(feature[queryModifiers.dateAccessor]).getTime() >=
|
new Date(feature[queryModifiers.dateAccessor]).getTime() >=
|
||||||
new Date(queryModifiers.date).getTime()
|
new Date(queryModifiers.date).getTime()
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -303,6 +307,7 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
|
|||||||
feature[queryModifiers.dateAccessor],
|
feature[queryModifiers.dateAccessor],
|
||||||
).getTime();
|
).getTime();
|
||||||
if (
|
if (
|
||||||
|
queryModifiers.range &&
|
||||||
featureDate >= new Date(queryModifiers.range[0]).getTime() &&
|
featureDate >= new Date(queryModifiers.range[0]).getTime() &&
|
||||||
featureDate <= new Date(queryModifiers.range[1]).getTime()
|
featureDate <= new Date(queryModifiers.range[1]).getTime()
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -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(
|
||||||
frontendApiOrigins: this.config.frontendApiOrigins,
|
frontendSettingsKey,
|
||||||
});
|
{
|
||||||
|
frontendApiOrigins: this.config.frontendApiOrigins,
|
||||||
|
},
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.debug('Unable to fetch frontend settings', error);
|
this.logger.debug('Unable to fetch frontend settings', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
.map((p) => p.name)
|
?.filter((p) => p.required)
|
||||||
.filter(
|
.map((p) => p.name)
|
||||||
(requiredParam) =>
|
.filter(
|
||||||
!Object.keys(parameters).includes(requiredParam),
|
(requiredParam) =>
|
||||||
);
|
!Object.keys(parameters).includes(requiredParam),
|
||||||
|
) || [];
|
||||||
if (requiredParamsMissing.length > 0) {
|
if (requiredParamsMissing.length > 0) {
|
||||||
throw new ValidationError(
|
throw new ValidationError(
|
||||||
`Missing required parameters: ${requiredParamsMissing.join(
|
`Missing required parameters: ${requiredParamsMissing.join(
|
||||||
|
|||||||
@ -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