diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f63f2b2d..95a64eba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,17 +1,6 @@ name: CI -on: - push: - branches: - - release - - release/* - - feature/* - - fix/* - - develop - - master - - tags: - - '*' +on: push env: NPM_TOKEN: ${{secrets.GA_TOKEN}} diff --git a/packages/nde-erfgoed-core/lib/errors/argument-error.ts b/packages/nde-erfgoed-core/lib/errors/argument-error.ts new file mode 100644 index 00000000..62904fe9 --- /dev/null +++ b/packages/nde-erfgoed-core/lib/errors/argument-error.ts @@ -0,0 +1,21 @@ +import { BaseError } from './base-error'; + +/** + * An error thrown when a function is called with invalid arguments. + */ +export class ArgumentError extends BaseError { + public readonly name = ArgumentError.name; + + /** + * Instantiates the error. + * + * @param message A message which describes the error. + * @param value The value of the invalid argument. + * @param cause The underlying error. + */ + constructor(message: string, public value: unknown, cause?: Error) { + super(message, cause); + + Object.setPrototypeOf(this, ArgumentError.prototype); + } +} diff --git a/packages/nde-erfgoed-core/lib/errors/base-error.ts b/packages/nde-erfgoed-core/lib/errors/base-error.ts new file mode 100644 index 00000000..dcea3cce --- /dev/null +++ b/packages/nde-erfgoed-core/lib/errors/base-error.ts @@ -0,0 +1,18 @@ +/** + * A base error which takes a cause. + */ +export class BaseError extends Error { + public readonly name = BaseError.name; + + /** + * Instantiates an error. + * + * @param messsage Describes the error. + * @param cause The underlying cause of the error. + */ + constructor(messsage: string, public cause: Error) { + super(messsage); + + Object.setPrototypeOf(this, BaseError.prototype); + } +} diff --git a/packages/nde-erfgoed-core/lib/index.ts b/packages/nde-erfgoed-core/lib/index.ts index ec2ae53b..09cbc2de 100644 --- a/packages/nde-erfgoed-core/lib/index.ts +++ b/packages/nde-erfgoed-core/lib/index.ts @@ -2,6 +2,11 @@ * Exports the modules of the package. */ export * from './collections/collection'; +export * from './errors/argument-error'; +export * from './errors/base-error'; +export * from './logging/console-logger'; +export * from './logging/logger-level'; +export * from './logging/logger'; export * from './stores/memory-store'; export * from './stores/resource'; export * from './stores/store'; diff --git a/packages/nde-erfgoed-core/lib/logging/console-logger.spec.ts b/packages/nde-erfgoed-core/lib/logging/console-logger.spec.ts new file mode 100644 index 00000000..cb9f1fbc --- /dev/null +++ b/packages/nde-erfgoed-core/lib/logging/console-logger.spec.ts @@ -0,0 +1,89 @@ +import { ConsoleLogger } from './console-logger'; +import { LoggerLevel } from './logger-level'; + +describe('ConsoleLogger', () => { + let service: ConsoleLogger; + + beforeEach(async () => { + service = new ConsoleLogger(LoggerLevel.silly, LoggerLevel.silly); + }); + + afterEach(() => { + // clear spies + jest.clearAllMocks(); + }); + + it('should be correctly instantiated', () => { + expect(service).toBeTruthy(); + }); + + describe('log', () => { + + const levels = [ 'info', 'debug', 'warn', 'error' ]; + + it('LoggerLevel.silly should call console.log', () => { + const consoleSpy = jest.spyOn(console, 'log'); + service.log(LoggerLevel.silly, 'TestService', 'test message', 'data'); + expect(consoleSpy).toHaveBeenCalled(); + }); + + for (const level of levels) { + if (level) { + it(`LoggerLevel.${level} should call console.${level}`, () => { + const consoleSpy = jest.spyOn(console, level as any); + service.log(LoggerLevel[level], 'TestService', 'test message', 'data'); + expect(consoleSpy).toHaveBeenCalled(); + }); + } + } + + const params = { + level: LoggerLevel.info, + typeName: ' TestService', + message: 'test message', + }; + const args = Object.keys(params); + args.forEach((argument) => { + it(`should throw error when ${argument} is null or undefined`, () => { + const testArgs = args.map((arg) => arg === argument ? null : arg); + expect(() => service.log.apply(service.log, testArgs)) + .toThrow(`${argument} should be set`); + }); + }); + }); + + describe('level logs', () => { + + const levels = [ 'info', 'debug', 'warn', 'error' ]; + + for (const level of levels) { + if (level) { + it(`should log a ${level} message`, () => { + const loggerSpy = jest.spyOn(service, 'log'); + if (level === 'error') { + service[level]('TestService', 'test message', 'test error', 'error'); + expect(loggerSpy).toHaveBeenCalledWith(LoggerLevel.error, 'TestService', 'test message', { error: 'test error', caught: 'error' }); + } else { + service[level]('TestService', 'test message', 'test data'); + expect(loggerSpy).toHaveBeenCalledWith(LoggerLevel[level], 'TestService', 'test message', 'test data'); + } + }); + + // test arguments for null or undefined + const params = { + level: LoggerLevel.info, + typeName: ' TestService', + }; + const args = Object.keys(params); + args.forEach((argument) => { + it(`should throw error when ${argument} is null or undefined`, () => { + const testArgs = args.map((arg) => arg === argument ? null : arg); + expect(() => service.log.apply(service[level], testArgs)) + .toThrow(`${argument} should be set`); + }); + }); + } + } + }); + +}); diff --git a/packages/nde-erfgoed-core/lib/logging/console-logger.ts b/packages/nde-erfgoed-core/lib/logging/console-logger.ts new file mode 100644 index 00000000..3608cf80 --- /dev/null +++ b/packages/nde-erfgoed-core/lib/logging/console-logger.ts @@ -0,0 +1,76 @@ +/* eslint-disable no-console -- this is a logger service */ + +import { ArgumentError } from '../errors/argument-error'; +import { Logger } from './logger'; +import { LoggerLevel } from './logger-level'; + +/** + * JavaScript console-based logger service + */ +export class ConsoleLogger extends Logger { + + /** + * Instantiates the logger. + * + * @param minimumLevel The minimum level of a log to be printed. + * @param minimumLevelPrintData The minimum level of a log for data to be printed. + */ + constructor( + protected readonly minimumLevel: LoggerLevel, + protected readonly minimumLevelPrintData: LoggerLevel, + ) { + super(minimumLevel, minimumLevelPrintData); + } + + /** + * Logs a message + * + * @param level Severity level of the log + * @param typeName The location of the log + * @param message Message that should be logged + * @param data Any relevant data that should be logged + */ + log(level: LoggerLevel, typeName: string, message: string, data?: any) { + if (level === null || level === undefined) { + throw new ArgumentError('level should be set', typeName); + } + + if (!typeName) { + throw new ArgumentError('typeName should be set', typeName); + } + + if (!message) { + throw new ArgumentError('message should be set', message); + } + + const timestamp: string = new Date().toISOString(); + + if (level <= this.minimumLevel) { + const logMessage = `[${timestamp} ${typeName}] ${message}`; + const logData = level >= this.minimumLevelPrintData ? '' : data||''; + const log = [ logMessage, logData ]; + switch (level) { + + case LoggerLevel.info: + console.info(...log); + break; + + case LoggerLevel.debug: + console.debug(...log); + break; + + case LoggerLevel.warn: + console.warn(...log); + break; + + case LoggerLevel.error: + console.error(...log); + break; + + default: + console.log(...log); + break; + } + } + } +} diff --git a/packages/nde-erfgoed-core/lib/logging/logger-level.ts b/packages/nde-erfgoed-core/lib/logging/logger-level.ts new file mode 100644 index 00000000..af23204e --- /dev/null +++ b/packages/nde-erfgoed-core/lib/logging/logger-level.ts @@ -0,0 +1,12 @@ +/** + * Level of log severity based on node.js' + */ +export enum LoggerLevel { + error = 0, + warn = 1, + info = 2, + http = 3, + verbose = 4, + debug = 5, + silly = 6, +} diff --git a/packages/nde-erfgoed-core/lib/logging/logger.ts b/packages/nde-erfgoed-core/lib/logging/logger.ts new file mode 100644 index 00000000..b8a8006f --- /dev/null +++ b/packages/nde-erfgoed-core/lib/logging/logger.ts @@ -0,0 +1,109 @@ +/* eslint-disable no-console -- this is a logger service */ + +import { ArgumentError } from '../errors/argument-error'; +import { LoggerLevel } from './logger-level'; + +/** + * An abstract definition of a logger. + */ +export abstract class Logger { + + /** + * Instantiates the logger. + * + * @param minimumLevel The minimum level of a log to be printed. + * @param minimumLevelPrintData The minimum level of a log for data to be printed. + */ + constructor( + protected readonly minimumLevel: LoggerLevel, + protected readonly minimumLevelPrintData: LoggerLevel, + ) {} + + /** + * Logs an info message + * + * @param typeName The location of the log + * @param message Message that should be logged + * @param data Any relevant data that should be logged + */ + info(typeName: string, message: string, data?: any) { + if (!typeName) { + throw new ArgumentError('Typename should be set', typeName); + } + + if (!message) { + throw new ArgumentError('Message should be set', message); + } + + this.log(LoggerLevel.info, typeName, message, data); + } + + /** + * Logs a debug message + * + * @param typeName The location of the log + * @param message Message that should be logged + * @param data Any relevant data that should be logged + */ + debug(typeName: string, message: string, data?: any) { + if (!typeName) { + throw new ArgumentError('Typename should be set', typeName); + } + + if (!message) { + throw new ArgumentError('Message should be set', message); + } + + this.log(LoggerLevel.debug, typeName, message, data); + } + + /** + * Logs a warning message + * + * @param typeName The location of the log + * @param message Message that should be logged + * @param data Any relevant data that should be logged + */ + warn(typeName: string, message: string, data?: any) { + if (!typeName) { + throw new ArgumentError('Typename should be set', typeName); + } + + if (!message) { + throw new ArgumentError('Message should be set', message); + } + + this.log(LoggerLevel.warn, typeName, message, data); + } + + /** + * Logs an error message + * + * @param typeName The location of the log + * @param message Message that should be logged + * @param error The error that was thrown + * @param caught The error that was caught + */ + error(typeName: string, message: string, error?: Error | any, caught?: any) { + if (!typeName) { + throw new ArgumentError('Typename should be set', typeName); + } + + if (!message) { + throw new ArgumentError('Message should be set', message); + } + + this.log(LoggerLevel.error, typeName, message, { error, caught }); + } + + /** + * Logs a message + * + * @param level Severity level of the log + * @param typeName The location of the log + * @param message Message that should be logged + * @param data Any relevant data that should be logged + */ + abstract log(level: LoggerLevel, typeName: string, message: string, data?: any): void; + +} diff --git a/packages/nde-erfgoed-core/tsconfig.json b/packages/nde-erfgoed-core/tsconfig.json index 49710eda..063b5447 100644 --- a/packages/nde-erfgoed-core/tsconfig.json +++ b/packages/nde-erfgoed-core/tsconfig.json @@ -25,6 +25,7 @@ "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "downlevelIteration": true, + "suppressImplicitAnyIndexErrors": true, }, "include": ["lib"], "exclude": [ diff --git a/packages/nde-erfgoed-manage/lib/app.root.ts b/packages/nde-erfgoed-manage/lib/app.root.ts index b91d3b71..f3b3b03f 100644 --- a/packages/nde-erfgoed-manage/lib/app.root.ts +++ b/packages/nde-erfgoed-manage/lib/app.root.ts @@ -4,6 +4,7 @@ import { interpret, State } from 'xstate'; import { RxLitElement } from 'rx-lit'; import { from } from 'rxjs'; import { tap } from 'rxjs/operators'; +import { ConsoleLogger, Logger, LoggerLevel } from '@digita-ai/nde-erfgoed-core'; import { AppActors, appMachine } from './app.machine'; import { CollectionsRootComponent } from './features/collections/root/collections-root.component'; import { AppStates } from './app.states'; @@ -14,6 +15,8 @@ import { AppContext } from './app.context'; */ export class AppRootComponent extends RxLitElement { + private logger: Logger = new ConsoleLogger(LoggerLevel.silly, LoggerLevel.silly); + /** * The constructor of the application root component, * which starts the root machine actor. @@ -45,7 +48,7 @@ export class AppRootComponent extends RxLitElement { super.firstUpdated(changed); this.subscribe('state', from(this.actor).pipe( - tap((state) => console.log('AppState change', state)), + tap((state) => this.logger.debug(CollectionsRootComponent.name, 'AppState change:', state)), )); } diff --git a/packages/nde-erfgoed-manage/lib/features/collections/root/collections-root.component.ts b/packages/nde-erfgoed-manage/lib/features/collections/root/collections-root.component.ts index cbc2b84a..022ec44f 100644 --- a/packages/nde-erfgoed-manage/lib/features/collections/root/collections-root.component.ts +++ b/packages/nde-erfgoed-manage/lib/features/collections/root/collections-root.component.ts @@ -1,6 +1,5 @@ -/* eslint-disable no-console */ import { css, html, property, PropertyValues, internalProperty } from 'lit-element'; -import { Collection } from '@digita-ai/nde-erfgoed-core'; +import { Collection, ConsoleLogger, Logger, LoggerLevel } from '@digita-ai/nde-erfgoed-core'; import { map, tap } from 'rxjs/operators'; import { from, of } from 'rxjs'; import { SpawnedActorRef, State} from 'xstate'; @@ -14,6 +13,8 @@ import { CollectionsStates } from '../collections.states'; */ export class CollectionsRootComponent extends RxLitElement { + private logger: Logger = new ConsoleLogger(LoggerLevel.silly, LoggerLevel.silly); + /** * The actor controlling this component. */ @@ -40,7 +41,7 @@ export class CollectionsRootComponent extends RxLitElement { super.firstUpdated(changed); this.subscribe('state', from(this.actor).pipe( - tap((state) => console.log('CollectionState change:', state)), + tap((state) => this.logger.debug(CollectionsRootComponent.name, 'CollectionState change:', state)), )); this.subscribe('collections', from(this.actor).pipe(