From c5a15e7376c68a6ab4a98ba22bc1c946e4ff68c0 Mon Sep 17 00:00:00 2001 From: ronaldsg Date: Fri, 30 Aug 2024 14:01:07 -0500 Subject: [PATCH 1/3] Refactor features with terms and conditions I've updated the features controller creating a new dataservice for the terms collections in order to retrieve the user only the enabled terms and conditions version available. --- .../unit/features.controller.unit.ts | 4 +- src/controllers/features.controller.ts | 17 +++++-- src/dependency-injection-bindings.ts | 1 + src/dependency-injection-handler.ts | 14 ++++-- src/models/features-data.model.ts | 13 +++--- src/models/index.ts | 1 + src/models/terms-db-data.model.ts | 31 +++++++++++++ src/services/features-mongo.service.ts | 2 +- src/services/terms-data.service.ts | 6 +++ src/services/terms-mongo.service.ts | 44 +++++++++++++++++++ 10 files changed, 118 insertions(+), 15 deletions(-) create mode 100644 src/models/terms-db-data.model.ts create mode 100644 src/services/terms-data.service.ts create mode 100644 src/services/terms-mongo.service.ts diff --git a/src/__tests__/unit/features.controller.unit.ts b/src/__tests__/unit/features.controller.unit.ts index d0e6cb9..79b6132 100644 --- a/src/__tests__/unit/features.controller.unit.ts +++ b/src/__tests__/unit/features.controller.unit.ts @@ -3,15 +3,17 @@ import { } from '@loopback/testlab'; import { FeaturesController } from '../../controllers/features.controller'; import { FeaturesDataService } from '../../services'; +import { TermsDataService } from '../../services/terms-data.service'; describe('FeaturesController (unit)', () => { const mockedService = {}; + const mockedTermsService = {}; let context = stubExpressContext(); describe('get()',() => { it('retrieves the features flags Information', async() => { - const controller = new FeaturesController(context.response, mockedService); + const controller = new FeaturesController(context.response, mockedService, mockedTermsService); await controller.get(); let result = await context.result; expect(result.payload).not.null(); diff --git a/src/controllers/features.controller.ts b/src/controllers/features.controller.ts index 705e5ef..831b0ab 100644 --- a/src/controllers/features.controller.ts +++ b/src/controllers/features.controller.ts @@ -4,18 +4,23 @@ import { RestBindings, get, getModelSchemaRef, Response, } from '@loopback/rest' import { ServicesBindings } from '../dependency-injection-bindings'; import { FeaturesDataService } from '../services/features-data.service'; import { FeaturesDbDataModel } from '../models/features-data.model'; +import { TermsDataService } from '../services/terms-data.service'; export class FeaturesController { logger: Logger; private featuresDatService: FeaturesDataService; + private termsDatService: TermsDataService; HTTP_SUCCESS_OK = 200; HTTP_ERROR = 500; constructor( @inject(RestBindings.Http.RESPONSE) private response: Response, @inject(ServicesBindings.FEATURES_SERVICE) featuresDatService: FeaturesDataService, + @inject(ServicesBindings.TERMS_SERVICE) + termsDatService: TermsDataService, ) { this.featuresDatService = featuresDatService; + this.termsDatService = termsDatService; this.logger = getLogger('features-controller'); } @@ -51,17 +56,21 @@ export class FeaturesController { }) public async get(): Promise { this.logger.debug('[get] started'); - let retorno = [new FeaturesDbDataModel()]; + let features = [new FeaturesDbDataModel()]; let responseCode = this.HTTP_ERROR; try { - retorno = await this.featuresDatService.getAll(); + features = await this.featuresDatService.getAll(); + const termsIdx = features.findIndex((feature) => feature.name === 'terms_and_conditions'); + this.logger.info(`[get] Retrieved terms idx: ${termsIdx}`); + const terms = await this.termsDatService.getVersion(features[termsIdx].version); + features[termsIdx].value = terms.value; responseCode = this.HTTP_SUCCESS_OK; - this.logger.info(`[get] Retrieved the features: ${JSON.stringify(retorno)}`); + this.logger.info(`[get] Retrieved the features: ${JSON.stringify(features)}`); } catch (e) { this.logger.warn(`[get] Got an error: ${e}`); } this.response.contentType('application/json').status(responseCode).send( - retorno + features ); return this.response; } diff --git a/src/dependency-injection-bindings.ts b/src/dependency-injection-bindings.ts index fecac51..e806619 100644 --- a/src/dependency-injection-bindings.ts +++ b/src/dependency-injection-bindings.ts @@ -38,4 +38,5 @@ export const ServicesBindings = { REGISTER_SERVICE: 'services.RegisterService', FEATURES_SERVICE: 'services.FeaturesDataService', FLYOVER_SERVICE: 'services.FlyoverService', + TERMS_SERVICE: 'services.TermsDataService', }; diff --git a/src/dependency-injection-handler.ts b/src/dependency-injection-handler.ts index 4df8edc..1b29e04 100644 --- a/src/dependency-injection-handler.ts +++ b/src/dependency-injection-handler.ts @@ -21,6 +21,7 @@ import {RskNodeService} from './services/rsk-node.service'; import {SyncStatusMongoService} from './services/sync-status-mongo.service'; import { PegoutDataProcessor } from './services/pegout-data.processor'; import { FeaturesMongoDbDataService } from './services/features-mongo.service'; +import { TermsMongoDbDataService } from './services/terms-mongo.service'; export class DependencyInjectionHandler { public static configureDependencies(app: Application): void { @@ -155,9 +156,14 @@ export class DependencyInjectionHandler { .toClass(RegisterService) .inScope(BindingScope.SINGLETON); - app - .bind(ServicesBindings.FEATURES_SERVICE) - .toClass(FeaturesMongoDbDataService) - .inScope(BindingScope.SINGLETON); + app + .bind(ServicesBindings.FEATURES_SERVICE) + .toClass(FeaturesMongoDbDataService) + .inScope(BindingScope.SINGLETON); + + app + .bind(ServicesBindings.TERMS_SERVICE) + .toClass(TermsMongoDbDataService) + .inScope(BindingScope.SINGLETON); } } diff --git a/src/models/features-data.model.ts b/src/models/features-data.model.ts index a07fc33..0cb9e06 100644 --- a/src/models/features-data.model.ts +++ b/src/models/features-data.model.ts @@ -5,7 +5,7 @@ export interface FeaturesDataModel { creationDate: Date; lastUpdateDate: Date; name: string; - value: string; + enabled: boolean; version: number; } @@ -17,7 +17,7 @@ export class FeaturesAppDataModel implements FeaturesDataModel{ creationDate: Date; lastUpdateDate: Date; name: string; - value: string; + enabled: boolean; version: number; } @@ -42,15 +42,18 @@ export class FeaturesDbDataModel implements SearchableModel, FeaturesDataModel { name: string; @property({ - type: 'string', + type: 'boolean', }) - value: string; + enabled: boolean; @property({ type: 'number', }) version: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [prop: string]: any; + getId() { return this.name; } @@ -63,7 +66,7 @@ export class FeaturesDbDataModel implements SearchableModel, FeaturesDataModel { features.creationDate = other.creationDate; features.lastUpdateDate = other.lastUpdateDate; features.name = other.name; - features.value = other.value; + features.enabled = other.enabled; features.version = other.version; return features; } diff --git a/src/models/index.ts b/src/models/index.ts index 088a3de..7ebc80c 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -19,3 +19,4 @@ export * from './fee-amount.model'; export * from './register-payload.model'; export * from './features.model'; export * from './pegout-status.model'; +export * from './terms-db-data.model'; diff --git a/src/models/terms-db-data.model.ts b/src/models/terms-db-data.model.ts new file mode 100644 index 0000000..aa312e1 --- /dev/null +++ b/src/models/terms-db-data.model.ts @@ -0,0 +1,31 @@ +import { model, property} from '@loopback/repository'; +import { SearchableModel } from "./rsk/searchable-model"; + +@model() +export class TermsDbDataModel implements SearchableModel { + + @property({ + type: 'number', + required: true, + }) + version: number; + + @property({ + type: 'string', + required: true, + }) + value: string; + + + constructor(data?: Partial) { + Object.assign(this, data);; + } + + getId() { + return this.version; + } + // eslint-disable-next-line class-methods-use-this + getIdFieldName(): string { + return "version"; + } +} diff --git a/src/services/features-mongo.service.ts b/src/services/features-mongo.service.ts index 57b8ce2..335792b 100644 --- a/src/services/features-mongo.service.ts +++ b/src/services/features-mongo.service.ts @@ -15,7 +15,7 @@ const FeaturesSchema = new mongoose.Schema({ creationDate: {type: Date}, lastUpdateDate: {type: Date}, name: {type: String, required: true}, - value: {type: String, required: true}, + enabled: {type: Boolean, required: true}, version: {type: Number, required: true}, }); diff --git a/src/services/terms-data.service.ts b/src/services/terms-data.service.ts new file mode 100644 index 0000000..5059ee7 --- /dev/null +++ b/src/services/terms-data.service.ts @@ -0,0 +1,6 @@ +import { GenericDataService } from './generic-data-service'; +import { TermsDbDataModel } from '../models'; + +export interface TermsDataService extends GenericDataService { + getVersion(version: number): Promise; +} diff --git a/src/services/terms-mongo.service.ts b/src/services/terms-mongo.service.ts new file mode 100644 index 0000000..a25b3d4 --- /dev/null +++ b/src/services/terms-mongo.service.ts @@ -0,0 +1,44 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ +import mongoose from 'mongoose'; +import {MongoDbDataService} from './mongodb-data.service'; +import { TermsDbDataModel } from '../models'; +import { TermsDataService } from './terms-data.service'; + +/* +- THESE MODEL INTERFACES AND CLASSES ARE REQUIRED FOR MONGO BUT WE DON'T WANT THEM EXPOSED OUT OF THIS LAYER +*/ +interface TermsMongoModel extends mongoose.Document, TermsDbDataModel { +} + +const TermsSchema = new mongoose.Schema({ + version: {type: Number, required: true}, + value: {type: String, required: true}, +}); + +const TermsConnector = mongoose.model("Terms", TermsSchema); + +export class TermsMongoDbDataService extends MongoDbDataService implements TermsDataService { + protected getByIdFilter(id: any) { + throw new Error('Method not implemented.'); + } + protected getManyFilter(filter?: any) { + throw new Error('Method not implemented.'); + } + protected getLoggerName(): string { + return 'TermsMongoService'; + } + protected getConnector(): mongoose.Model { + this.verifyAndCreateConnectionIfIsNecessary(); + return TermsConnector; + } + async verifyAndCreateConnectionIfIsNecessary() { + await this.ensureConnection(); + } + public async getVersion(version: number): Promise { + const [document] = await this.getConnector() + .find({ version }) + .exec(); + return document; + } + +} From 4a5ca887335c129dc14c457fadded52e2af6d293 Mon Sep 17 00:00:00 2001 From: ronaldsg Date: Thu, 19 Sep 2024 17:09:26 -0500 Subject: [PATCH 2/3] Update the features call response code when are not stored the terms --- .../unit/features.controller.unit.ts | 24 +++++++--- src/controllers/features.controller.ts | 44 ++++++++++++------- src/models/features-data.model.ts | 17 ++++--- 3 files changed, 58 insertions(+), 27 deletions(-) diff --git a/src/__tests__/unit/features.controller.unit.ts b/src/__tests__/unit/features.controller.unit.ts index 79b6132..451f784 100644 --- a/src/__tests__/unit/features.controller.unit.ts +++ b/src/__tests__/unit/features.controller.unit.ts @@ -1,22 +1,36 @@ import { + createStubInstance, expect, stubExpressContext, } from '@loopback/testlab'; import { FeaturesController } from '../../controllers/features.controller'; import { FeaturesDataService } from '../../services'; import { TermsDataService } from '../../services/terms-data.service'; +import { StubbedInstanceWithSinonAccessor} from "@loopback/testlab"; +import { FeaturesMongoDbDataService } from '../../services/features-mongo.service'; +import { FeaturesDbDataModel } from '../../models/features-data.model'; +import { TermsMongoDbDataService } from '../../services/terms-mongo.service'; describe('FeaturesController (unit)', () => { - const mockedService = {}; - const mockedTermsService = {}; + let mockedService: StubbedInstanceWithSinonAccessor; + let mockedTermsService:StubbedInstanceWithSinonAccessor; let context = stubExpressContext(); + mockedService = createStubInstance(FeaturesMongoDbDataService); + mockedTermsService = createStubInstance(TermsMongoDbDataService); describe('get()',() => { it('retrieves the features flags Information', async() => { + mockedService.stubs.getAll.resolves([FeaturesDbDataModel.clone({ + name: 'feature1', value: true, + creationDate: undefined, + lastUpdateDate: undefined, + enabled: false, + version: 0, + })]); + mockedTermsService.stubs.getVersion.resolves(); const controller = new FeaturesController(context.response, mockedService, mockedTermsService); - await controller.get(); - let result = await context.result; - expect(result.payload).not.null(); + const response = await controller.get(); + expect(response.statusCode).to.equal(200); }); }); diff --git a/src/controllers/features.controller.ts b/src/controllers/features.controller.ts index 831b0ab..a22538d 100644 --- a/src/controllers/features.controller.ts +++ b/src/controllers/features.controller.ts @@ -55,23 +55,35 @@ export class FeaturesController { }, }) public async get(): Promise { - this.logger.debug('[get] started'); - let features = [new FeaturesDbDataModel()]; - let responseCode = this.HTTP_ERROR; - try { - features = await this.featuresDatService.getAll(); - const termsIdx = features.findIndex((feature) => feature.name === 'terms_and_conditions'); + return new Promise((resolve) => { + this.logger.debug('[get] started'); + let features = [new FeaturesDbDataModel()]; + let responseCode = this.HTTP_ERROR; + this.featuresDatService.getAll() + .then((featuresFromDb) => { + features = featuresFromDb; + const termsIdx = featuresFromDb.findIndex((feature) => feature.name === 'terms_and_conditions'); + if (!termsIdx) { + responseCode = this.HTTP_SUCCESS_OK; + this.response.contentType('application/json').status(responseCode) + .send(features); + resolve(this.response); + } this.logger.info(`[get] Retrieved terms idx: ${termsIdx}`); - const terms = await this.termsDatService.getVersion(features[termsIdx].version); - features[termsIdx].value = terms.value; - responseCode = this.HTTP_SUCCESS_OK; + return Promise.all([this.termsDatService.getVersion(features[termsIdx].version), termsIdx]); + }) + .then(([terms, termsIdx]) => { + features[termsIdx].value = terms ? terms.value : 'Version not found'; this.logger.info(`[get] Retrieved the features: ${JSON.stringify(features)}`); - } catch (e) { - this.logger.warn(`[get] Got an error: ${e}`); - } - this.response.contentType('application/json').status(responseCode).send( - features - ); - return this.response; + responseCode = this.HTTP_SUCCESS_OK; + this.response.contentType('application/json').status(responseCode) + .send(features); + resolve(this.response); + }) + .catch((error) => { + this.logger.warn(`[get] Got an error: ${error}`); + resolve(this.response); + }); + }); } } diff --git a/src/models/features-data.model.ts b/src/models/features-data.model.ts index 0cb9e06..b6987cd 100644 --- a/src/models/features-data.model.ts +++ b/src/models/features-data.model.ts @@ -61,13 +61,18 @@ export class FeaturesDbDataModel implements SearchableModel, FeaturesDataModel { return 'name'; } - public static clone(other: FeaturesDbDataModel): FeaturesDbDataModel { + public static clone(other: Partial): FeaturesDbDataModel { + const sanitizedData: Partial = {}; const features: FeaturesDbDataModel = new FeaturesDbDataModel(); - features.creationDate = other.creationDate; - features.lastUpdateDate = other.lastUpdateDate; - features.name = other.name; - features.enabled = other.enabled; - features.version = other.version; + Object.entries(other).forEach(([key, value]) => { + const theKey = key as keyof FeaturesDbDataModel; + if (value !== undefined) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + sanitizedData[theKey] = value; + } + }); + Object.assign(features, sanitizedData); return features; } From b97b78337d9eecf09fb8074be9f0b9064c77bd3f Mon Sep 17 00:00:00 2001 From: ronaldsg Date: Mon, 23 Sep 2024 15:29:44 -0500 Subject: [PATCH 3/3] Fixing model cloning and tests --- src/__tests__/unit/features.controller.unit.ts | 12 ++++++------ src/controllers/features.controller.ts | 1 - src/models/features-data.model.ts | 18 +++++++----------- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/__tests__/unit/features.controller.unit.ts b/src/__tests__/unit/features.controller.unit.ts index 451f784..c306639 100644 --- a/src/__tests__/unit/features.controller.unit.ts +++ b/src/__tests__/unit/features.controller.unit.ts @@ -7,26 +7,26 @@ import { FeaturesDataService } from '../../services'; import { TermsDataService } from '../../services/terms-data.service'; import { StubbedInstanceWithSinonAccessor} from "@loopback/testlab"; import { FeaturesMongoDbDataService } from '../../services/features-mongo.service'; -import { FeaturesDbDataModel } from '../../models/features-data.model'; import { TermsMongoDbDataService } from '../../services/terms-mongo.service'; describe('FeaturesController (unit)', () => { let mockedService: StubbedInstanceWithSinonAccessor; let mockedTermsService:StubbedInstanceWithSinonAccessor; + let featuresGetAllStub: sinon.SinonStub; let context = stubExpressContext(); mockedService = createStubInstance(FeaturesMongoDbDataService); mockedTermsService = createStubInstance(TermsMongoDbDataService); - + featuresGetAllStub = mockedService.getAll as sinon.SinonStub; describe('get()',() => { it('retrieves the features flags Information', async() => { - mockedService.stubs.getAll.resolves([FeaturesDbDataModel.clone({ + featuresGetAllStub.resolves([{ name: 'feature1', value: true, - creationDate: undefined, - lastUpdateDate: undefined, + creationDate: new Date(), + lastUpdateDate: new Date(), enabled: false, version: 0, - })]); + }]); mockedTermsService.stubs.getVersion.resolves(); const controller = new FeaturesController(context.response, mockedService, mockedTermsService); const response = await controller.get(); diff --git a/src/controllers/features.controller.ts b/src/controllers/features.controller.ts index a22538d..cde4eb7 100644 --- a/src/controllers/features.controller.ts +++ b/src/controllers/features.controller.ts @@ -69,7 +69,6 @@ export class FeaturesController { .send(features); resolve(this.response); } - this.logger.info(`[get] Retrieved terms idx: ${termsIdx}`); return Promise.all([this.termsDatService.getVersion(features[termsIdx].version), termsIdx]); }) .then(([terms, termsIdx]) => { diff --git a/src/models/features-data.model.ts b/src/models/features-data.model.ts index b6987cd..2c3cd33 100644 --- a/src/models/features-data.model.ts +++ b/src/models/features-data.model.ts @@ -61,18 +61,14 @@ export class FeaturesDbDataModel implements SearchableModel, FeaturesDataModel { return 'name'; } - public static clone(other: Partial): FeaturesDbDataModel { - const sanitizedData: Partial = {}; + public static clone(other:FeaturesDbDataModel): FeaturesDbDataModel { const features: FeaturesDbDataModel = new FeaturesDbDataModel(); - Object.entries(other).forEach(([key, value]) => { - const theKey = key as keyof FeaturesDbDataModel; - if (value !== undefined) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - sanitizedData[theKey] = value; - } - }); - Object.assign(features, sanitizedData); + features.creationDate = other.creationDate; + features.lastUpdateDate = other.lastUpdateDate; + features.name = other.name; + features.value = other.value; + features.version = other.version; + features.enabled = other.enabled; return features; }