From 4246baee163fad7bcb27d336add94a1482671677 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Tue, 20 Apr 2021 12:32:02 +0200 Subject: [PATCH] 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 --- ...-field-store.js => context-field-store.ts} | 43 +++++-- src/lib/db/feature-type-store.js | 27 ---- src/lib/db/feature-type-store.ts | 48 ++++++++ .../db/{project-store.js => project-store.ts} | 49 ++++++-- .../{strategy-store.js => strategy-store.ts} | 80 ++++++++---- ...-tag-error.js => feature-has-tag-error.ts} | 8 +- ...on-error.js => invalid-operation-error.ts} | 9 +- ...e-exists-error.js => name-exists-error.ts} | 9 +- .../{notfound-error.js => notfound-error.ts} | 6 +- .../routes/admin-api/bootstrap-controller.ts | 115 ++++++++++++++++++ src/lib/routes/admin-api/index.js | 5 + ...{context-service.js => context-service.ts} | 15 ++- src/lib/services/email-service.ts | 4 + src/lib/services/project-service.ts | 44 +++---- ...{strategy-schema.js => strategy-schema.ts} | 2 +- ...trategy-service.js => strategy-service.ts} | 50 ++++++-- src/lib/services/tag-type-service.ts | 2 +- src/lib/types/core.ts | 18 +++ 18 files changed, 410 insertions(+), 124 deletions(-) rename src/lib/db/{context-field-store.js => context-field-store.ts} (70%) delete mode 100644 src/lib/db/feature-type-store.js create mode 100644 src/lib/db/feature-type-store.ts rename src/lib/db/{project-store.js => project-store.ts} (72%) rename src/lib/db/{strategy-store.js => strategy-store.ts} (67%) rename src/lib/error/{feature-has-tag-error.js => feature-has-tag-error.ts} (79%) rename src/lib/error/{invalid-operation-error.js => invalid-operation-error.ts} (80%) rename src/lib/error/{name-exists-error.js => name-exists-error.ts} (80%) rename src/lib/error/{notfound-error.js => notfound-error.ts} (77%) create mode 100644 src/lib/routes/admin-api/bootstrap-controller.ts rename src/lib/services/{context-service.js => context-service.ts} (87%) rename src/lib/services/{strategy-schema.js => strategy-schema.ts} (96%) rename src/lib/services/{strategy-service.js => strategy-service.ts} (70%) diff --git a/src/lib/db/context-field-store.js b/src/lib/db/context-field-store.ts similarity index 70% rename from src/lib/db/context-field-store.js rename to src/lib/db/context-field-store.ts index 681662d2e3..7560216800 100644 --- a/src/lib/db/context-field-store.js +++ b/src/lib/db/context-field-store.ts @@ -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 { 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 { return this.db .first(COLUMNS) .from(TABLE) @@ -73,21 +98,21 @@ class ContextFieldStore { .then(mapRow); } - async create(contextField) { + async create(contextField): Promise { return this.db(TABLE).insert(this.fieldToRow(contextField)); } - async update(data) { + async update(data): Promise { return this.db(TABLE) .where({ name: data.name }) .update(this.fieldToRow(data)); } - async delete(name) { + async delete(name): Promise { return this.db(TABLE) .where({ name }) .del(); } } - +export default ContextFieldStore; module.exports = ContextFieldStore; diff --git a/src/lib/db/feature-type-store.js b/src/lib/db/feature-type-store.js deleted file mode 100644 index c7396a17a4..0000000000 --- a/src/lib/db/feature-type-store.js +++ /dev/null @@ -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; diff --git a/src/lib/db/feature-type-store.ts b/src/lib/db/feature-type-store.ts new file mode 100644 index 0000000000..4358e96055 --- /dev/null +++ b/src/lib/db/feature-type-store.ts @@ -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 { + 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; diff --git a/src/lib/db/project-store.js b/src/lib/db/project-store.ts similarity index 72% rename from src/lib/db/project-store.js rename to src/lib/db/project-store.ts index c0ec669ad8..907c59666b 100644 --- a/src/lib/db/project-store.js +++ b/src/lib/db/project-store.ts @@ -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 { 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 { return this.db .first(COLUMNS) .from(TABLE) @@ -34,7 +59,7 @@ class ProjectStore { .then(this.mapRow); } - async hasProject(id) { + async hasProject(id): Promise { return this.db .first('id') .from(TABLE) @@ -50,14 +75,14 @@ class ProjectStore { }); } - async create(project) { + async create(project): Promise { const [id] = await this.db(TABLE) .insert(this.fieldToRow(project)) .returning('id'); return { ...project, id }; } - async update(data) { + async update(data): Promise { try { await this.db(TABLE) .where({ id: data.id }) @@ -67,7 +92,7 @@ class ProjectStore { } } - async importProjects(projects) { + async importProjects(projects): Promise { 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 { await this.db(TABLE).del(); } - async delete(id) { + async delete(id): Promise { 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; diff --git a/src/lib/db/strategy-store.js b/src/lib/db/strategy-store.ts similarity index 67% rename from src/lib/db/strategy-store.js rename to src/lib/db/strategy-store.ts index 6d6964ee9b..3820d2c9be 100644 --- a/src/lib/db/strategy-store.js +++ b/src/lib/db/strategy-store.ts @@ -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 { 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 { 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 { 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 { 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 { 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 { + await this.db(TABLE) .where({ built_in: 0 }) // eslint-disable-line .delete() .catch(err => diff --git a/src/lib/error/feature-has-tag-error.js b/src/lib/error/feature-has-tag-error.ts similarity index 79% rename from src/lib/error/feature-has-tag-error.js rename to src/lib/error/feature-has-tag-error.ts index 720245c026..41c1d35091 100644 --- a/src/lib/error/feature-has-tag-error.js +++ b/src/lib/error/feature-has-tag-error.ts @@ -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; diff --git a/src/lib/error/invalid-operation-error.js b/src/lib/error/invalid-operation-error.ts similarity index 80% rename from src/lib/error/invalid-operation-error.js rename to src/lib/error/invalid-operation-error.ts index d494cd0dd4..5ca226f55c 100644 --- a/src/lib/error/invalid-operation-error.js +++ b/src/lib/error/invalid-operation-error.ts @@ -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; diff --git a/src/lib/error/name-exists-error.js b/src/lib/error/name-exists-error.ts similarity index 80% rename from src/lib/error/name-exists-error.js rename to src/lib/error/name-exists-error.ts index bfe1ad16cc..3ac37c5f2f 100644 --- a/src/lib/error/name-exists-error.js +++ b/src/lib/error/name-exists-error.ts @@ -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; diff --git a/src/lib/error/notfound-error.js b/src/lib/error/notfound-error.ts similarity index 77% rename from src/lib/error/notfound-error.js rename to src/lib/error/notfound-error.ts index 1c4a92ab69..e469e10714 100644 --- a/src/lib/error/notfound-error.js +++ b/src/lib/error/notfound-error.ts @@ -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; diff --git a/src/lib/routes/admin-api/bootstrap-controller.ts b/src/lib/routes/admin-api/bootstrap-controller.ts new file mode 100644 index 0000000000..be3cfa5d42 --- /dev/null +++ b/src/lib/routes/admin-api/bootstrap-controller.ts @@ -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 { + const jobs: [ + Promise, + Promise, + Promise, + Promise, + Promise, + Promise, + ] = [ + 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; diff --git a/src/lib/routes/admin-api/index.js b/src/lib/routes/admin-api/index.js index 51550fb579..e1916acb52 100644 --- a/src/lib/routes/admin-api/index.js +++ b/src/lib/routes/admin-api/index.js @@ -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, diff --git a/src/lib/services/context-service.js b/src/lib/services/context-service.ts similarity index 87% rename from src/lib/services/context-service.js rename to src/lib/services/context-service.ts index ef29b175b0..6f8aa1154f 100644 --- a/src/lib/services/context-service.js +++ b/src/lib/services/context-service.ts @@ -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; diff --git a/src/lib/services/email-service.ts b/src/lib/services/email-service.ts index 885c85cea4..3c06b9ad39 100644 --- a/src/lib/services/email-service.ts +++ b/src/lib/services/email-service.ts @@ -137,6 +137,10 @@ export class EmailService { }); } + isEnabled(): boolean { + return this.mailer !== undefined; + } + private async compileTemplate( templateName: string, format: TemplateFormat, diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index 4f54c48ffc..5ef3c601c2 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -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 { return this.projectStore.getAll(); } - async getProject(id) { + async getProject(id: number): Promise { return this.projectStore.get(id); } @@ -127,7 +129,7 @@ class ProjectService { } // RBAC methods - async getUsersWithAccess(projectId: string) { + async getUsersWithAccess(projectId: string): Promise { const [roles, users] = await this.accessService.getProjectRoleUsers( projectId, ); diff --git a/src/lib/services/strategy-schema.js b/src/lib/services/strategy-schema.ts similarity index 96% rename from src/lib/services/strategy-schema.js rename to src/lib/services/strategy-schema.ts index d3acf73194..6f5850c44b 100644 --- a/src/lib/services/strategy-schema.js +++ b/src/lib/services/strategy-schema.ts @@ -31,5 +31,5 @@ const strategySchema = joi ), }) .options({ allowUnknown: false, stripUnknown: true, abortEarly: false }); - +export default strategySchema; module.exports = strategySchema; diff --git a/src/lib/services/strategy-service.js b/src/lib/services/strategy-service.ts similarity index 70% rename from src/lib/services/strategy-service.js rename to src/lib/services/strategy-service.ts index bc81cc305c..818a7df828 100644 --- a/src/lib/services/strategy-service.js +++ b/src/lib/services/strategy-service.ts @@ -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, + { getLogger }: Pick, + ) { this.strategyStore = strategyStore; this.eventStore = eventStore; this.logger = getLogger('services/strategy-service.js'); } - async getStrategies() { + async getStrategies(): Promise { return this.strategyStore.getStrategies(); } - async getStrategy(name) { + async getStrategy(name: string): Promise { return this.strategyStore.getStrategy(name); } - async removeStrategy(strategyName, userName) { + async removeStrategy( + strategyName: string, + userName: string, + ): Promise { 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 { 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 { 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 { 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 { 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, + ): Promise> { 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; diff --git a/src/lib/services/tag-type-service.ts b/src/lib/services/tag-type-service.ts index 8e29e6af7f..73857ecb4a 100644 --- a/src/lib/services/tag-type-service.ts +++ b/src/lib/services/tag-type-service.ts @@ -49,7 +49,7 @@ export default class TagTypeService { return data; } - async validateUnique({ name }: Partial): Promise { + async validateUnique({ name }: Pick): Promise { const exists = await this.tagTypeStore.exists(name); if (exists) { throw new NameExistsError( diff --git a/src/lib/types/core.ts b/src/lib/types/core.ts index 981ac9722e..4d1c62a32f 100644 --- a/src/lib/types/core.ts +++ b/src/lib/types/core.ts @@ -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; +}