diff --git a/.release-it.json b/.release-it.json index 32d66d39..64086512 100644 --- a/.release-it.json +++ b/.release-it.json @@ -3,7 +3,7 @@ "git": { "commitMessage": "chore: release v${version}", "tagName": "v${version}", - "push": false + "push": true }, "github": { "release": true, diff --git a/config/default.yml b/config/default.yml index c2884744..a887a343 100644 --- a/config/default.yml +++ b/config/default.yml @@ -1,2 +1,4 @@ db: uri: "mongodb://db/enmon-adapter" + +enmon: diff --git a/src/app.module.ts b/src/app.module.ts index b943b4fd..01b35c2e 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -18,7 +18,7 @@ import { Config } from './config/schemas.js'; useFactory: (config: ConfigService) => ({ processEvery: '15 seconds', db: { - address: config.get('db.uri', { infer: true }), + address: config.getOrThrow('db.uri', { infer: true }), }, }), }), @@ -26,7 +26,7 @@ import { Config } from './config/schemas.js'; MongooseModule.forRootAsync({ inject: [ConfigService], useFactory: (config: ConfigService) => ({ - uri: config.get('db.uri', { infer: true }), + uri: config.getOrThrow('db.uri', { infer: true }), }), }), ConfigModule.forRoot({ @@ -34,7 +34,12 @@ import { Config } from './config/schemas.js'; load: [configuration], cache: false, }), - EnmonModule, + EnmonModule.forRootAsync({ + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + contactEmail: config.getOrThrow('enmon.contactEmail', { infer: true }), + }), + }), WinstonModule.forRootAsync({ inject: [ConfigService], useFactory: (config: ConfigService) => ({ diff --git a/src/config/__snapshots__/parse.spec.ts.snap b/src/config/__snapshots__/parse.spec.ts.snap index 05701353..d41d0984 100644 --- a/src/config/__snapshots__/parse.spec.ts.snap +++ b/src/config/__snapshots__/parse.spec.ts.snap @@ -20,6 +20,15 @@ exports[`should reject invalid config 0 1`] = ` ], "received": "undefined", }, + { + "code": "invalid_type", + "expected": "object", + "message": "Required", + "path": [ + "enmon", + ], + "received": "undefined", + }, { "code": "invalid_type", "expected": "'UNI7xxx' | 'UNI1xxx'", @@ -87,6 +96,15 @@ exports[`should reject invalid config 1 1`] = ` ], "received": "undefined", }, + { + "code": "invalid_type", + "expected": "object", + "message": "Required", + "path": [ + "enmon", + ], + "received": "undefined", + }, { "code": "invalid_enum_value", "message": "Invalid enum value. Expected 'UNI7xxx' | 'UNI1xxx', received 'invalid model'", @@ -235,6 +253,15 @@ exports[`should reject invalid config 2 1`] = ` ], "validation": "url", }, + { + "code": "invalid_string", + "message": "Invalid email", + "path": [ + "enmon", + "contactEmail", + ], + "validation": "email", + }, { "code": "invalid_enum_value", "message": "Invalid enum value. Expected 'UNI7xxx' | 'UNI1xxx', received 'invalid model'", diff --git a/src/config/parse.spec.ts b/src/config/parse.spec.ts index 2d308280..103052bb 100644 --- a/src/config/parse.spec.ts +++ b/src/config/parse.spec.ts @@ -13,6 +13,9 @@ const VALID_CONFIG_INPUT = { db: { uri: 'mongodb://host/dbname', }, + enmon: { + contactEmail: faker.internet.email(), + }, thermometers: [ { model: ThermometerModel.UNI1xxx, @@ -49,6 +52,7 @@ const VALID_CONFIG_INPUT = { const VALID_CONFIG_OUTPUT = { DEV: VALID_CONFIG_INPUT.DEV, db: VALID_CONFIG_INPUT.db, + enmon: VALID_CONFIG_INPUT.enmon, thermometers: VALID_CONFIG_INPUT.thermometers, wattrouters: [ { @@ -86,6 +90,9 @@ it.each([ db: { uri: 'invalid URL', }, + enmon: { + contactEmail: 'invalid email', + }, thermometer: { dataSourceUrl: 'invalid URL', enmon: { customerId: 'invalid ID', devEUI: '', env: 'invalid EnmonEnv', token: '' }, diff --git a/src/config/schemas.ts b/src/config/schemas.ts index 3924d06d..f61331bf 100644 --- a/src/config/schemas.ts +++ b/src/config/schemas.ts @@ -8,6 +8,9 @@ export const configSchema = z db: z.object({ uri: z.string().url(), }), + enmon: z.object({ + contactEmail: z.string().email().optional(), + }), thermometers: z.array(configThermometerSchema).nullish(), wattrouter: configWattRouterSchema.nullish(), wattrouters: z.array(configWattRouterSchema).nullish(), diff --git a/src/enmon/ApiClient.ts b/src/enmon/ApiClient.ts index 19919435..abc7262a 100644 --- a/src/enmon/ApiClient.ts +++ b/src/enmon/ApiClient.ts @@ -34,7 +34,7 @@ export interface PostMeterPlainValueArgs { } export class EnmonApiClient { - constructor(public readonly env = EnmonEnv.App) {} + constructor(public readonly contactEmail?: string | undefined) {} async postMeterPlainCounterMulti({ env, @@ -70,7 +70,10 @@ export class EnmonApiClient { private http({ env }: { env?: EnmonEnv | undefined }): AxiosInstance { return axios.create({ - baseURL: `https://${env ?? this.env}.enmon.tech`, + baseURL: `https://${env ?? EnmonEnv.App}.enmon.tech`, + headers: { + 'User-Agent': `enmon-adapter (${this.contactEmail ?? ''})`, + }, }); } } diff --git a/src/enmon/enmon.module.ts b/src/enmon/enmon.module.ts index 55401f53..dc712f2c 100644 --- a/src/enmon/enmon.module.ts +++ b/src/enmon/enmon.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { DynamicModule, FactoryProvider, Inject, Logger, OnApplicationBootstrap } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { AgendaModule } from 'agenda-nest'; import { ReadingProcessor } from './reading.processor.js'; @@ -7,24 +7,53 @@ import { UploadReadingRepository } from './upload-reading.repository.js'; import { EnmonApiClient } from './ApiClient.js'; import { READINGS_QUEUE_NAME } from './constants.js'; -@Module({ - imports: [ - AgendaModule.registerQueue(READINGS_QUEUE_NAME), - MongooseModule.forFeature([ - { - name: UPLOAD_READING_MODEL_NAME, - schema: UploadReadingSchema, - }, - ]), - ], - providers: [ - ReadingProcessor, - UploadReadingRepository, - { - provide: EnmonApiClient, - useClass: EnmonApiClient, - }, - ], - exports: [UploadReadingRepository], -}) -export class EnmonModule {} +export interface ModuleOptions { + contactEmail?: string | undefined; +} + +const OPTIONS_TOKEN = Symbol('enmon/options'); + +export class EnmonModule implements OnApplicationBootstrap { + private readonly logger = new Logger(EnmonModule.name); + + constructor( + @Inject(OPTIONS_TOKEN) + private readonly opts: ModuleOptions, + ) {} + + static forRootAsync(options: Pick, 'inject' | 'useFactory'>): DynamicModule { + return { + module: EnmonModule, + imports: [ + AgendaModule.registerQueue(READINGS_QUEUE_NAME), + MongooseModule.forFeature([ + { + name: UPLOAD_READING_MODEL_NAME, + schema: UploadReadingSchema, + }, + ]), + ], + providers: [ + { + provide: OPTIONS_TOKEN, + inject: options.inject, + useFactory: options.useFactory, + } as FactoryProvider, + ReadingProcessor, + UploadReadingRepository, + { + provide: EnmonApiClient, + inject: [OPTIONS_TOKEN], + useFactory: (opts: ModuleOptions) => new EnmonApiClient(opts.contactEmail), + }, + ], + exports: [UploadReadingRepository], + }; + } + + onApplicationBootstrap() { + if (!this.opts.contactEmail) { + this.logger.warn('Contact email not provided!'); + } + } +} diff --git a/tests/configuration.ts b/tests/configuration.ts index 7045d1ce..02f2c8b6 100644 --- a/tests/configuration.ts +++ b/tests/configuration.ts @@ -10,6 +10,9 @@ export default (): Config => db: { uri: 'mongodb://db/dbname', }, + enmon: { + contactEmail: 'test@test.tst', + }, thermometers: [ { model: ThermometerModel.UNI7xxx,