diff --git a/package.json b/package.json index 63c9f55988..21a6ae50af 100644 --- a/package.json +++ b/package.json @@ -125,8 +125,8 @@ "passport-google-auth": "^1.0.2", "prettier": "^1.19.1", "proxyquire": "^2.1.3", - "source-map-support": "^0.5.19", "sinon": "^9.2.4", + "source-map-support": "^0.5.19", "superagent": "^6.1.0", "supertest": "^5.0.0", "ts-node": "^9.1.1", diff --git a/src/lib/db/tag-store.js b/src/lib/db/tag-store.ts similarity index 56% rename from src/lib/db/tag-store.js rename to src/lib/db/tag-store.ts index d84544ffbe..fa965dd972 100644 --- a/src/lib/db/tag-store.js +++ b/src/lib/db/tag-store.ts @@ -1,13 +1,33 @@ 'use strict'; -const metricsHelper = require('../metrics-helper'); -const { DB_TIME } = require('../events'); -const NotFoundError = require('../error/notfound-error'); +import { Knex } from 'knex'; +import { EventEmitter } from 'events'; +import { DB_TIME } from '../events'; +import metricsHelper from '../metrics-helper'; +import { LogProvider, Logger } from '../logger'; +import NotFoundError from '../error/notfound-error'; const COLUMNS = ['type', 'value']; const TABLE = 'tags'; -class TagStore { - constructor(db, eventBus, getLogger) { + +interface ITagTable { + type: string; + value: string; +} + +export interface ITag { + type: string; + value: string; +} + +export default class TagStore { + private db: Knex; + + private logger: Logger; + + private readonly timer: Function; + + constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) { this.db = db; this.logger = getLogger('tag-store.js'); this.timer = action => @@ -17,7 +37,7 @@ class TagStore { }); } - async getTagsByType(type) { + async getTagsByType(type: string): Promise { const stopTimer = this.timer('getTagByType'); const rows = await this.db .select(COLUMNS) @@ -27,14 +47,14 @@ class TagStore { return rows.map(this.rowToTag); } - async getAll() { + async getAll(): Promise { const stopTimer = this.timer('getAll'); const rows = await this.db.select(COLUMNS).from(TABLE); stopTimer(); return rows.map(this.rowToTag); } - async getTag(type, value) { + async getTag(type: string, value: string): Promise { const stopTimer = this.timer('getTag'); const tag = await this.db .first(COLUMNS) @@ -49,13 +69,24 @@ class TagStore { return tag; } - async createTag(tag) { + async exists(tag: ITag): Promise { + const stopTimer = this.timer('exists'); + const result = await this.db.raw( + `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE type = ? AND value = ?) AS present`, + [tag.type, tag.value], + ); + const { present } = result.rows[0]; + stopTimer(); + return present; + } + + async createTag(tag: ITag): Promise { const stopTimer = this.timer('createTag'); await this.db(TABLE).insert(tag); stopTimer(); } - async deleteTag(tag) { + async deleteTag(tag: ITag): Promise { const stopTimer = this.timer('deleteTag'); await this.db(TABLE) .where(tag) @@ -63,13 +94,13 @@ class TagStore { stopTimer(); } - async dropTags() { + async dropTags(): Promise { const stopTimer = this.timer('dropTags'); await this.db(TABLE).del(); stopTimer(); } - async bulkImport(tags) { + async bulkImport(tags: ITag[]): Promise { return this.db(TABLE) .insert(tags) .returning(COLUMNS) @@ -77,7 +108,7 @@ class TagStore { .ignore(); } - rowToTag(row) { + rowToTag(row: ITagTable): ITag { return { type: row.type, value: row.value, diff --git a/src/lib/db/tag-type-store.js b/src/lib/db/tag-type-store.ts similarity index 59% rename from src/lib/db/tag-type-store.js rename to src/lib/db/tag-type-store.ts index 972f838dbb..c49dd892b2 100644 --- a/src/lib/db/tag-type-store.js +++ b/src/lib/db/tag-type-store.ts @@ -1,14 +1,33 @@ -'use strict'; - -const metricsHelper = require('../metrics-helper'); -const { DB_TIME } = require('../events'); -const NotFoundError = require('../error/notfound-error'); +import { Knex } from 'knex'; +import { EventEmitter } from 'events'; +import { LogProvider, Logger } from '../logger'; +import { DB_TIME } from '../events'; +import metricsHelper from '../metrics-helper'; +import NotFoundError from '../error/notfound-error'; const COLUMNS = ['name', 'description', 'icon']; const TABLE = 'tag_types'; -class TagTypeStore { - constructor(db, eventBus, getLogger) { +interface ITagTypeTable { + name: string; + description?: string; + icon?: string; +} + +export interface ITagType { + name: string; + description?: string; + icon?: string; +} + +export default class TagTypeStore { + private db: Knex; + + private logger: Logger; + + private readonly timer: Function; + + constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) { this.db = db; this.logger = getLogger('tag-type-store.js'); this.timer = action => @@ -18,14 +37,14 @@ class TagTypeStore { }); } - async getAll() { + async getAll(): Promise { const stopTimer = this.timer('getTagTypes'); const rows = await this.db.select(COLUMNS).from(TABLE); stopTimer(); return rows.map(this.rowToTagType); } - async getTagType(name) { + async getTagType(name: string): Promise { const stopTimer = this.timer('getTagTypeByName'); return this.db .first(COLUMNS) @@ -41,23 +60,24 @@ class TagTypeStore { }); } - async exists(name) { + async exists(name: string): Promise { const stopTimer = this.timer('exists'); - const row = await this.db - .first(COLUMNS) - .from(TABLE) - .where({ name }); + const result = await this.db.raw( + `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE name = ?) AS present`, + [name], + ); + const { present } = result.rows[0]; stopTimer(); - return row; + return present; } - async createTagType(newTagType) { + async createTagType(newTagType: ITagType): Promise { const stopTimer = this.timer('createTagType'); await this.db(TABLE).insert(newTagType); stopTimer(); } - async deleteTagType(name) { + async deleteTagType(name: string): Promise { const stopTimer = this.timer('deleteTagType'); await this.db(TABLE) .where({ name }) @@ -65,13 +85,13 @@ class TagTypeStore { stopTimer(); } - async dropTagTypes() { + async dropTagTypes(): Promise { const stopTimer = this.timer('dropTagTypes'); await this.db(TABLE).del(); stopTimer(); } - async bulkImport(tagTypes) { + async bulkImport(tagTypes: ITagType[]): Promise { const rows = await this.db(TABLE) .insert(tagTypes) .returning(COLUMNS) @@ -83,7 +103,7 @@ class TagTypeStore { return []; } - async updateTagType({ name, description, icon }) { + async updateTagType({ name, description, icon }: ITagType): Promise { const stopTimer = this.timer('updateTagType'); await this.db(TABLE) .where({ name }) @@ -91,7 +111,7 @@ class TagTypeStore { stopTimer(); } - rowToTagType(row) { + rowToTagType(row: ITagTypeTable): ITagType { return { name: row.name, description: row.description, diff --git a/src/lib/services/tag-schema.js b/src/lib/services/tag-schema.ts similarity index 64% rename from src/lib/services/tag-schema.js rename to src/lib/services/tag-schema.ts index 91de3e9870..8606ef9fc8 100644 --- a/src/lib/services/tag-schema.js +++ b/src/lib/services/tag-schema.ts @@ -1,13 +1,10 @@ -'use strict'; +import Joi from 'joi'; -const joi = require('joi'); -const { customJoi } = require('../routes/admin-api/util'); +import { customJoi } from '../routes/admin-api/util'; -const tagSchema = joi - .object() +export const tagSchema = Joi.object() .keys({ - value: joi - .string() + value: Joi.string() .min(2) .max(50), type: customJoi diff --git a/src/lib/services/tag-service.js b/src/lib/services/tag-service.js deleted file mode 100644 index 1bd97f214c..0000000000 --- a/src/lib/services/tag-service.js +++ /dev/null @@ -1,62 +0,0 @@ -const { tagSchema } = require('./tag-schema'); -const NotFoundError = require('../error/notfound-error'); -const NameExistsError = require('../error/name-exists-error'); -const { TAG_CREATED, TAG_DELETED } = require('../event-type'); - -class TagService { - constructor({ tagStore, eventStore }, { getLogger }) { - this.tagStore = tagStore; - this.eventStore = eventStore; - this.logger = getLogger('services/tag-service.js'); - } - - async getTags() { - return this.tagStore.getAll(); - } - - async getTagsByType(type) { - return this.tagStore.getTagsByType(type); - } - - async getTag({ type, value }) { - return this.tagStore.getTag(type, value); - } - - async validateUnique(tag) { - try { - await this.tagStore.getTag(tag.type, tag.value); - } catch (err) { - if (err instanceof NotFoundError) { - return; - } - } - throw new NameExistsError(`A tag of ${tag} already exists`); - } - - async validate(tag) { - const data = await tagSchema.validateAsync(tag); - await this.validateUnique(tag); - return data; - } - - async createTag(tag, userName) { - const data = await this.validate(tag); - await this.tagStore.createTag(data); - await this.eventStore.store({ - type: TAG_CREATED, - createdBy: userName, - data, - }); - } - - async deleteTag(tag, userName) { - await this.tagStore.deleteTag(tag); - await this.eventStore.store({ - type: TAG_DELETED, - createdBy: userName, - data: tag, - }); - } -} - -module.exports = TagService; diff --git a/src/lib/services/tag-service.ts b/src/lib/services/tag-service.ts new file mode 100644 index 0000000000..1bdcc58bd9 --- /dev/null +++ b/src/lib/services/tag-service.ts @@ -0,0 +1,66 @@ +import { tagSchema } from './tag-schema'; +import TagStore, { ITag } from '../db/tag-store'; +import EventStore from '../db/event-store'; +import NameExistsError from '../error/name-exists-error'; +import { TAG_CREATED, TAG_DELETED } from '../event-type'; +import { Logger } from '../logger'; + +export default class TagService { + private tagStore: TagStore; + + private eventStore: EventStore; + + private logger: Logger; + + constructor({ tagStore, eventStore }, { getLogger }) { + this.tagStore = tagStore; + this.eventStore = eventStore; + this.logger = getLogger('services/tag-service.js'); + } + + async getTags(): Promise { + return this.tagStore.getAll(); + } + + async getTagsByType(type): Promise { + return this.tagStore.getTagsByType(type); + } + + async getTag({ type, value }: ITag): Promise { + return this.tagStore.getTag(type, value); + } + + async validateUnique(tag: ITag): Promise { + const exists = await this.tagStore.exists(tag); + if (exists) { + throw new NameExistsError(`A tag of ${tag} already exists`); + } + } + + async validate(tag): Promise { + const data = (await tagSchema.validateAsync(tag)) as ITag; + await this.validateUnique(tag); + return data; + } + + async createTag(tag: ITag, userName: string): Promise { + const data = await this.validate(tag); + await this.tagStore.createTag(data); + await this.eventStore.store({ + type: TAG_CREATED, + createdBy: userName, + data, + }); + } + + async deleteTag(tag: ITag, userName: string): Promise { + await this.tagStore.deleteTag(tag); + await this.eventStore.store({ + type: TAG_DELETED, + createdBy: userName, + data: tag, + }); + } +} + +module.exports = TagService; diff --git a/src/lib/services/tag-type-schema.js b/src/lib/services/tag-type-schema.ts similarity index 55% rename from src/lib/services/tag-type-schema.js rename to src/lib/services/tag-type-schema.ts index 46e0aa3577..3580ac11cd 100644 --- a/src/lib/services/tag-type-schema.js +++ b/src/lib/services/tag-type-schema.ts @@ -1,18 +1,15 @@ -'use strict'; +import Joi from 'joi'; +import { customJoi } from '../routes/admin-api/util'; -const joi = require('joi'); -const { customJoi } = require('../routes/admin-api/util'); - -const tagTypeSchema = joi - .object() +export const tagTypeSchema = Joi.object() .keys({ name: customJoi .isUrlFriendly() .min(2) .max(50) .required(), - description: joi.string().allow(''), - icon: joi.string().allow(''), + description: Joi.string().allow(''), + icon: Joi.string().allow(''), }) .options({ allowUnknown: false, diff --git a/src/lib/services/tag-type-service.js b/src/lib/services/tag-type-service.js deleted file mode 100644 index dffb279ad4..0000000000 --- a/src/lib/services/tag-type-service.js +++ /dev/null @@ -1,76 +0,0 @@ -const NameExistsError = require('../error/name-exists-error'); -const NotFoundError = require('../error/notfound-error'); -const { tagTypeSchema } = require('./tag-type-schema'); -const { - TAG_TYPE_CREATED, - TAG_TYPE_DELETED, - TAG_TYPE_UPDATED, -} = require('../event-type'); - -class TagTypeService { - constructor({ tagTypeStore, eventStore }, { getLogger }) { - this.tagTypeStore = tagTypeStore; - this.eventStore = eventStore; - this.logger = getLogger('services/tag-type-service.js'); - } - - async getAll() { - return this.tagTypeStore.getAll(); - } - - async getTagType(name) { - return this.tagTypeStore.getTagType(name); - } - - async createTagType(newTagType, userName) { - const data = await tagTypeSchema.validateAsync(newTagType); - await this.validateUnique(newTagType); - await this.tagTypeStore.createTagType(data); - await this.eventStore.store({ - type: TAG_TYPE_CREATED, - createdBy: userName || 'unleash-system', - data, - }); - return data; - } - - async validateUnique({ name }) { - try { - await this.tagTypeStore.getTagType(name); - } catch (err) { - if (err instanceof NotFoundError) { - return; - } - } - throw new NameExistsError( - `There already exists a tag-type with the name ${name}`, - ); - } - - async validate(tagType) { - await tagTypeSchema.validateAsync(tagType); - await this.validateUnique(tagType); - } - - async deleteTagType(name, userName) { - await this.tagTypeStore.deleteTagType(name); - await this.eventStore.store({ - type: TAG_TYPE_DELETED, - createdBy: userName || 'unleash-system', - data: { name }, - }); - } - - async updateTagType(updatedTagType, userName) { - const data = await tagTypeSchema.validateAsync(updatedTagType); - await this.tagTypeStore.updateTagType(data); - await this.eventStore.store({ - type: TAG_TYPE_UPDATED, - createdBy: userName || 'unleash-system', - data, - }); - return data; - } -} - -module.exports = TagTypeService; diff --git a/src/lib/services/tag-type-service.ts b/src/lib/services/tag-type-service.ts new file mode 100644 index 0000000000..8e29e6af7f --- /dev/null +++ b/src/lib/services/tag-type-service.ts @@ -0,0 +1,91 @@ +import NameExistsError from '../error/name-exists-error'; + +import { tagTypeSchema } from './tag-type-schema'; + +import { + TAG_TYPE_CREATED, + TAG_TYPE_DELETED, + TAG_TYPE_UPDATED, +} from '../event-type'; +import EventStore from '../db/event-store'; +import { Logger } from '../logger'; +import TagTypeStore, { ITagType } from '../db/tag-type-store'; + +export default class TagTypeService { + private tagTypeStore: TagTypeStore; + + private eventStore: EventStore; + + private logger: Logger; + + constructor({ tagTypeStore, eventStore }, { getLogger }) { + this.tagTypeStore = tagTypeStore; + this.eventStore = eventStore; + this.logger = getLogger('services/tag-type-service.js'); + } + + async getAll(): Promise { + return this.tagTypeStore.getAll(); + } + + async getTagType(name: string): Promise { + return this.tagTypeStore.getTagType(name); + } + + async createTagType( + newTagType: ITagType, + userName: string, + ): Promise { + const data = (await tagTypeSchema.validateAsync( + newTagType, + )) as ITagType; + await this.validateUnique(data); + await this.tagTypeStore.createTagType(data); + await this.eventStore.store({ + type: TAG_TYPE_CREATED, + createdBy: userName || 'unleash-system', + data, + }); + return data; + } + + async validateUnique({ name }: Partial): Promise { + const exists = await this.tagTypeStore.exists(name); + if (exists) { + throw new NameExistsError( + `There already exists a tag-type with the name ${name}`, + ); + } + return Promise.resolve(true); + } + + async validate(tagType: ITagType): Promise { + await tagTypeSchema.validateAsync(tagType); + await this.validateUnique(tagType); + } + + async deleteTagType(name: string, userName: string): Promise { + await this.tagTypeStore.deleteTagType(name); + await this.eventStore.store({ + type: TAG_TYPE_DELETED, + createdBy: userName || 'unleash-system', + data: { name }, + }); + } + + async updateTagType( + updatedTagType: ITagType, + userName: string, + ): Promise { + const data = await tagTypeSchema.validateAsync(updatedTagType); + await this.tagTypeStore.updateTagType(data); + await this.eventStore.store({ + type: TAG_TYPE_UPDATED, + createdBy: userName || 'unleash-system', + data, + }); + return data; + } +} + +module.exports = TagTypeService; diff --git a/src/test/fixtures/fake-tag-store.js b/src/test/fixtures/fake-tag-store.js index d821f8a6b3..c404af1b90 100644 --- a/src/test/fixtures/fake-tag-store.js +++ b/src/test/fixtures/fake-tag-store.js @@ -42,5 +42,9 @@ module.exports = (databaseIsUp = true) => { _tags.splice(0, _tags.length); return Promise.resolve(); }, + exists: tag => + Promise.resolve( + _tags.some(t => t.type === tag.type && t.value === tag.value), + ), }; }; diff --git a/src/test/fixtures/fake-tag-type-store.js b/src/test/fixtures/fake-tag-type-store.js index 920034d47a..aad24ac87e 100644 --- a/src/test/fixtures/fake-tag-type-store.js +++ b/src/test/fixtures/fake-tag-type-store.js @@ -22,5 +22,6 @@ module.exports = () => { _tagTypes.splice(0, _tagTypes.length); return Promise.resolve(); }, + exists: name => Promise.resolve(_tagTypes.some(t => t.name === name)), }; };