mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-26 13:48:33 +02:00
feat: add ui-bootstrap endpoint (#790)
* feat: add ui-bootstrap endpoint - Reducing calls needed for frontend to 1 instead of the current 6 fixes: #789
This commit is contained in:
parent
332f1c4544
commit
4246baee16
@ -1,5 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
import { Knex } from 'knex';
|
||||
import { Logger, LogProvider } from '../logger';
|
||||
|
||||
const COLUMNS = [
|
||||
'name',
|
||||
'description',
|
||||
@ -10,7 +13,7 @@ const COLUMNS = [
|
||||
];
|
||||
const TABLE = 'context_fields';
|
||||
|
||||
const mapRow = row => ({
|
||||
const mapRow: (IContextRow) => IContextField = row => ({
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
stickiness: row.stickiness,
|
||||
@ -19,8 +22,30 @@ const mapRow = row => ({
|
||||
createdAt: row.created_at,
|
||||
});
|
||||
|
||||
export interface ICreateContextField {
|
||||
name: string;
|
||||
description: string;
|
||||
stickiness: boolean;
|
||||
sort_order: number;
|
||||
legal_values?: string[];
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export interface IContextField {
|
||||
name: string;
|
||||
description: string;
|
||||
stickiness: boolean;
|
||||
sortOrder: number;
|
||||
legalValues?: string[];
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
class ContextFieldStore {
|
||||
constructor(db, customContextFields, getLogger) {
|
||||
private db: Knex;
|
||||
|
||||
private logger: Logger;
|
||||
|
||||
constructor(db: Knex, customContextFields, getLogger: LogProvider) {
|
||||
this.db = db;
|
||||
this.logger = getLogger('context-field-store.js');
|
||||
this._createFromConfig(customContextFields);
|
||||
@ -45,7 +70,7 @@ class ContextFieldStore {
|
||||
}
|
||||
}
|
||||
|
||||
fieldToRow(data) {
|
||||
fieldToRow(data): ICreateContextField {
|
||||
return {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
@ -56,7 +81,7 @@ class ContextFieldStore {
|
||||
};
|
||||
}
|
||||
|
||||
async getAll() {
|
||||
async getAll(): Promise<IContextField[]> {
|
||||
const rows = await this.db
|
||||
.select(COLUMNS)
|
||||
.from(TABLE)
|
||||
@ -65,7 +90,7 @@ class ContextFieldStore {
|
||||
return rows.map(mapRow);
|
||||
}
|
||||
|
||||
async get(name) {
|
||||
async get(name): Promise<IContextField> {
|
||||
return this.db
|
||||
.first(COLUMNS)
|
||||
.from(TABLE)
|
||||
@ -73,21 +98,21 @@ class ContextFieldStore {
|
||||
.then(mapRow);
|
||||
}
|
||||
|
||||
async create(contextField) {
|
||||
async create(contextField): Promise<void> {
|
||||
return this.db(TABLE).insert(this.fieldToRow(contextField));
|
||||
}
|
||||
|
||||
async update(data) {
|
||||
async update(data): Promise<void> {
|
||||
return this.db(TABLE)
|
||||
.where({ name: data.name })
|
||||
.update(this.fieldToRow(data));
|
||||
}
|
||||
|
||||
async delete(name) {
|
||||
async delete(name): Promise<void> {
|
||||
return this.db(TABLE)
|
||||
.where({ name })
|
||||
.del();
|
||||
}
|
||||
}
|
||||
|
||||
export default ContextFieldStore;
|
||||
module.exports = ContextFieldStore;
|
@ -1,27 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const COLUMNS = ['id', 'name', 'description', 'lifetime_days'];
|
||||
const TABLE = 'feature_types';
|
||||
|
||||
class FeatureToggleStore {
|
||||
constructor(db, getLogger) {
|
||||
this.db = db;
|
||||
this.logger = getLogger('feature-type-store.js');
|
||||
}
|
||||
|
||||
async getAll() {
|
||||
const rows = await this.db.select(COLUMNS).from(TABLE);
|
||||
return rows.map(this.rowToFeatureType);
|
||||
}
|
||||
|
||||
rowToFeatureType(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
lifetimeDays: row.lifetime_days,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = FeatureToggleStore;
|
48
src/lib/db/feature-type-store.ts
Normal file
48
src/lib/db/feature-type-store.ts
Normal file
@ -0,0 +1,48 @@
|
||||
'use strict';
|
||||
|
||||
import { Knex } from 'knex';
|
||||
import { Logger, LogProvider } from '../logger';
|
||||
|
||||
const COLUMNS = ['id', 'name', 'description', 'lifetime_days'];
|
||||
const TABLE = 'feature_types';
|
||||
|
||||
export interface IFeatureType {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
lifetimeDays: number;
|
||||
}
|
||||
|
||||
interface IFeatureTypeRow {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
lifetime_days: number;
|
||||
}
|
||||
|
||||
class FeatureTypeStore {
|
||||
private db: Knex;
|
||||
|
||||
private logger: Logger;
|
||||
|
||||
constructor(db: Knex, getLogger: LogProvider) {
|
||||
this.db = db;
|
||||
this.logger = getLogger('feature-type-store.js');
|
||||
}
|
||||
|
||||
async getAll(): Promise<IFeatureType[]> {
|
||||
const rows = await this.db.select(COLUMNS).from(TABLE);
|
||||
return rows.map(this.rowToFeatureType);
|
||||
}
|
||||
|
||||
rowToFeatureType(row: IFeatureTypeRow): IFeatureType {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
lifetimeDays: row.lifetime_days,
|
||||
};
|
||||
}
|
||||
}
|
||||
export default FeatureTypeStore;
|
||||
module.exports = FeatureTypeStore;
|
@ -1,15 +1,40 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Logger, LogProvider } from '../logger';
|
||||
|
||||
const NotFoundError = require('../error/notfound-error');
|
||||
|
||||
const COLUMNS = ['id', 'name', 'description', 'created_at'];
|
||||
const TABLE = 'projects';
|
||||
|
||||
export interface IProject {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
interface IProjectInsert {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface IProjectArchived {
|
||||
id: number;
|
||||
archived: boolean;
|
||||
}
|
||||
|
||||
class ProjectStore {
|
||||
constructor(db, getLogger) {
|
||||
private db: Knex;
|
||||
|
||||
private logger: Logger;
|
||||
|
||||
constructor(db: Knex, getLogger: LogProvider) {
|
||||
this.db = db;
|
||||
this.logger = getLogger('project-store.js');
|
||||
}
|
||||
|
||||
fieldToRow(data) {
|
||||
fieldToRow(data): IProjectInsert {
|
||||
return {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
@ -17,7 +42,7 @@ class ProjectStore {
|
||||
};
|
||||
}
|
||||
|
||||
async getAll() {
|
||||
async getAll(): Promise<IProject[]> {
|
||||
const rows = await this.db
|
||||
.select(COLUMNS)
|
||||
.from(TABLE)
|
||||
@ -26,7 +51,7 @@ class ProjectStore {
|
||||
return rows.map(this.mapRow);
|
||||
}
|
||||
|
||||
async get(id) {
|
||||
async get(id): Promise<IProject> {
|
||||
return this.db
|
||||
.first(COLUMNS)
|
||||
.from(TABLE)
|
||||
@ -34,7 +59,7 @@ class ProjectStore {
|
||||
.then(this.mapRow);
|
||||
}
|
||||
|
||||
async hasProject(id) {
|
||||
async hasProject(id): Promise<IProjectArchived> {
|
||||
return this.db
|
||||
.first('id')
|
||||
.from(TABLE)
|
||||
@ -50,14 +75,14 @@ class ProjectStore {
|
||||
});
|
||||
}
|
||||
|
||||
async create(project) {
|
||||
async create(project): Promise<IProject> {
|
||||
const [id] = await this.db(TABLE)
|
||||
.insert(this.fieldToRow(project))
|
||||
.returning('id');
|
||||
return { ...project, id };
|
||||
}
|
||||
|
||||
async update(data) {
|
||||
async update(data): Promise<void> {
|
||||
try {
|
||||
await this.db(TABLE)
|
||||
.where({ id: data.id })
|
||||
@ -67,7 +92,7 @@ class ProjectStore {
|
||||
}
|
||||
}
|
||||
|
||||
async importProjects(projects) {
|
||||
async importProjects(projects): Promise<IProject[]> {
|
||||
const rows = await this.db(TABLE)
|
||||
.insert(projects.map(this.fieldToRow))
|
||||
.returning(COLUMNS)
|
||||
@ -79,11 +104,11 @@ class ProjectStore {
|
||||
return [];
|
||||
}
|
||||
|
||||
async dropProjects() {
|
||||
async dropProjects(): Promise<void> {
|
||||
await this.db(TABLE).del();
|
||||
}
|
||||
|
||||
async delete(id) {
|
||||
async delete(id): Promise<void> {
|
||||
try {
|
||||
await this.db(TABLE)
|
||||
.where({ id })
|
||||
@ -93,7 +118,7 @@ class ProjectStore {
|
||||
}
|
||||
}
|
||||
|
||||
mapRow(row) {
|
||||
mapRow(row): IProject {
|
||||
if (!row) {
|
||||
throw new NotFoundError('No project found');
|
||||
}
|
||||
@ -106,5 +131,5 @@ class ProjectStore {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default ProjectStore;
|
||||
module.exports = ProjectStore;
|
@ -1,5 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
import { Knex } from 'knex';
|
||||
import { Logger, LogProvider } from '../logger';
|
||||
|
||||
const NotFoundError = require('../error/notfound-error');
|
||||
|
||||
const STRATEGY_COLUMNS = [
|
||||
@ -11,13 +14,49 @@ const STRATEGY_COLUMNS = [
|
||||
];
|
||||
const TABLE = 'strategies';
|
||||
|
||||
class StrategyStore {
|
||||
constructor(db, getLogger) {
|
||||
export interface IStrategy {
|
||||
name: string;
|
||||
editable: boolean;
|
||||
description: string;
|
||||
parameters: object;
|
||||
deprecated: boolean;
|
||||
}
|
||||
|
||||
export interface IEditableStrategy {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: object;
|
||||
deprecated: boolean;
|
||||
}
|
||||
|
||||
export interface IMinimalStrategy {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: string;
|
||||
}
|
||||
|
||||
export interface IStrategyName {
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface IStrategyRow {
|
||||
name: string;
|
||||
built_in: number;
|
||||
description: string;
|
||||
parameters: object;
|
||||
deprecated: boolean;
|
||||
}
|
||||
export default class StrategyStore {
|
||||
private db: Knex;
|
||||
|
||||
private logger: Logger;
|
||||
|
||||
constructor(db: Knex, getLogger: LogProvider) {
|
||||
this.db = db;
|
||||
this.logger = getLogger('strategy-store.js');
|
||||
}
|
||||
|
||||
async getStrategies() {
|
||||
async getStrategies(): Promise<IStrategy[]> {
|
||||
const rows = await this.db
|
||||
.select(STRATEGY_COLUMNS)
|
||||
.from(TABLE)
|
||||
@ -26,7 +65,7 @@ class StrategyStore {
|
||||
return rows.map(this.rowToStrategy);
|
||||
}
|
||||
|
||||
async getEditableStrategies() {
|
||||
async getEditableStrategies(): Promise<IEditableStrategy[]> {
|
||||
const rows = await this.db
|
||||
.select(STRATEGY_COLUMNS)
|
||||
.from(TABLE)
|
||||
@ -35,7 +74,7 @@ class StrategyStore {
|
||||
return rows.map(this.rowToEditableStrategy);
|
||||
}
|
||||
|
||||
async getStrategy(name) {
|
||||
async getStrategy(name: string): Promise<IStrategy> {
|
||||
return this.db
|
||||
.first(STRATEGY_COLUMNS)
|
||||
.from(TABLE)
|
||||
@ -43,7 +82,7 @@ class StrategyStore {
|
||||
.then(this.rowToStrategy);
|
||||
}
|
||||
|
||||
rowToStrategy(row) {
|
||||
rowToStrategy(row: IStrategyRow): IStrategy {
|
||||
if (!row) {
|
||||
throw new NotFoundError('No strategy found');
|
||||
}
|
||||
@ -56,7 +95,7 @@ class StrategyStore {
|
||||
};
|
||||
}
|
||||
|
||||
rowToEditableStrategy(row) {
|
||||
rowToEditableStrategy(row: IStrategyRow): IEditableStrategy {
|
||||
if (!row) {
|
||||
throw new NotFoundError('No strategy found');
|
||||
}
|
||||
@ -68,7 +107,7 @@ class StrategyStore {
|
||||
};
|
||||
}
|
||||
|
||||
eventDataToRow(data) {
|
||||
eventDataToRow(data): IMinimalStrategy {
|
||||
return {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
@ -84,7 +123,7 @@ class StrategyStore {
|
||||
);
|
||||
}
|
||||
|
||||
async updateStrategy(data) {
|
||||
async updateStrategy(data): Promise<void> {
|
||||
this.db(TABLE)
|
||||
.where({ name: data.name })
|
||||
.update(this.eventDataToRow(data))
|
||||
@ -93,7 +132,7 @@ class StrategyStore {
|
||||
);
|
||||
}
|
||||
|
||||
async deprecateStrategy({ name }) {
|
||||
async deprecateStrategy({ name }: IStrategyName): Promise<void> {
|
||||
this.db(TABLE)
|
||||
.where({ name })
|
||||
.update({ deprecated: true })
|
||||
@ -102,7 +141,7 @@ class StrategyStore {
|
||||
);
|
||||
}
|
||||
|
||||
async reactivateStrategy({ name }) {
|
||||
async reactivateStrategy({ name }: IStrategyName) {
|
||||
this.db(TABLE)
|
||||
.where({ name })
|
||||
.update({ deprecated: false })
|
||||
@ -114,7 +153,7 @@ class StrategyStore {
|
||||
);
|
||||
}
|
||||
|
||||
async deleteStrategy({ name }) {
|
||||
async deleteStrategy({ name }: IStrategyName) {
|
||||
return this.db(TABLE)
|
||||
.where({ name })
|
||||
.del()
|
||||
@ -125,19 +164,14 @@ class StrategyStore {
|
||||
|
||||
async importStrategy(data) {
|
||||
const rowData = this.eventDataToRow(data);
|
||||
return this.db(TABLE)
|
||||
.where({ name: rowData.name, built_in: 0 }) // eslint-disable-line
|
||||
.update(rowData)
|
||||
.then(result =>
|
||||
result === 0 ? this.db(TABLE).insert(rowData) : result,
|
||||
)
|
||||
.catch(err =>
|
||||
this.logger.error('Could not import strategy, error: ', err),
|
||||
);
|
||||
await this.db(TABLE)
|
||||
.insert(rowData)
|
||||
.onConflict(['name'])
|
||||
.merge();
|
||||
}
|
||||
|
||||
async dropStrategies() {
|
||||
return this.db(TABLE)
|
||||
async dropStrategies(): Promise<void> {
|
||||
await this.db(TABLE)
|
||||
.where({ built_in: 0 }) // eslint-disable-line
|
||||
.delete()
|
||||
.catch(err =>
|
@ -1,5 +1,5 @@
|
||||
class FeatureHasTagError extends Error {
|
||||
constructor(message) {
|
||||
constructor(message: string) {
|
||||
super();
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
@ -7,8 +7,8 @@ class FeatureHasTagError extends Error {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const obj = {
|
||||
toJSON(): object {
|
||||
return {
|
||||
isJoi: true,
|
||||
name: this.constructor.name,
|
||||
details: [
|
||||
@ -17,7 +17,7 @@ class FeatureHasTagError extends Error {
|
||||
},
|
||||
],
|
||||
};
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
export default FeatureHasTagError;
|
||||
module.exports = FeatureHasTagError;
|
@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
class InvalidOperationError extends Error {
|
||||
constructor(message) {
|
||||
constructor(message: string) {
|
||||
super();
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
@ -9,8 +9,8 @@ class InvalidOperationError extends Error {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const obj = {
|
||||
toJSON(): object {
|
||||
return {
|
||||
isJoi: true,
|
||||
name: this.constructor.name,
|
||||
details: [
|
||||
@ -19,8 +19,7 @@ class InvalidOperationError extends Error {
|
||||
},
|
||||
],
|
||||
};
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
export default InvalidOperationError;
|
||||
module.exports = InvalidOperationError;
|
@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
class NameExistsError extends Error {
|
||||
constructor(message) {
|
||||
constructor(message: string) {
|
||||
super();
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
@ -9,8 +9,8 @@ class NameExistsError extends Error {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const obj = {
|
||||
toJSON(): object {
|
||||
return {
|
||||
isJoi: true,
|
||||
name: this.constructor.name,
|
||||
details: [
|
||||
@ -19,8 +19,7 @@ class NameExistsError extends Error {
|
||||
},
|
||||
],
|
||||
};
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
export default NameExistsError;
|
||||
module.exports = NameExistsError;
|
@ -1,7 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
class NotFoundError extends Error {
|
||||
constructor(message) {
|
||||
constructor(message?: string) {
|
||||
super();
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
@ -9,5 +7,5 @@ class NotFoundError extends Error {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
export default NotFoundError;
|
||||
module.exports = NotFoundError;
|
115
src/lib/routes/admin-api/bootstrap-controller.ts
Normal file
115
src/lib/routes/admin-api/bootstrap-controller.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import { Response } from 'express';
|
||||
import Controller from '../controller';
|
||||
import { AuthedRequest, IUnleashConfig } from '../../types/core';
|
||||
import { Logger } from '../../logger';
|
||||
import ContextService from '../../services/context-service';
|
||||
import FeatureTypeStore, { IFeatureType } from '../../db/feature-type-store';
|
||||
import TagTypeService from '../../services/tag-type-service';
|
||||
import StrategyService from '../../services/strategy-service';
|
||||
import ProjectService from '../../services/project-service';
|
||||
import { IContextField } from '../../db/context-field-store';
|
||||
import { ITagType } from '../../db/tag-type-store';
|
||||
import { IProject } from '../../db/project-store';
|
||||
import { IStrategy } from '../../db/strategy-store';
|
||||
import { IUserPermission } from '../../db/access-store';
|
||||
import { AccessService } from '../../services/access-service';
|
||||
import { EmailService } from '../../services/email-service';
|
||||
|
||||
export default class BootstrapController extends Controller {
|
||||
private logger: Logger;
|
||||
|
||||
private contextService: ContextService;
|
||||
|
||||
private featureTypeStore: FeatureTypeStore;
|
||||
|
||||
private tagTypeService: TagTypeService;
|
||||
|
||||
private strategyService: StrategyService;
|
||||
|
||||
private projectService: ProjectService;
|
||||
|
||||
private accessService: AccessService;
|
||||
|
||||
private emailService: EmailService;
|
||||
|
||||
constructor(
|
||||
config: IUnleashConfig,
|
||||
{
|
||||
contextService,
|
||||
tagTypeService,
|
||||
strategyService,
|
||||
projectService,
|
||||
accessService,
|
||||
emailService,
|
||||
},
|
||||
) {
|
||||
super(config);
|
||||
this.contextService = contextService;
|
||||
this.tagTypeService = tagTypeService;
|
||||
this.strategyService = strategyService;
|
||||
this.projectService = projectService;
|
||||
this.accessService = accessService;
|
||||
this.featureTypeStore = config.stores.featureTypeStore;
|
||||
this.emailService = emailService;
|
||||
|
||||
this.logger = config.getLogger(
|
||||
'routes/admin-api/bootstrap-controller.ts',
|
||||
);
|
||||
|
||||
this.get('/', this.bootstrap);
|
||||
}
|
||||
|
||||
private isContextEnabled(): boolean {
|
||||
return this.config.ui && this.config.ui.flags && this.config.ui.flags.C;
|
||||
}
|
||||
|
||||
private isProjectEnabled(): boolean {
|
||||
return this.config.ui && this.config.ui.flags && this.config.ui.flags.P;
|
||||
}
|
||||
|
||||
async bootstrap(req: AuthedRequest, res: Response): Promise<void> {
|
||||
const jobs: [
|
||||
Promise<IContextField[]>,
|
||||
Promise<IFeatureType[]>,
|
||||
Promise<ITagType[]>,
|
||||
Promise<IStrategy[]>,
|
||||
Promise<IProject[]>,
|
||||
Promise<IUserPermission[]>,
|
||||
] = [
|
||||
this.isContextEnabled()
|
||||
? this.contextService.getAll()
|
||||
: Promise.resolve([]),
|
||||
this.featureTypeStore.getAll(),
|
||||
this.tagTypeService.getAll(),
|
||||
this.strategyService.getStrategies(),
|
||||
this.isProjectEnabled()
|
||||
? this.projectService.getProjects()
|
||||
: Promise.resolve([]),
|
||||
this.accessService.getPermissionsForUser(req.user),
|
||||
];
|
||||
const [
|
||||
context,
|
||||
featureTypes,
|
||||
tagTypes,
|
||||
strategies,
|
||||
projects,
|
||||
userPermissions,
|
||||
] = await Promise.all(jobs);
|
||||
|
||||
res.json({
|
||||
...this.config.ui,
|
||||
unleashUrl: this.config.unleashUrl,
|
||||
baseUriPath: this.config.baseUriPath,
|
||||
version: this.config.version,
|
||||
user: { ...req.user, permissions: userPermissions },
|
||||
email: this.emailService.isEnabled(),
|
||||
context,
|
||||
featureTypes,
|
||||
tagTypes,
|
||||
strategies,
|
||||
projects,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BootstrapController;
|
@ -18,6 +18,7 @@ const ApiTokenController = require('./api-token-controller');
|
||||
const EmailController = require('./email');
|
||||
const UserAdminController = require('./user-admin');
|
||||
const apiDef = require('./api-def.json');
|
||||
const BootstrapController = require('./bootstrap-controller');
|
||||
|
||||
class AdminApi extends Controller {
|
||||
constructor(config, services) {
|
||||
@ -50,6 +51,10 @@ class AdminApi extends Controller {
|
||||
'/ui-config',
|
||||
new ConfigController(config, services).router,
|
||||
);
|
||||
this.app.use(
|
||||
'/ui-bootstrap',
|
||||
new BootstrapController(config, services).router,
|
||||
);
|
||||
this.app.use(
|
||||
'/context',
|
||||
new ContextController(config, services).router,
|
||||
|
@ -1,5 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
import ContextFieldStore from '../db/context-field-store';
|
||||
import EventStore from '../db/event-store';
|
||||
import ProjectStore from '../db/project-store';
|
||||
import { Logger } from '../logger';
|
||||
|
||||
const { contextSchema, nameSchema } = require('./context-schema');
|
||||
const NameExistsError = require('../error/name-exists-error');
|
||||
|
||||
@ -10,6 +15,14 @@ const {
|
||||
} = require('../event-type');
|
||||
|
||||
class ContextService {
|
||||
private projectStore: ProjectStore;
|
||||
|
||||
private eventStore: EventStore;
|
||||
|
||||
private contextFieldStore: ContextFieldStore;
|
||||
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
{ projectStore, eventStore, contextFieldStore },
|
||||
{ getLogger },
|
||||
@ -88,5 +101,5 @@ class ContextService {
|
||||
await this.validateUniqueName({ name });
|
||||
}
|
||||
}
|
||||
|
||||
export default ContextService;
|
||||
module.exports = ContextService;
|
@ -137,6 +137,10 @@ export class EmailService {
|
||||
});
|
||||
}
|
||||
|
||||
isEnabled(): boolean {
|
||||
return this.mailer !== undefined;
|
||||
}
|
||||
|
||||
private async compileTemplate(
|
||||
templateName: string,
|
||||
format: TemplateFormat,
|
||||
|
@ -1,31 +1,33 @@
|
||||
import User from '../user';
|
||||
import { AccessService, RoleName } from './access-service';
|
||||
|
||||
const NameExistsError = require('../error/name-exists-error');
|
||||
const InvalidOperationError = require('../error/invalid-operation-error');
|
||||
const eventType = require('../event-type');
|
||||
const { nameType } = require('../routes/admin-api/util');
|
||||
const schema = require('./project-schema');
|
||||
const NotFoundError = require('../error/notfound-error');
|
||||
|
||||
interface IProject {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
import { AccessService, IUserWithRole, RoleName } from './access-service';
|
||||
import ProjectStore, { IProject } from '../db/project-store';
|
||||
import EventStore from '../db/event-store';
|
||||
import NameExistsError from '../error/name-exists-error';
|
||||
import InvalidOperationError from '../error/invalid-operation-error';
|
||||
import eventType from '../event-type';
|
||||
import { nameType } from '../routes/admin-api/util';
|
||||
import schema from './project-schema';
|
||||
import NotFoundError from '../error/notfound-error';
|
||||
import FeatureToggleStore from '../db/feature-toggle-store';
|
||||
import { IRole } from '../db/access-store';
|
||||
|
||||
const getCreatedBy = (user: User) => user.email || user.username;
|
||||
|
||||
const DEFAULT_PROJECT = 'default';
|
||||
|
||||
class ProjectService {
|
||||
private projectStore: any;
|
||||
export interface UsersWithRoles {
|
||||
users: IUserWithRole[];
|
||||
roles: IRole[];
|
||||
}
|
||||
|
||||
export default class ProjectService {
|
||||
private projectStore: ProjectStore;
|
||||
|
||||
private accessService: AccessService;
|
||||
|
||||
private eventStore: any;
|
||||
private eventStore: EventStore;
|
||||
|
||||
private featureToggleStore: any;
|
||||
private featureToggleStore: FeatureToggleStore;
|
||||
|
||||
private logger: any;
|
||||
|
||||
@ -41,11 +43,11 @@ class ProjectService {
|
||||
this.logger = config.getLogger('services/project-service.js');
|
||||
}
|
||||
|
||||
async getProjects() {
|
||||
async getProjects(): Promise<IProject[]> {
|
||||
return this.projectStore.getAll();
|
||||
}
|
||||
|
||||
async getProject(id) {
|
||||
async getProject(id: number): Promise<IProject> {
|
||||
return this.projectStore.get(id);
|
||||
}
|
||||
|
||||
@ -127,7 +129,7 @@ class ProjectService {
|
||||
}
|
||||
|
||||
// RBAC methods
|
||||
async getUsersWithAccess(projectId: string) {
|
||||
async getUsersWithAccess(projectId: string): Promise<UsersWithRoles> {
|
||||
const [roles, users] = await this.accessService.getProjectRoleUsers(
|
||||
projectId,
|
||||
);
|
||||
|
@ -31,5 +31,5 @@ const strategySchema = joi
|
||||
),
|
||||
})
|
||||
.options({ allowUnknown: false, stripUnknown: true, abortEarly: false });
|
||||
|
||||
export default strategySchema;
|
||||
module.exports = strategySchema;
|
@ -1,3 +1,8 @@
|
||||
import { Logger } from '../logger';
|
||||
import EventStore from '../db/event-store';
|
||||
import StrategyStore, { IStrategy, IStrategyName } from '../db/strategy-store';
|
||||
import { IUnleashConfig, IUnleashStores } from '../types/core';
|
||||
|
||||
const strategySchema = require('./strategy-schema');
|
||||
const NameExistsError = require('../error/name-exists-error');
|
||||
const {
|
||||
@ -9,21 +14,36 @@ const {
|
||||
} = require('../event-type');
|
||||
|
||||
class StrategyService {
|
||||
constructor({ strategyStore, eventStore }, { getLogger }) {
|
||||
private logger: Logger;
|
||||
|
||||
private strategyStore: StrategyStore;
|
||||
|
||||
private eventStore: EventStore;
|
||||
|
||||
constructor(
|
||||
{
|
||||
strategyStore,
|
||||
eventStore,
|
||||
}: Pick<IUnleashStores, 'strategyStore' | 'eventStore'>,
|
||||
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
|
||||
) {
|
||||
this.strategyStore = strategyStore;
|
||||
this.eventStore = eventStore;
|
||||
this.logger = getLogger('services/strategy-service.js');
|
||||
}
|
||||
|
||||
async getStrategies() {
|
||||
async getStrategies(): Promise<IStrategy[]> {
|
||||
return this.strategyStore.getStrategies();
|
||||
}
|
||||
|
||||
async getStrategy(name) {
|
||||
async getStrategy(name: string): Promise<IStrategy> {
|
||||
return this.strategyStore.getStrategy(name);
|
||||
}
|
||||
|
||||
async removeStrategy(strategyName, userName) {
|
||||
async removeStrategy(
|
||||
strategyName: string,
|
||||
userName: string,
|
||||
): Promise<void> {
|
||||
const strategy = await this.strategyStore.getStrategy(strategyName);
|
||||
await this._validateEditable(strategy);
|
||||
await this.strategyStore.deleteStrategy({ name: strategyName });
|
||||
@ -36,7 +56,10 @@ class StrategyService {
|
||||
});
|
||||
}
|
||||
|
||||
async deprecateStrategy(strategyName, userName) {
|
||||
async deprecateStrategy(
|
||||
strategyName: string,
|
||||
userName: string,
|
||||
): Promise<void> {
|
||||
await this.strategyStore.getStrategy(strategyName); // Check existence
|
||||
await this.strategyStore.deprecateStrategy({ name: strategyName });
|
||||
await this.eventStore.store({
|
||||
@ -48,7 +71,10 @@ class StrategyService {
|
||||
});
|
||||
}
|
||||
|
||||
async reactivateStrategy(strategyName, userName) {
|
||||
async reactivateStrategy(
|
||||
strategyName: string,
|
||||
userName: string,
|
||||
): Promise<void> {
|
||||
await this.strategyStore.getStrategy(strategyName); // Check existence
|
||||
await this.strategyStore.reactivateStrategy({ name: strategyName });
|
||||
await this.eventStore.store({
|
||||
@ -60,7 +86,7 @@ class StrategyService {
|
||||
});
|
||||
}
|
||||
|
||||
async createStrategy(value, userName) {
|
||||
async createStrategy(value, userName: string): Promise<void> {
|
||||
const strategy = await strategySchema.validateAsync(value);
|
||||
strategy.deprecated = false;
|
||||
await this._validateStrategyName(strategy);
|
||||
@ -72,7 +98,7 @@ class StrategyService {
|
||||
});
|
||||
}
|
||||
|
||||
async updateStrategy(input, userName) {
|
||||
async updateStrategy(input, userName: string): Promise<void> {
|
||||
const value = await strategySchema.validateAsync(input);
|
||||
const strategy = await this.strategyStore.getStrategy(input.name);
|
||||
await this._validateEditable(strategy);
|
||||
@ -84,7 +110,9 @@ class StrategyService {
|
||||
});
|
||||
}
|
||||
|
||||
async _validateStrategyName(data) {
|
||||
private _validateStrategyName(
|
||||
data: Pick<IStrategy, 'name'>,
|
||||
): Promise<Pick<IStrategy, 'name'>> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.strategyStore
|
||||
.getStrategy(data.name)
|
||||
@ -100,11 +128,11 @@ class StrategyService {
|
||||
}
|
||||
|
||||
// This check belongs in the store.
|
||||
_validateEditable(strategy) {
|
||||
_validateEditable(strategy: IStrategy): void {
|
||||
if (strategy.editable === false) {
|
||||
throw new Error(`Cannot edit strategy ${strategy.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default StrategyService;
|
||||
module.exports = StrategyService;
|
@ -49,7 +49,7 @@ export default class TagTypeService {
|
||||
return data;
|
||||
}
|
||||
|
||||
async validateUnique({ name }: Partial<ITagType>): Promise<boolean> {
|
||||
async validateUnique({ name }: Pick<ITagType, 'name'>): Promise<boolean> {
|
||||
const exists = await this.tagTypeStore.exists(name);
|
||||
if (exists) {
|
||||
throw new NameExistsError(
|
||||
|
@ -1,5 +1,11 @@
|
||||
import { Request } from 'express';
|
||||
import { LogProvider } from '../logger';
|
||||
import { IEmailOptions } from '../services/email-service';
|
||||
import ProjectStore from '../db/project-store';
|
||||
import EventStore from '../db/event-store';
|
||||
import FeatureTypeStore from '../db/feature-type-store';
|
||||
import User from '../user';
|
||||
import StrategyStore from '../db/strategy-store';
|
||||
|
||||
interface IExperimentalFlags {
|
||||
[key: string]: boolean;
|
||||
@ -15,6 +21,14 @@ export interface IUnleashConfig {
|
||||
};
|
||||
unleashUrl: string;
|
||||
email?: IEmailOptions;
|
||||
stores?: IUnleashStores;
|
||||
}
|
||||
|
||||
export interface IUnleashStores {
|
||||
projectStore: ProjectStore;
|
||||
eventStore: EventStore;
|
||||
featureTypeStore: FeatureTypeStore;
|
||||
strategyStore: StrategyStore;
|
||||
}
|
||||
|
||||
export enum AuthenticationType {
|
||||
@ -24,3 +38,7 @@ export enum AuthenticationType {
|
||||
openSource = 'open-source',
|
||||
enterprise = 'enterprise',
|
||||
}
|
||||
|
||||
export interface AuthedRequest extends Request {
|
||||
user: User;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user